diff --git a/docs/design.md b/docs/design.md index fe2f612..e7a5c62 100644 --- a/docs/design.md +++ b/docs/design.md @@ -22,14 +22,14 @@ The compiler is a single Rust binary (`floe`) that takes `.fl` files and emits ` A React developer should read Floe and understand it in 30 minutes. We keep familiar syntax and add targeted upgrades. -### Three Operators +### Key Operators ``` -|x| anonymous functions |a| a + 1 --> match arms Ok(x) -> x -|> pipe data through data |> transform -? unwrap Result/Option fetchUser(id)? -.x dot shorthand .name (implicit lambda for field access) +fn(x) anonymous functions fn(a) a + 1 +-> match arms Ok(x) -> x +|> pipe data through data |> transform +? unwrap Result/Option fetchUser(id)? +.x dot shorthand .name (implicit closure for field access) ``` All four of TypeScript's `?` uses (`?.`, `??`, `?:`, `? :`) are removed. `?` now means exactly one thing: unwrap or short-circuit. @@ -38,13 +38,13 @@ All four of TypeScript's `?` uses (`?.`, `??`, `?:`, `? :`) are removed. `?` now - `const`, `export`, `import`, type annotations - `fn` for named/exported functions -- Pipe lambdas `|x|` for inline/anonymous functions -- Dot shorthand `.field` for implicit field-access lambdas +- Closures `fn(x)` for inline/anonymous functions +- Dot shorthand `.field` for implicit field-access closures - JSX / TSX (full support) - Generics, template literals - `async`/`await` - Destructuring, spread, rest params -- `||`, `&&`, `!` (boolean operators) +- `||` (boolean OR), `&&`, `!` (boolean operators) - `==` (but only between same types — structural equality on objects) - Unit type `()` instead of `void` @@ -74,7 +74,7 @@ All four of TypeScript's `?` uses (`?.`, `??`, `?:`, `? :`) are removed. `?` now | Type constructors | `User(name: "Ryan", email: e)` | `{ name: "Ryan", email: e }` (compiler adds tags for unions) | | Record spread | `User(..user, name: "New")` | `{ ...user, name: "New" }` | | Named arguments | `fetchUsers(page: 3, limit: 50)` | `fetchUsers(3, 50)` (labels erased) | -| Pipe lambdas | `\|x\| x + 1` | `(x) => x + 1` | +| Closures | `fn(x) x + 1` | `(x) => x + 1` | | Dot shorthand | `.name` in callback position | `(x) => x.name` | | Dot shorthand (predicate) | `.id != id` in callback position | `(x) => x.id != id` | | Implicit member expr | `.Variant` when type is known | `TypeName.Variant` | @@ -111,7 +111,7 @@ All four of TypeScript's `?` uses (`?.`, `??`, `?:`, `? :`) are removed. `?` now | `x?: T` | Optional fields | `x: Option` | | `+` on strings | Silent coercion bugs | Template literals only (warning) | | `void` | Not a real type, can't use in generics | Unit type `()` — a real value | -| `=>` | Two syntaxes for functions is one too many | `\|x\| expr` for anonymous functions | +| `=>` | Two syntaxes for functions is one too many | `fn(x) expr` for anonymous functions | | `function` | Verbose keyword | `fn` | | `if`/`else` | Redundant control flow | `match` expression | | `return` | Implicit returns — last expression is the value | Omit `return`; the last expression in a block is the return value | @@ -137,13 +137,13 @@ users const addTen = add(10, _) // (x) => add(10, x) [1, 2, 3] |> map(multiply(_, 2)) // [2, 4, 6] -// Dot shorthand — .field creates an implicit lambda +// Dot shorthand — .field creates an implicit closure todos |> Array.filter(.id != id) // filter(todos, x => x.id != id) todos |> Array.map(.text) // map(todos, x => x.text) -// Pipe lambdas — |x| for when you need a named param -todos |> Array.map(|t| Todo(..t, done: true)) -items |> Array.reduce(|acc, x| acc + x.price, 0) +// Closures — fn(x) for when you need a named param +todos |> Array.map(fn(t) Todo(..t, done: true)) +items |> Array.reduce(fn(acc, x) acc + x.price, 0) // tap — call a function for side effects, pass value through orders @@ -156,7 +156,7 @@ orders {users |> filter(.active) |> sortBy(.name) - |> map(|u|
  • {u.name}
  • ) + |> map(fn(u)
  • {u.name}
  • ) } ``` @@ -238,7 +238,7 @@ match url { ``` -Match uses `->` for arms (not `|x|`), so it's visually distinct from lambdas. +Match uses `->` for arms (not `fn(x)`), so it's visually distinct from closures. ### Array Pattern Matching @@ -399,10 +399,10 @@ const display = match user.nickname { const display = user.nickname |> Option.unwrapOr(user.name) // Transform inside without unwrapping -const upper: Option = user.nickname |> Option.map(|n| toUpper(n)) +const upper: Option = user.nickname |> Option.map(fn(n) toUpper(n)) // Chain -const avatar = user.nickname |> Option.flatMap(|n| findAvatar(n)) +const avatar = user.nickname |> Option.flatMap(fn(n) findAvatar(n)) ``` @@ -426,7 +426,7 @@ type BaseProps = { type ButtonProps = { ...BaseProps, - onClick: () -> (), + onClick: fn() -> (), label: string, } // ButtonProps has: className, disabled, onClick, label @@ -699,7 +699,7 @@ fetchUsers(limit: 50, sort: Descending) // override two // On React component props type ButtonProps = { label: string // required - onClick: () -> () // required + onClick: fn() -> () // required variant: Variant = Primary // default size: Size = Medium // default disabled: boolean = false // default @@ -930,10 +930,10 @@ Compiles to (test mode only): export fn TodoApp() -> JSX.Element { ... } export fn fetchUser(id: UserId) -> Result { ... } -// Inline/anonymous uses |x| pipe lambdas -todos |> Array.map(|t| t.name) -onClick={|| setCount(count + 1)} -items |> Array.reduce(|acc, x| acc + x.price, 0) +// Inline/anonymous uses fn(x) closures +todos |> Array.map(fn(t) t.name) +onClick={fn() setCount(count + 1)} +items |> Array.reduce(fn(acc, x) acc + x.price, 0) // Dot shorthand for simple field access todos |> Array.filter(.done == false) @@ -946,8 +946,8 @@ fn greet(name: string, greeting: string = "Hello") -> string { greet("Ryan") // "Hello, Ryan!" greet("Ryan", greeting: "Hey") // "Hey, Ryan!" -// COMPILE ERROR: const + lambda — use fn instead -const double = |x| x * 2 // ERROR: Use `fn double(x) -> ...` +// COMPILE ERROR: const + closure — use fn instead +const double = fn(x) x * 2 // ERROR: Use `fn double(x) -> ...` fn double(x: number) -> number { x * 2 } // correct ``` @@ -966,13 +966,13 @@ type Tab = Overview | Team | Analytics export fn Dashboard(userId: UserId) -> JSX.Element { const [tab, setTab] = useState(Overview) - const user = useAsync(|| fetchUser(userId)) + const user = useAsync(fn() fetchUser(userId)) true, _ -> false }} - onClick={|| setTab(Overview)}> + onClick={fn() setTab(Overview)}> Overview @@ -1023,7 +1023,7 @@ These are enforced at compile time with clear error messages. | Rule | Error | Fix | |------|-------|-----| | Exported functions must declare return types | `ERROR: missing return type` | Add `-> ReturnType` | -| `const name = \|x\| ...` | `ERROR: use fn instead` | `fn name(x) -> T { ... }` | +| `const name = fn(x) ...` | `ERROR: use fn instead` | `fn name(x) -> T { ... }` | | No unused variables | `ERROR: x is never used` | Remove or prefix with `_` | | No unused imports | `ERROR: useRef is never used` | Remove the import | | No implicit type widening | `ERROR: mixed array needs explicit type` | Add type annotation | @@ -1132,7 +1132,7 @@ Key tokens beyond standard TypeScript: | `Arrow` | `->` (match arms, return types, function types) | | `Question` | `?` (postfix, Result/Option unwrap) | | `Underscore` | `_` (placeholder/partial application) | -| `PipePipe` | `\|\|` (zero-arg lambda, also boolean OR) | +| `PipePipe` | `\|\|` (boolean OR) | | `Match` | `match` keyword | | `Fn` | `fn` keyword | | `Some` | `Some` keyword | @@ -1163,7 +1163,7 @@ Banned tokens (immediate compile errors with helpful messages): - `enum` → "Use type with | variants" - `void` → "Use the unit type () instead" - `function` → "Use fn instead" -- `=>` → "Use |x| for lambdas, -> for types and match arms" +- `=>` → "Use fn(x) for closures, -> for types and match arms" ### Parser (`zs_parser`) @@ -1176,7 +1176,7 @@ enum Expr { Identifier(String), BinaryOp { left: Box, op: BinOp, right: Box }, Call { callee: Box, args: Vec }, - Lambda { params: Vec, body: Box }, // |x| expr + Lambda { params: Vec, body: Box }, // fn(x) expr DotShorthand(Box), // .field or .field op expr Jsx(JsxElement), @@ -1361,7 +1361,7 @@ Emits clean, readable `.tsx`. Zero runtime imports. | `a \|> f(b, c)` | `f(a, b, c)` | | `a \|> f(b, _, c)` | `f(b, a, c)` | | `add(10, _)` | `(x) => add(10, x)` | -| `\|x\| x + 1` | `(x) => x + 1` | +| `fn(x) x + 1` | `(x) => x + 1` | | `.name` (in callback) | `(x) => x.name` | | `.id != id` (in callback) | `(x) => x.id != id` | | `.Variant` (implicit member) | `Variant` (resolved by compiler) | @@ -1506,7 +1506,7 @@ fn add(a: number, b: number) -> number { } ``` -This applies to `fn` bodies, `for`-block functions, match arms with block bodies, and lambdas with block bodies. +This applies to `fn` bodies, `for`-block functions, match arms with block bodies, and closures with block bodies. --- @@ -1515,7 +1515,7 @@ This applies to `fn` bodies, `for`-block functions, match arms with block bodies ### Phase 1: Proof of concept (2-4 weeks) - [ ] Lexer with pipe, match, `?` tokens and banned keyword errors -- [ ] Parser for: const, fn, |x| lambdas, .field shorthand, pipes, basic expressions +- [ ] Parser for: const, fn, fn(x) closures, .field shorthand, pipes, basic expressions - [ ] Parser: `Type(field: value)` constructor syntax and `..spread` - [ ] Parser: named arguments at call sites - [ ] Parser: default values on function params and record fields @@ -1612,7 +1612,7 @@ fn deleteUser(id: UserId) -> Result<(), ApiError> { // Callbacks type ButtonProps = { - onClick: () -> () + onClick: fn() -> () } ``` @@ -1690,10 +1690,10 @@ const c = { ...a, ...b } // WARNING: 'y' from 'a' is overwritten by 'b' | Question | Decision | Rationale | |----------|----------|-----------| | Syntax style | TS keywords + Gleam match/pipe | Familiar to React devs, 30min learning curve | -| Function style | `fn` for named, `\|x\|` for inline lambdas, `.field` for shorthand | One keyword, two lambda forms, no overlap | +| Function style | `fn` for named, `fn(x)` for inline closures, `.field` for shorthand | One keyword, two closure forms, no overlap | | Arrow `->` | Match arms, return types, function types | "Maps to" everywhere — consistent single meaning | -| `const name = \|x\| ...` | Compile error | If it has a name, use `fn`. No two ways to name a function. | -| Dot shorthand | `.field` in callback position creates implicit lambda | Covers 80% of inline callbacks (filter, map, sort) | +| `const name = fn(x) ...` | Compile error | If it has a name, use `fn`. No two ways to name a function. | +| Dot shorthand | `.field` in callback position creates implicit closure | Covers 80% of inline callbacks (filter, map, sort) | | Implicit member expr | `.Variant` when type is known from context | Swift-style; NOT used in match arms (match already establishes type) | | Pipe semantics | First-arg default, `_` placeholder | Gleam approach — clean 90% of the time | | Partial application | `f(a, _)` creates `(x) => f(a, x)` | Free bonus from `_` placeholder | @@ -1702,7 +1702,7 @@ const c = { ...a, ...b } // WARNING: 'y' from 'a' is overwritten by 'b' | npm null interop | Auto-wrap to `Option` at boundary | Transparent — devs never see null | | Boolean operators | Keep `||`, `&&`, `!` | Everyone knows them, no coercion issues | | Compiler target | Pure vanilla .tsx, zero dependencies | Eject-friendly, no runtime cost | -| Type keyword | `type` for everything, no `enum` | `enum` is broken in TS; `type` with `\|` covers unions, records, brands, opaques | +| Type keyword | `type` for everything, no `enum` | `enum` is broken in TS; `type` with `|` covers unions, records, brands, opaques | | Nested unions | Unions can contain other union types, match at any depth | More powerful than Gleam and TS; compiler generates discrimination tags | | Constructors | `Type(field: value)` — parens, not braces | Same syntax for records, unions, and function calls. Consistent. | | Record updates | `Type(..existing, field: newValue)` | Gleam-style spread with `..` — compiles to `{ ...existing, field: newValue }` | diff --git a/docs/llms.txt b/docs/llms.txt index b4f6de8..dda7415 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -15,16 +15,16 @@ export fn greet(name: string) -> string { `Hello, ${name}!` } -// Anonymous functions use |params| body (pipe lambdas) -items |> map(|x| x + 1) +// Anonymous functions use fn(params) body (closures) +items |> map(fn(x) x + 1) -// Zero-arg lambda -onClick={|| doSomething()} +// Zero-arg closure +onClick={fn() doSomething()} // Dot shorthand for field access in callbacks -users |> filter(.active) // same as |u| u.active -users |> map(.name) // same as |u| u.name -users |> filter(.id != id) // same as |u| u.id != id +users |> filter(.active) // same as fn(u) u.active +users |> map(.name) // same as fn(u) u.name +users |> filter(.id != id) // same as fn(u) u.id != id // No semicolons const x = 5 @@ -92,7 +92,7 @@ type BaseProps = { type ButtonProps = { ...BaseProps, - onClick: () -> (), + onClick: fn() -> (), label: string, } // ButtonProps has: className, disabled, onClick, label @@ -285,7 +285,7 @@ match user.nickname { // Option helpers user.nickname |> Option.unwrapOr(user.name) -user.nickname |> Option.map(|n| toUpper(n)) +user.nickname |> Option.map(fn(n) toUpper(n)) // collect block — accumulate all errors instead of short-circuiting fn validateForm(input: FormInput) -> Result> { @@ -441,15 +441,15 @@ export fn App() -> JSX.Element {

    Todos

    handleAdd(), _ -> (), }} />
      - {todos |> map(|item| + {todos |> map(fn(item)
    • "line-through", @@ -457,7 +457,7 @@ export fn App() -> JSX.Element { }}> {item.text} - + } ``` diff --git a/docs/site/src/content/docs/guide/from-typescript.md b/docs/site/src/content/docs/guide/from-typescript.md index 8a60a65..de856e7 100644 --- a/docs/site/src/content/docs/guide/from-typescript.md +++ b/docs/site/src/content/docs/guide/from-typescript.md @@ -29,7 +29,7 @@ fn greet(name: string) -> string { } ``` -### `|x|` instead of `=>` +### `fn(x)` instead of `=>` ```typescript // TypeScript @@ -38,7 +38,7 @@ onClick={() => setCount(count + 1)} // Floe const result = items |> Array.filter(.active) -onClick={|| setCount(count + 1)} +onClick={fn() setCount(count + 1)} ``` ### `->` for return types and function types @@ -50,7 +50,7 @@ type Transform = (s: string) => number // Floe fn add(a: number, b: number) -> number { ... } -type Transform = (string) -> number +type Transform = fn(string) -> number ``` ### `const` only @@ -182,7 +182,7 @@ fn find(id: string) -> Option { | `for` / `while` | Mutation-heavy | Pipes + map/filter/reduce | | `throw` | Invisible error paths | `Result` | | `function` | Verbose | `fn` | -| `=>` | Two function syntaxes | `\|x\|` for lambdas | +| `=>` | Two function syntaxes | `fn(x)` for closures | | `return` | Implicit returns | Last expression is the return value | ## Incremental Adoption diff --git a/docs/site/src/content/docs/guide/functions.md b/docs/site/src/content/docs/guide/functions.md index 84fe61c..2ced47f 100644 --- a/docs/site/src/content/docs/guide/functions.md +++ b/docs/site/src/content/docs/guide/functions.md @@ -64,14 +64,14 @@ fn greet(name: string = "world") -> string { } ``` -### Anonymous Functions (Lambdas) +### Anonymous Functions (Closures) -Use `|x|` for inline anonymous functions: +Use `fn(x)` for inline anonymous functions: ```floe -todos |> Array.map(|t| t.text) -items |> Array.reduce(|acc, x| acc + x.price, 0) -onClick={|| setCount(count + 1)} +todos |> Array.map(fn(t) t.text) +items |> Array.reduce(fn(acc, x) acc + x.price, 0) +onClick={fn() setCount(count + 1)} ``` For simple field access, use dot shorthand: @@ -82,11 +82,11 @@ todos |> Array.map(.text) users |> Array.sortBy(.name) ``` -**`const name = |x| ...` is a compile error.** If it has a name, use `fn`: +**`const name = fn(x) ...` is a compile error.** If it has a name, use `fn`: ```floe // COMPILE ERROR -const double = |x| x * 2 +const double = fn(x) x * 2 // correct fn double(x: number) -> number { x * 2 } @@ -94,12 +94,12 @@ fn double(x: number) -> number { x * 2 } ### Function Types -Use `->` to describe function types: +Use `fn` and `->` to describe function types: ```floe -type Transform = (string) -> number -type Predicate = (Todo) -> boolean -type Callback = () -> () +type Transform = fn(string) -> number +type Predicate = fn(Todo) -> boolean +type Callback = fn() -> () ``` ### Async Functions @@ -117,6 +117,6 @@ async fn fetchUser(id: string) -> Promise { - **No `class`** - use functions and records - **No `this`** - functions are pure by default - **No `function*` generators** - use arrays and pipes -- **No `=>`** - use `|x|` for lambdas, `->` for types and match arms +- **No `=>`** - use `fn(x)` for closures, `->` for types and match arms These are removed intentionally. See the [comparison](/guide/comparison) for the reasoning. diff --git a/docs/site/src/content/docs/guide/jsx.md b/docs/site/src/content/docs/guide/jsx.md index 7b21464..d09502b 100644 --- a/docs/site/src/content/docs/guide/jsx.md +++ b/docs/site/src/content/docs/guide/jsx.md @@ -14,7 +14,7 @@ export fn Counter() -> JSX.Element {

      Count: {count}

      - +
      } ``` @@ -26,7 +26,7 @@ Components are exported `fn` declarations with a `JSX.Element` return type. The ```floe type ButtonProps = { label: string, - onClick: () -> (), + onClick: fn() -> (), disabled: boolean, } @@ -59,7 +59,7 @@ Use pipes with `map`: ```floe
        - {items |> map(|item|
      • {item.name}
      • )} + {items |> map(fn(item)
      • {item.name}
      • )}
      ``` diff --git a/docs/site/src/content/docs/guide/pipes.md b/docs/site/src/content/docs/guide/pipes.md index b5316bd..a4d32f3 100644 --- a/docs/site/src/content/docs/guide/pipes.md +++ b/docs/site/src/content/docs/guide/pipes.md @@ -56,12 +56,12 @@ todos |> Array.map(.text) users |> Array.sortBy(.name) ``` -`.field` creates an implicit lambda. `.done == false` is shorthand for `|t| t.done == false`. +`.field` creates an implicit closure. `.done == false` is shorthand for `fn(t) t.done == false`. -For anything more complex, use `|x|`: +For anything more complex, use `fn(x)`: ```floe -todos |> Array.map(|t| Todo(..t, done: !t.done)) +todos |> Array.map(fn(t) Todo(..t, done: !t.done)) ``` ## Method-Style Pipes @@ -74,7 +74,7 @@ import trusted { map, filter, reduce } from "ramda" const total = orders |> filter(.status == "complete") |> map(.amount) - |> reduce(|sum, n| sum + n, 0, _) + |> reduce(fn(sum, n) sum + n, 0, _) ``` ## Debugging with `tap` @@ -144,9 +144,9 @@ Pipes shine when you have a sequence of transformations. They replace: Floe has three arrow-like operators: ``` -|x| anonymous lambdas |a| a + 1 --> match arms / types Ok(x) -> x, (string) -> number -|> pipe data data |> transform +fn(x) anonymous closures fn(a) a + 1 +-> match arms / types Ok(x) -> x, fn(string) -> number +|> pipe data data |> transform ``` Each has a distinct purpose. No ambiguity. diff --git a/docs/site/src/content/docs/guide/types.md b/docs/site/src/content/docs/guide/types.md index 7f8be02..7f858e3 100644 --- a/docs/site/src/content/docs/guide/types.md +++ b/docs/site/src/content/docs/guide/types.md @@ -44,7 +44,7 @@ type BaseProps = { type ButtonProps = { ...BaseProps, - onClick: () -> (), + onClick: fn() -> (), label: string, } // ButtonProps has: className, disabled, onClick, label @@ -220,7 +220,7 @@ Tuples compile to TypeScript readonly tuples: `(number, string)` becomes `readon ```floe type Name = string -type Callback = (Event) -> () +type Callback = fn(Event) -> () ``` ## What's Banned diff --git a/docs/site/src/content/docs/guide/typescript-interop.md b/docs/site/src/content/docs/guide/typescript-interop.md index 8f343ca..1cd496d 100644 --- a/docs/site/src/content/docs/guide/typescript-interop.md +++ b/docs/site/src/content/docs/guide/typescript-interop.md @@ -112,11 +112,11 @@ import trusted { useState, useEffect, useCallback } from "react" export fn Counter() -> JSX.Element { const [count, setCount] = useState(0) - useEffect(|| { + useEffect(fn() { Console.log("count changed:", count) }, [count]) - } @@ -133,7 +133,7 @@ export fn MyPage() -> JSX.Element { const [open, setOpen] = useState(false)
      - +

      Dialog content

      diff --git a/docs/site/src/content/docs/reference/operators.md b/docs/site/src/content/docs/reference/operators.md index 69a40f4..7d2e704 100644 --- a/docs/site/src/content/docs/reference/operators.md +++ b/docs/site/src/content/docs/reference/operators.md @@ -56,13 +56,13 @@ x |> match { ... } // match x { ... } The `?` operator unwraps `Ok(value)` or `Some(value)`, and returns early with `Err(e)` or `None` on failure. Only valid inside functions that return `Result` or `Option`. -## Arrow and Lambda Operators +## Arrow and Closure Operators | Operator | Context | Meaning | |----------|---------|---------| -| `\|x\|` | Lambdas | `\|x\| x + 1` | -| `.field` | Dot shorthand | `.name` (implicit field-access lambda) | -| `->` | Match arms, return types, function types | `Ok(x) -> x`, `(string) -> number` | +| `fn(x)` | Closures | `fn(x) x + 1` | +| `.field` | Dot shorthand | `.name` (implicit field-access closure) | +| `->` | Match arms, return types, function types | `Ok(x) -> x`, `fn(string) -> number` | | `\|>` | Pipes | `data \|> transform` | ## Precedence (high to low) diff --git a/docs/site/src/content/docs/reference/stdlib.md b/docs/site/src/content/docs/reference/stdlib.md index 0c20f6f..9beae46 100644 --- a/docs/site/src/content/docs/reference/stdlib.md +++ b/docs/site/src/content/docs/reference/stdlib.md @@ -9,7 +9,7 @@ All stdlib functions are **pipe-friendly**: the first argument is the data, so t ```floe [3, 1, 2] |> Array.sort - |> Array.map(|n| n * 10) + |> Array.map(fn(n) n * 10) |> Array.reverse // [30, 20, 10] ``` @@ -23,12 +23,12 @@ All array functions return new arrays. They never mutate the original. | Function | Signature | Description | |----------|-----------|-------------| | `Array.sort` | `Array -> Array` | Sort numerically (returns new array) | -| `Array.sortBy` | `Array, (T -> number) -> Array` | Sort by key function | -| `Array.map` | `Array, (T -> U) -> Array` | Transform each element | -| `Array.filter` | `Array, (T -> boolean) -> Array` | Keep elements matching predicate | -| `Array.find` | `Array, (T -> boolean) -> Option` | First element matching predicate | -| `Array.findIndex` | `Array, (T -> boolean) -> Option` | Index of first match | -| `Array.flatMap` | `Array, (T -> Array) -> Array` | Map then flatten one level | +| `Array.sortBy` | `Array, fn(T) -> number -> Array` | Sort by key function | +| `Array.map` | `Array, fn(T) -> U -> Array` | Transform each element | +| `Array.filter` | `Array, fn(T) -> boolean -> Array` | Keep elements matching predicate | +| `Array.find` | `Array, fn(T) -> boolean -> Option` | First element matching predicate | +| `Array.findIndex` | `Array, fn(T) -> boolean -> Option` | Index of first match | +| `Array.flatMap` | `Array, fn(T) -> Array -> Array` | Map then flatten one level | | `Array.at` | `Array, number -> Option` | Safe index access | | `Array.contains` | `Array, T -> boolean` | Check if element exists (structural equality) | | `Array.head` | `Array -> Option` | First element | @@ -36,16 +36,16 @@ All array functions return new arrays. They never mutate the original. | `Array.take` | `Array, number -> Array` | First n elements | | `Array.drop` | `Array, number -> Array` | All except first n elements | | `Array.reverse` | `Array -> Array` | Reverse order (returns new array) | -| `Array.reduce` | `Array, U, (U, T -> U) -> U` | Fold into a single value | +| `Array.reduce` | `Array, U, fn(U, T) -> U -> U` | Fold into a single value | | `Array.length` | `Array -> number` | Number of elements | -| `Array.any` | `Array, (T -> boolean) -> boolean` | True if any element matches predicate | -| `Array.all` | `Array, (T -> boolean) -> boolean` | True if all elements match predicate | +| `Array.any` | `Array, fn(T) -> boolean -> boolean` | True if any element matches predicate | +| `Array.all` | `Array, fn(T) -> boolean -> boolean` | True if all elements match predicate | | `Array.sum` | `Array -> number` | Sum all elements | | `Array.join` | `Array, string -> string` | Join elements with separator | | `Array.isEmpty` | `Array -> boolean` | True if array has no elements | | `Array.chunk` | `Array, number -> Array>` | Split into chunks of given size | | `Array.unique` | `Array -> Array` | Remove duplicate elements | -| `Array.groupBy` | `Array, (T -> string) -> Record` | Group elements by key function | +| `Array.groupBy` | `Array, fn(T) -> string -> Record` | Group elements by key function | | `Array.zip` | `Array, Array -> Array<[T, U]>` | Pair elements from two arrays | ### Examples @@ -94,8 +94,8 @@ Functions for working with `Option` (`Some(v)` / `None`) values. | Function | Signature | Description | |----------|-----------|-------------| -| `Option.map` | `Option, (T -> U) -> Option` | Transform the inner value if present | -| `Option.flatMap` | `Option, (T -> Option) -> Option` | Chain option-returning operations | +| `Option.map` | `Option, fn(T) -> U -> Option` | Transform the inner value if present | +| `Option.flatMap` | `Option, fn(T) -> Option -> Option` | Chain option-returning operations | | `Option.unwrapOr` | `Option, T -> T` | Extract value or use default | | `Option.isSome` | `Option -> boolean` | Check if value is present | | `Option.isNone` | `Option -> boolean` | Check if value is absent | @@ -106,12 +106,12 @@ Functions for working with `Option` (`Some(v)` / `None`) values. ```floe // Transform without unwrapping const upper = user.nickname - |> Option.map(|n| String.toUpper(n)) + |> Option.map(fn(n) String.toUpper(n)) // Some("RYAN") or None // Chain lookups const avatar = user.nickname - |> Option.flatMap(|n| findAvatar(n)) + |> Option.flatMap(fn(n) findAvatar(n)) // Extract with fallback const display = user.nickname @@ -130,9 +130,9 @@ Functions for working with `Result` (`Ok(v)` / `Err(e)`) values. | Function | Signature | Description | |----------|-----------|-------------| -| `Result.map` | `Result, (T -> U) -> Result` | Transform the Ok value | -| `Result.mapErr` | `Result, (E -> F) -> Result` | Transform the Err value | -| `Result.flatMap` | `Result, (T -> Result) -> Result` | Chain result-returning operations | +| `Result.map` | `Result, fn(T) -> U -> Result` | Transform the Ok value | +| `Result.mapErr` | `Result, fn(E) -> F -> Result` | Transform the Err value | +| `Result.flatMap` | `Result, fn(T) -> Result -> Result` | Chain result-returning operations | | `Result.unwrapOr` | `Result, T -> T` | Extract Ok value or use default | | `Result.isOk` | `Result -> boolean` | Check if result is Ok | | `Result.isErr` | `Result -> boolean` | Check if result is Err | @@ -143,15 +143,15 @@ Functions for working with `Result` (`Ok(v)` / `Err(e)`) values. ```floe // Transform success value const doubled = fetchCount() - |> Result.map(|n| n * 2) + |> Result.map(fn(n) n * 2) // Handle errors const result = fetchUser(id) - |> Result.mapErr(|e| AppError(e)) + |> Result.mapErr(fn(e) AppError(e)) // Chain operations const profile = fetchUser(id) - |> Result.flatMap(|u| fetchProfile(u.profileId)) + |> Result.flatMap(fn(u) fetchProfile(u.profileId)) // Extract with fallback const count = fetchCount() @@ -195,7 +195,7 @@ const cleaned = " Hello, World! " // Split and process const words = "one,two,three" |> String.split(",") - |> Array.map(|w| String.toUpper(w)) + |> Array.map(fn(w) String.toUpper(w)) // ["ONE", "TWO", "THREE"] ``` @@ -284,7 +284,7 @@ Standard math functions. Compile directly to JavaScript `Math` methods. | `Math.sin` | `number -> number` | Sine | | `Math.cos` | `number -> number` | Cosine | | `Math.tan` | `number -> number` | Tangent | -| `Math.random` | `() -> number` | Random number between 0 (inclusive) and 1 (exclusive) | +| `Math.random` | `fn() -> number` | Random number between 0 (inclusive) and 1 (exclusive) | ### Examples @@ -442,7 +442,7 @@ const result = await Http.post("https://api.example.com/users", { name: "Alice" // Full pipeline const users = await Http.get(url)? |> Http.json? - |> Result.map(|data| Array.filter(data, .active)) + |> Result.map(fn(data) Array.filter(data, .active)) // Error handling with match match await Http.get(url) { @@ -461,7 +461,7 @@ Utility functions for pipeline debugging and control flow. | Function | Signature | Description | |----------|-----------|-------------| -| `tap` | `T, (T -> ()) -> T` | Call a function for side effects, return value unchanged | +| `tap` | `T, fn(T) -> () -> T` | Call a function for side effects, return value unchanged | ### Examples @@ -471,12 +471,12 @@ const result = orders |> Array.filter(.active) |> tap(Console.log) // logs filtered orders, passes them through |> Array.map(.total) - |> Array.reduce(|sum, n| sum + n, 0) + |> Array.reduce(fn(sum, n) sum + n, 0) -// Use a lambda for custom logging +// Use a closure for custom logging const processed = data |> transform - |> tap(|x| Console.log("after transform:", x)) + |> tap(fn(x) Console.log("after transform:", x)) |> validate // Works with any type diff --git a/docs/site/src/content/docs/reference/syntax.md b/docs/site/src/content/docs/reference/syntax.md index 4dc288e..2caadad 100644 --- a/docs/site/src/content/docs/reference/syntax.md +++ b/docs/site/src/content/docs/reference/syntax.md @@ -199,28 +199,28 @@ Option.Some(value) // variant with data Result.Ok(value) // variant with data ``` -### Anonymous Functions (Lambdas) +### Anonymous Functions (Closures) ```floe -|a, b| a + b -|x| x * 2 -|| doSomething() +fn(a, b) a + b +fn(x) x * 2 +fn() doSomething() ``` Dot shorthand for field access: ```floe -.name // |x| x.name -.id != id // |x| x.id != id -.done == false // |x| x.done == false +.name // fn(x) x.name +.id != id // fn(x) x.id != id +.done == false // fn(x) x.done == false ``` ### Function Types ```floe -() -> () // takes nothing, returns nothing -(string) -> number // takes string, returns number -(number, number) -> boolean // takes two numbers, returns boolean +fn() -> () // takes nothing, returns nothing +fn(string) -> number // takes string, returns number +fn(number, number) -> boolean // takes two numbers, returns boolean ``` ### JSX diff --git a/docs/site/src/content/docs/reference/types.md b/docs/site/src/content/docs/reference/types.md index dd663d1..b104778 100644 --- a/docs/site/src/content/docs/reference/types.md +++ b/docs/site/src/content/docs/reference/types.md @@ -54,7 +54,7 @@ type BaseProps = { type ButtonProps = { ...BaseProps, - onClick: () -> (), + onClick: fn() -> (), label: string, } ``` @@ -149,7 +149,7 @@ Result Option // Function -(number, number) -> number +fn(number, number) -> number // Record (inline) { name: string, age: number } diff --git a/docs/site/src/content/docs/tooling/vscode.md b/docs/site/src/content/docs/tooling/vscode.md index 9897a89..8054acb 100644 --- a/docs/site/src/content/docs/tooling/vscode.md +++ b/docs/site/src/content/docs/tooling/vscode.md @@ -25,7 +25,7 @@ npm run build Full TextMate grammar for `.fl` files: - Keywords (`const`, `fn`, `match`, `type`, etc.) -- Operators (`|>`, `->`, `|x|`, `?`) +- Operators (`|>`, `->`, `fn()`, `?`) - JSX elements and attributes - Template literals with interpolation - Banned keyword highlighting (visual warning for `let`, `class`, etc.) diff --git a/editors/neovim/queries/floe/highlights.scm b/editors/neovim/queries/floe/highlights.scm index 68072e6..5f58fff 100644 --- a/editors/neovim/queries/floe/highlights.scm +++ b/editors/neovim/queries/floe/highlights.scm @@ -72,8 +72,7 @@ name: (identifier) @variable.parameter) ; ── Lambda ─────────────────────────────────────────────── -(pipe_lambda "|" @punctuation.delimiter) -(pipe_lambda "||" @punctuation.delimiter) +(pipe_lambda "fn" @keyword) ; ── Dot shorthand ──────────────────────────────────────── (dot_shorthand diff --git a/editors/tree-sitter-floe/grammar.js b/editors/tree-sitter-floe/grammar.js index 361a71e..bb22ebd 100644 --- a/editors/tree-sitter-floe/grammar.js +++ b/editors/tree-sitter-floe/grammar.js @@ -242,6 +242,7 @@ module.exports = grammar({ function_type: ($) => seq( + "fn", "(", commaSep($._type_expression), ")", @@ -540,16 +541,12 @@ module.exports = grammar({ pipe_lambda: ($) => prec.right( "pipe", - choice( - // |x| expr or |a, b| expr - seq( - "|", - commaSep1($.lambda_parameter), - "|", - field("body", $._expression), - ), - // || expr (zero-arg) - seq("||", field("body", $._expression)), + seq( + "fn", + "(", + commaSep($.lambda_parameter), + ")", + field("body", $._expression), ), ), diff --git a/editors/tree-sitter-floe/queries/highlights.scm b/editors/tree-sitter-floe/queries/highlights.scm index 68072e6..5f58fff 100644 --- a/editors/tree-sitter-floe/queries/highlights.scm +++ b/editors/tree-sitter-floe/queries/highlights.scm @@ -72,8 +72,7 @@ name: (identifier) @variable.parameter) ; ── Lambda ─────────────────────────────────────────────── -(pipe_lambda "|" @punctuation.delimiter) -(pipe_lambda "||" @punctuation.delimiter) +(pipe_lambda "fn" @keyword) ; ── Dot shorthand ──────────────────────────────────────── (dot_shorthand diff --git a/editors/vscode/snippets/floe.json b/editors/vscode/snippets/floe.json index a700941..feba5de 100644 --- a/editors/vscode/snippets/floe.json +++ b/editors/vscode/snippets/floe.json @@ -168,6 +168,13 @@ ], "description": "Trait implementation" }, + "Closure": { + "prefix": "closure", + "body": [ + "fn(${1:params}) ${0:expr}" + ], + "description": "Closure expression" + }, "Tap Debug": { "prefix": "tap", "body": [ diff --git a/editors/vscode/syntaxes/floe.tmLanguage.json b/editors/vscode/syntaxes/floe.tmLanguage.json index d39ba54..a3e5a3b 100644 --- a/editors/vscode/syntaxes/floe.tmLanguage.json +++ b/editors/vscode/syntaxes/floe.tmLanguage.json @@ -230,20 +230,13 @@ "lambda": { "patterns": [ { - "comment": "Zero-arg lambda: ||", - "match": "(\\|)(\\|)(?!>|\\|)", + "comment": "Closure: fn(params) expr — fn followed by ( in expression context (not followed by identifier = fn declaration)", + "match": "\\b(fn)\\s*(\\()([^)]*)(\\))(?!\\s*->\\s*[A-Z].*\\{|\\s*\\{)", "captures": { - "1": { "name": "punctuation.definition.parameters.begin.floe" }, - "2": { "name": "punctuation.definition.parameters.end.floe" } - } - }, - { - "comment": "Pipe lambda parameters: |x| or |a, b|", - "match": "(\\|)([^|>][^|]*?)(\\|)(?!>)", - "captures": { - "1": { "name": "punctuation.definition.parameters.begin.floe" }, - "2": { "name": "variable.parameter.floe" }, - "3": { "name": "punctuation.definition.parameters.end.floe" } + "1": { "name": "storage.type.function.floe" }, + "2": { "name": "punctuation.definition.parameters.begin.floe" }, + "3": { "name": "variable.parameter.floe" }, + "4": { "name": "punctuation.definition.parameters.end.floe" } } }, { diff --git a/examples/store/src/checkout.fl b/examples/store/src/checkout.fl index 12cf5eb..c5f5569 100644 --- a/examples/store/src/checkout.fl +++ b/examples/store/src/checkout.fl @@ -23,7 +23,7 @@ fn validatePhone(phone: string) -> Result { fn validateStock(cart: Array) -> Result, CheckoutError> { const outOfStock = cart - |> Array.filter(|item| item.quantity > item.product.stock) + |> Array.filter(fn(item) item.quantity > item.product.stock) match outOfStock { [] -> Ok(cart), diff --git a/examples/store/src/pages/cart.fl b/examples/store/src/pages/cart.fl index 20bf230..9631434 100644 --- a/examples/store/src/pages/cart.fl +++ b/examples/store/src/pages/cart.fl @@ -6,8 +6,8 @@ import { for Array, formatPrice } from "../product" fn CartItemRow(props: { item: CartItem, - onUpdateQty: (ProductId, number) -> (), - onRemove: (ProductId) -> (), + onUpdateQty: fn(ProductId, number) -> (), + onRemove: fn(ProductId) -> (), }) -> JSX.Element { const item = props.item const product = item.product @@ -28,14 +28,14 @@ fn CartItemRow(props: {
      {item.quantity}
      , items ->
      - {items |> Array.map(|item| + {items |> Array.map(fn(item) (), + onAddToCart: fn(Product) -> (), }) -> JSX.Element { const product = props.product const hasDiscount = product.discountPercentage > 0 @@ -64,7 +64,7 @@ fn ProductCard(props: {
      - {props.categories |> Array.map(|cat: CategoryResponse| + {props.categories |> Array.map(fn(cat: CategoryResponse)
        - {visible |> map(|item| + {visible |> map(fn(item)
      • - {userIds |> Array.map(|id| + {userIds |> Array.map(fn(id)