Functions & Let
Let Declarations
Section titled “Let Declarations”All bindings are immutable. Use let:
let name = "Floe"let count = 42let active = trueWith type annotations:
let name: string = "Floe"let count: number = 42Destructuring
Section titled “Destructuring”let (left, right) = getPair() // `()` — tuplelet { name, age } = getUser() // `{}` — recordArray destructuring (let [a, b] = arr) is a parse error in let
bindings — it would lie about runtime length. Use Array.get(arr, i) -> Option<T> or a match pattern:
match arr { [first, ..rest] -> todo, _ -> todo,}Functions
Section titled “Functions”let add(a: number, b: number) -> number = { a + b}The last expression in a function body is the return value. The return keyword is not used in Floe.
In multi-statement functions, floe fmt adds a blank line before the final expression to visually separate the return value:
let loadProfile(id: string) -> Result<Profile, ApiError> = { let user = fetchUser(id)? let posts = fetchPosts(user.id)? let stats = computeStats(posts)
Profile(user, posts, stats)}Exported functions must have return type annotations:
export let greet(name: string) -> string = { `Hello, ${name}!`}Generic Functions
Section titled “Generic Functions”Functions can declare type parameters using angle brackets after the function name:
let identity<T>(x: T) -> T = { x }
let pair<A, B>(a: A, b: B) -> (A, B) = { (a, b) }
let mapResult<T, U, E>(r: Result<T, E>, f: (T) -> U) -> Result<U, E> = { match r { Ok(value) -> Ok(f(value)), Err(e) -> Err(e), }}Generic functions compile directly to TypeScript generics:
function identity<T>(x: T): T { return x; }function pair<A, B>(a: A, b: B): readonly [A, B] { return [a, b] as const; }Default Parameters
Section titled “Default Parameters”let greet(name: string = "world") -> string = { `Hello, ${name}!`}Anonymous Functions (Closures)
Section titled “Anonymous Functions (Closures)”Use (x) -> expr for inline anonymous functions:
todos |> Array.map((t) -> t.text)items |> Array.reduce((acc, x) -> acc + x.price, 0)For simple field access, use dot shorthand:
todos |> Array.filter(.done == false)todos |> Array.map(.text)users |> Array.sortBy(.name)Two Ways to Write a Named Function
Section titled “Two Ways to Write a Named Function”Floe accepts two forms for binding a function to a name. Both compile to the same TypeScript — pick the one that reads better for the situation.
Def-form — params and return type sit inline on the let:
let add(a: number, b: number) -> number = a + bValue-binding form — function expression on the right of =:
// Plain value bindinglet handleClick = () -> setCount(count + 1)
// With a type annotation on the let — useful when the type is namedtypealias Handler = (Request) -> Promise<Response>let placeOrder: Handler = (req) -> req |> validate? |> price |> OkWhen to pick which:
- Def-form for top-level functions with several parameters or a meaningful return type — params and return read together as a signature, no closure noise.
- Value-binding form when the function implements a named type alias (the type lives once on the
let, not duplicated in the parameters) or when the body is a one-liner that already reads well as an expression.
Exported functions must have a return type annotation regardless of which form you use.
Function Types
Section titled “Function Types”Use -> to describe function types:
typealias Transform = (string) -> numbertypealias Predicate = (Todo) -> booleantypealias Callback = () -> ()Async Functions
Section titled “Async Functions”A function is async when its body uses |> await (or |> Promise.await). The return type must be Promise<T> — the compiler enforces this, just like ? requires Result<T, E>:
let fetchUser(id: string) -> Promise<User> = { let response = fetch(`/api/users/${id}`) |> await response.json() |> await}For functions without a return type annotation, the compiler infers Promise<T> automatically.
async let sugar. When the return type is verbose (e.g. Promise<Result<Option<T>, Error>>), use async let f() -> T = { ... } to write the inner type directly. The compiler wraps it in Promise<>:
// Verboselet findUser(id: string) -> Promise<Result<Option<User>, Error>> = { // ...}
// Sugar — the Promise<> wrapper is impliedasync let findUser(id: string) -> Result<Option<User>, Error> = { // ...}Both forms compile to the same async function in TypeScript. Callers still use |> await to unwrap. See the Promise reference for details.
Callback Flattening with use
Section titled “Callback Flattening with use”The use keyword flattens nested callbacks. The rest of the block becomes the callback body:
// Without use — deeply nestedFile.open(path, (file) -> File.readAll(file, (contents) -> contents |> String.toUpper ))
// With use — flat and readableuse file <- File.open(path)use contents <- File.readAll(file)contents |> String.toUpperZero-binding form for callbacks that don’t pass a value:
use <- Timer.delay(1000)Console.log("step 1")use <- Timer.delay(500)Console.log("done")use works with any function whose last parameter is a callback. It’s complementary to ? (which only works on Result/Option).
What’s Not Here
Section titled “What’s Not Here”- No
constorvar- all bindings useletand are always immutable - No
class- use functions and records - No
this- functions are pure by default - No
function*generators - use arrays and pipes - No
=>anywhere in Floe source —->covers closures, match arms, return types, and function types.=>only appears in emitted TypeScript. - No
functionkeyword — useletfor named functions
These are removed intentionally. See the introduction for the reasoning.