BEP-008/009: Function and Lambda Syntax Design#3231
Conversation
BEP-008 proposes a unified `->` syntax for declarations, lambdas, and function types. BEP-009 proposes adopting TypeScript conventions (`:` for return types, `=>` for lambdas/types) with targeted fixes for implicit return, function form reduction, and throws clauses. BEP-009 is Proposed; BEP-008 remains Draft. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughTwo new BEP documentation proposals (BEP-008 and BEP-009) are introduced to address function and lambda syntax design, with BEP-009 offering a TypeScript-aligned variant. The proposals index is updated to reflect these new entries. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Latest build complete • View logs |
|
🐑 BEPs Preview Ready Preview URL: https://dj7ggjkp4tlhz.cloudfront.net/bep-function-lambda-syntax-design/ Commit: |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| array.map((x): string -> x.name) | ||
| type foo = (x: string): x.name | ||
| array.map((x) -> x.name) | ||
| ``` |
There was a problem hiding this comment.
Accidentally committed scratch work in disambiguation section
Medium Severity
An unexplained block of mixed syntax examples appears between Sections 8.4 and 8.5. Several examples use : for return types ((x): string -> x.name), which BEP-008 explicitly rejects, and others are syntactically malformed (type foo = (x: string): x.name). This looks like working notes or scratch exploration that were not cleaned up before committing. It will confuse readers trying to understand the proposal's actual design.
There was a problem hiding this comment.
Actionable comments posted: 5
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 509bd004-9259-4a13-bf52-08a279eb654e
📒 Files selected for processing (3)
beps/docs/README.mdbeps/docs/proposals/BEP-008-function-and-lambda-syntax-design/README.mdbeps/docs/proposals/BEP-009-function-and-lambda-syntax-design-ts-aligned/README.md
| ``` | ||
| function add(a: int, b: int) -> int { | ||
| a + b | ||
| } | ||
| ``` | ||
|
|
||
| The differences are cosmetic: `function` instead of `def`, braces instead of colon-plus-indentation, implicit return instead of explicit `return`. The *type-level shape* is identical. A Python developer reading our function signatures can parse them immediately without learning new symbols. | ||
|
|
||
| Python's lambda syntax, however, is where our design diverges — intentionally. Python's `lambda x: x + 1` reuses `:` as the body separator, which collides with type annotations and makes typed lambdas impossible (see Section 3.2). Our `(x) -> x + 1` avoids that collision entirely while using the same `->` arrow Python developers already associate with "produces a value." | ||
|
|
||
| **Summary for Python users:** `->` means exactly what you think it means. Parameter types use `:` exactly as you'd expect. Lambdas work like you always wished they did. | ||
|
|
||
| ### 6.2 What TypeScript Developers Already Recognize | ||
|
|
||
| TypeScript developers will recognize `function`, parenthesized parameters with `: type` annotations, and brace-delimited bodies. The one adjustment is `->` instead of `:` for return types: | ||
|
|
||
| ```ts | ||
| // TypeScript | ||
| function add(x: number, y: number): number { return x + y; } | ||
|
|
||
| // Ours | ||
| function add(x: int, y: int) -> int { x + y } | ||
| ``` | ||
|
|
||
| This is a small change with a large payoff. TypeScript developers already know that `->` means "function" from Python, from Haskell-style type notation, and from their own `=>` arrow functions. The shift from `:` to `->` removes the declaration/type inconsistency that TypeScript developers already find confusing (see Section 2.2) — many will see this as a fix rather than a departure. | ||
|
|
||
| For lambdas, the adjustment is similarly minor — `->` instead of `=>`: | ||
|
|
||
| ```ts | ||
| // TypeScript | ||
| items.map((x) => x.name) | ||
|
|
||
| // Ours | ||
| items.map((x) -> x.name) | ||
| ``` | ||
|
|
||
| One character difference. The mental model is identical: parameters, arrow, body. | ||
|
|
||
| **Summary for TypeScript users:** `function`, `:` annotations, and braces all work as expected. The only change is `->` instead of `:` for return types and `->` instead of `=>` for lambdas — and both changes eliminate inconsistencies you've already bumped into. | ||
|
|
||
| ### 6.3 Why This Balance Matters | ||
|
|
||
| With 2x as many Python users as TypeScript users, we lean toward conventions that Python developers find natural — and `->` for return types is the single most impactful example. But we don't sacrifice TypeScript familiarity to get there: `function`, `:` for annotations, and `{ }` for bodies are all TypeScript staples. The result is a syntax that both communities can read on day one, with the consistency that neither language offers on its own. | ||
|
|
||
| --- | ||
|
|
||
| ## 7. Function Declarations | ||
|
|
||
| ### 7.1 Basic Form | ||
|
|
||
| A function declaration uses the `function` keyword, parenthesized parameters with type annotations, `->` for the return type, and a brace-delimited body. The last expression in the body is the return value. | ||
|
|
||
| ``` | ||
| function add(x: int, y: int) -> int { | ||
| x + y | ||
| } | ||
|
|
||
| function greet(name: string) -> string { | ||
| "Hello, " + name | ||
| } | ||
| ``` | ||
|
|
||
| **Why `function` and not `fn`?** Familiarity. Every TypeScript and JavaScript developer already reads `function` fluently. Brevity is nice, but we value recognition over keystroke savings. | ||
|
|
||
| **Why `->` and not `:`?** This is the core departure from TypeScript. Using `->` for the return type means the function's *declaration syntax* and its *type syntax* can share the same shape (see Section 9). The colon remains reserved for "has type" in annotation position: `x: int`. | ||
|
|
||
| **Why implicit return?** Explicit `return` is still supported for early returns, but the last expression in a block is its value. This reduces noise in small functions and is familiar from Rust and Kotlin. | ||
|
|
||
| ### 7.2 Functions with No Return Value | ||
|
|
||
| Functions that perform side effects and don't return a meaningful value omit the return type (or optionally annotate it as `void`): | ||
|
|
||
| ``` | ||
| function log(message: string) { | ||
| print(message) | ||
| } | ||
|
|
||
| // Equivalent, if you prefer to be explicit: | ||
| function log(message: string) -> void { | ||
| print(message) | ||
| } | ||
| ``` | ||
|
|
||
| ### 7.3 Comparison with TypeScript | ||
|
|
||
| | Concept | TypeScript | Ours | | ||
| |---------|-----------|------| | ||
| | Basic function | `function add(x: number, y: number): number { return x + y; }` | `function add(x: int, y: int) -> int { x + y }` | | ||
| | Return type marker | `:` (colon) | `->` (arrow) | | ||
| | Implicit return | No (requires `return`) | Yes (last expression) | | ||
| | Variations | 6 syntactic forms | 1 form | | ||
|
|
||
| --- | ||
|
|
||
| ## 8. Lambdas (Anonymous Functions) | ||
|
|
||
| ### 8.1 The Problem | ||
|
|
||
| TypeScript's arrow functions come in multiple forms and create ambiguity with type syntax (see Section 2 and 3). Python's lambdas are limited to single expressions. We need anonymous functions that are concise for simple cases but scale to complex ones. | ||
|
|
||
| ### 8.2 Braced Lambdas (Block Body) | ||
|
|
||
| A lambda is written as parameters, `->`, and a brace-delimited body: | ||
|
|
||
| ``` | ||
| (x: int, y: int) -> { | ||
| let sum = x + y | ||
| sum * 2 | ||
| } | ||
| ``` | ||
|
|
||
| This is identical to a function declaration minus the `function` keyword and name. The braces make it unambiguous — the parser sees `->` followed by `{` and knows it's a function body, not a type. | ||
|
|
||
| ### 8.3 Braceless Lambdas (Expression Body) | ||
|
|
||
| For simple one-expression lambdas, braces can be omitted: | ||
|
|
||
| ``` | ||
| (x: int) -> x + 1 | ||
| (a: string, b: string) -> a + b | ||
| ``` | ||
|
|
||
| This is the form that makes `.map()`, `.filter()`, and `.reduce()` ergonomic: | ||
|
|
||
| ``` | ||
| items.map((x) -> x.name) | ||
| items.filter((x) -> x.active) | ||
| items.reduce((a, b) -> a + b) | ||
| ``` | ||
|
|
||
| ### 8.4 Why This Isn't Ambiguous | ||
|
|
||
| At first glance, a braceless lambda and a function type look identical: | ||
|
|
||
| ``` | ||
| (x: int) -> int // Is this a type or a lambda returning a variable called `int`? | ||
| ``` | ||
|
|
||
| But in our language, **type names and variable names are always distinguishable**. Built-in types like `int`, `string`, and `bool` are reserved keywords — you cannot name a variable `int`. User-defined types follow a capitalized naming convention (e.g. `Response`, `UserProfile`). | ||
|
|
||
| This means the parser can always determine what follows `->`: | ||
|
|
||
| ``` | ||
| (x: int) -> int // `int` is a type → this is a function type | ||
| (x: int) -> count // `count` is a variable → this is a lambda | ||
| (x: int) -> int { x + 1 } // expression with operator → this is a lambda | ||
| (x: int) -> T { x + 1 } // brace → this is a lambda with block body | ||
| ``` | ||
|
|
||
| ``` | ||
| array.map((x) -> string { x.name }) | ||
| array.map((x) -> { x.name }) | ||
| type foo = (x: string) -> x.name | ||
| array.map((x) -> x.name) | ||
|
|
||
|
|
||
| array.map((x): string -> x.name) | ||
| type foo = (x: string): x.name | ||
| array.map((x) -> x.name) | ||
| ``` | ||
| ### 8.5 Typed Lambdas | ||
|
|
||
| Because braces clearly separate "body" from "type," you can optionally annotate a lambda's return type by placing it between `->` and `{`: | ||
|
|
||
| ``` | ||
| // Untyped lambda (return type inferred) | ||
| (x: int) -> { x + 1 } | ||
|
|
||
| // Typed lambda (return type annotated) | ||
| (x: int) -> int { x + 1 } | ||
| ``` | ||
|
|
||
| The parser sees `-> int` then checks the next token. If it's `{`, this is a typed lambda. If it's end-of-expression, it's a function type. Unambiguous. | ||
|
|
||
| Note that **braceless lambdas cannot have return type annotations** — there's no syntactic position for them without introducing ambiguity. This is an acceptable tradeoff: if a lambda is simple enough to be braceless, its return type is almost certainly inferrable. | ||
|
|
||
| ### 8.6 Comparison with TypeScript | ||
|
|
||
| #### How TypeScript Types Lambdas | ||
|
|
||
| TypeScript has two ways to annotate a lambda's return type, each with different syntax: | ||
|
|
||
| ```ts | ||
| // 1. Inline annotation — return type before the arrow | ||
| const f = (x: number): number => x + 1 | ||
| const g = (x: number): number => { return x + 1 } | ||
|
|
||
| // 2. Variable annotation — type on the left, lambda on the right | ||
| const f: (x: number) => number = (x) => x + 1 | ||
| ``` | ||
|
|
||
| Option 1 is the most common, but it creates visual noise — the `:` return type annotation sits between the parameters and the `=>`, breaking the flow. Option 2 duplicates the parameter list (once in the type, once in the value). | ||
|
|
||
| Note that in Option 1, the return type uses `:` (like a declaration), but in Option 2, the function type uses `=>`. This is the declaration/type inconsistency again, now appearing within the same feature. | ||
|
|
||
| #### How We Type Lambdas | ||
|
|
||
| We have one way: the return type sits between `->` and `{`. | ||
|
|
||
| ``` | ||
| // Untyped (inferred) | ||
| (x: int) -> x + 1 | ||
| (x: int) -> { x + 1 } | ||
|
|
||
| // Typed — return type before the brace | ||
| (x: int) -> int { x + 1 } | ||
| (x: int, y: int) -> int { | ||
| let sum = x + y | ||
| sum * 2 | ||
| } | ||
|
|
||
| // Variable annotation also works | ||
| let f: (int) -> int = (x) -> x + 1 | ||
| ``` | ||
|
|
||
| Braceless lambdas cannot have return type annotations — there's no syntactic position for them. This is an acceptable tradeoff: if a lambda is simple enough to be braceless, its return type is almost certainly inferrable. | ||
|
|
||
| #### Side-by-Side | ||
|
|
||
| | Concept | TypeScript | Ours | | ||
| |---------|-----------|------| | ||
| | Block lambda | `(x: number) => { return x + 1; }` | `(x: int) -> { x + 1 }` | | ||
| | Expression lambda | `(x: number) => x + 1` | `(x: int) -> x + 1` | | ||
| | Typed lambda (inline) | `(x: number): number => { return x + 1; }` | `(x: int) -> int { x + 1 }` | | ||
| | Typed lambda (variable) | `const f: (x: number) => number = (x) => x + 1` | `let f: (int) -> int = (x) -> x + 1` | | ||
| | Return type symbol in lambda | `:` (same as declarations, different from types) | `->` (same everywhere) | | ||
| | Object return gotcha | `(x) => ({ key: x })` (parens required) | Not applicable (braces always mean body) | | ||
|
|
||
| The TypeScript "object literal return" gotcha — where `(x) => { key: x }` is parsed as a block with a label, not an object — does not exist in our syntax because we do not overload `{}` to mean both "block" and "object literal" in lambda position. | ||
|
|
||
| --- | ||
|
|
||
| ## 9. Function Types | ||
|
|
||
| ### 9.1 The Core Insight | ||
|
|
||
| A function's type should be its declaration with the name and body removed. Given: | ||
|
|
||
| ``` | ||
| function add(x: int, y: int) -> int { | ||
| x + y | ||
| } | ||
| ``` | ||
|
|
||
| The type is: | ||
|
|
||
| ``` | ||
| (x: int, y: int) -> int | ||
| ``` | ||
|
|
||
| You literally strip the keyword, name, and body. The same `->` symbol, the same parameter syntax, the same return type position. | ||
|
|
||
| ### 9.2 Parameter Names Are Optional in Types | ||
|
|
||
| In type position, parameter names serve as documentation only. They don't affect type checking or calling convention: | ||
|
|
||
| ``` | ||
| // All three are the same type: | ||
| (x: int, y: int) -> int | ||
| (a: int, b: int) -> int | ||
| (int, int) -> int | ||
| ``` | ||
|
|
||
| Named parameters in types help with readability in complex signatures: | ||
|
|
||
| ``` | ||
| // Clearer with names | ||
| (url: string, timeout: int, retries: int) -> Response | ||
|
|
||
| // But structurally identical to | ||
| (string, int, int) -> Response | ||
| ``` | ||
|
|
||
| **Important:** Parameter names in type position are strictly for documentation. If you return `(x, y) -> { x - y }` from a function typed as `(y: int, x: int) -> int`, the returned function subtracts its second argument from its first. The type-level names don't rearrange arguments. | ||
|
|
||
| ### 9.3 Higher-Order Function Types | ||
|
|
||
| Functions that accept or return other functions use the same `(params) -> return` syntax nested: | ||
|
|
||
| ``` | ||
| // A function that takes a transformer and applies it | ||
| function apply(f: (int) -> int, value: int) -> int { | ||
| f(value) | ||
| } | ||
|
|
||
| // A function that returns a function | ||
| function makeAdder(n: int) -> (int) -> int { | ||
| (x) -> x + n | ||
| } | ||
|
|
||
| // A function that takes and returns functions | ||
| function compose( | ||
| f: (int) -> string, | ||
| g: (string) -> bool | ||
| ) -> (int) -> bool { | ||
| (x) -> g(f(x)) | ||
| } | ||
| ``` | ||
|
|
||
| ### 9.4 Nesting Reads Left to Right | ||
|
|
||
| The `->` arrow is right-associative, so chained function types read naturally: | ||
|
|
||
| ``` | ||
| // A function returning a function returning a function | ||
| (int) -> (int) -> (int) -> int | ||
|
|
||
| // Reads as: takes int, returns (takes int, returns (takes int, returns int)) | ||
| ``` | ||
|
|
||
| This is the same currying notation familiar from Haskell (`Int -> Int -> Int -> Int`) but with explicit parameter grouping via parentheses. | ||
|
|
||
| ### 9.5 Comparison with TypeScript | ||
|
|
||
| | Concept | TypeScript | Ours | | ||
| |---------|-----------|------| | ||
| | Function type | `(a: number, b: number) => number` | `(a: int, b: int) -> int` | | ||
| | Return type symbol | `=>` (different from `:` in declarations!) | `->` (same as declarations) | | ||
| | Method in interface | `add(a: number, b: number): number` | `add(a: int, b: int) -> int` | | ||
| | Call signature | `{ (x: number): number }` | `(x: int) -> int` | | ||
| | Consistency | 3+ forms for the same concept | 1 form everywhere | | ||
|
|
||
| --- | ||
|
|
||
| ## 10. Putting It Together: The Unified Shape | ||
|
|
||
| Everything is built from one pattern: | ||
|
|
||
| ``` | ||
| (parameters) -> [return type] [{ body }] | ||
| ``` | ||
|
|
||
| The presence or absence of each optional piece determines what you're looking at: | ||
|
|
||
| | Has return type? | Has body? | What is it? | | ||
| |-----------------|-----------|-------------| | ||
| | Yes | Yes (`{ ... }`) | Typed lambda | | ||
| | No | Yes (`{ ... }`) | Untyped lambda (inferred) | | ||
| | Yes | No | Function type | | ||
| | No | No | — (not valid alone) | | ||
|
|
||
| And a named function declaration is simply this pattern with `function name` prepended: | ||
|
|
||
| ``` | ||
| function name(parameters) -> [return type] { body } | ||
| ``` | ||
|
|
||
| Everything composes from the same pieces. There is nothing extra to memorize. | ||
|
|
||
| --- | ||
|
|
||
| ## 11. Error Handling: `throws` Clauses | ||
|
|
||
| ### 11.1 Motivation | ||
|
|
||
| Many languages treat errors as invisible — you can't tell from a function's signature whether it might fail. Java introduced checked exceptions but made them syntactically heavy. We want a lightweight way to express fallibility in the type system. | ||
|
|
||
| ### 11.2 Syntax | ||
|
|
||
| The `throws` clause sits between the return type and the body (or end of type): | ||
|
|
||
| ``` | ||
| // Named function | ||
| function divide(x: int, y: int) -> int throws DivideByZero { | ||
| if y == 0 { throw DivideByZero() } | ||
| x / y | ||
| } | ||
|
|
||
| // Typed lambda | ||
| (x: int, y: int) -> int throws DivideByZero { | ||
| if y == 0 { throw DivideByZero() } | ||
| x / y | ||
| } | ||
|
|
||
| // Braceless lambda — throws but no return type annotation | ||
| (x: int) throws ParseError -> parse(x) | ||
|
|
||
| // Function type | ||
| (int, int) -> int throws DivideByZero | ||
| ``` | ||
|
|
||
| ### 11.3 Multiple Error Types | ||
|
|
||
| Multiple error types are separated by `|` (union): | ||
|
|
||
| ``` | ||
| function fetch(url: string) -> Response throws NetworkError | TimeoutError { | ||
| ... | ||
| } | ||
|
|
||
| // In type position: | ||
| (string) -> Response throws NetworkError | TimeoutError | ||
| ``` | ||
|
|
||
| ### 11.4 Higher-Order Functions with Throws | ||
|
|
||
| The `throws` clause composes naturally in higher-order function types: | ||
|
|
||
| ``` | ||
| // Accept a fallible function and handle its errors | ||
| function withRetry( | ||
| f: (string) -> Response throws NetworkError, | ||
| retries: int | ||
| ) -> Response throws TimeoutError { | ||
| ... | ||
| } | ||
| ``` | ||
|
|
||
| ### 11.5 Why This Position? | ||
|
|
||
| The `throws` clause appears after the return type because it modifies the function's contract as a whole, not just the return value. Reading left to right: "takes these parameters, returns this type, but might throw these errors." It's also the position Java and Kotlin use, so it's familiar. | ||
|
|
||
| --- | ||
|
|
||
| ## 12. Pattern Matching: `match` and `=>` | ||
|
|
||
| ### 12.1 The Arrow Budget | ||
|
|
||
| We've established that `->` means "produces a value / returns a type" in function context. Pattern matching also involves a "produces" relationship — a pattern arm produces a result. Should it reuse `->`? | ||
|
|
||
| Consider a match expression where the result is a lambda: | ||
|
|
||
| ``` | ||
| // Using -> for both match arms and function returns | ||
| match mode { | ||
| "add" -> (x: int) -> x + 1 | ||
| "sub" -> (x: int) -> x - 1 | ||
| } | ||
| ``` | ||
|
|
||
| Two `->` on the same line with different meanings. Now consider returning a function type: | ||
|
|
||
| ``` | ||
| match mode { | ||
| "add" -> (int) -> int | ||
| "sub" -> (int) -> int | ||
| } | ||
| ``` | ||
|
|
||
| This is technically parseable but visually confusing. Which `->` is the match arm separator and which is the function return arrow? | ||
|
|
||
| ### 12.2 Decision: `=>` for Match Arms | ||
|
|
||
| We use `=>` exclusively for pattern matching, keeping `->` exclusively for functions: | ||
|
|
||
| ``` | ||
| match mode { | ||
| "add" => (x: int) -> x + 1 | ||
| "sub" => (x: int) -> x - 1 | ||
| } | ||
| ``` | ||
|
|
||
| Now every `->` in the language means "function arrow" and every `=>` means "pattern maps to result." No mixing. | ||
|
|
||
| ``` | ||
| match request { | ||
| GET(path) => handleGet(path) | ||
| POST(path, body) => handlePost(path, body) | ||
| _ => (req) -> { defaultHandler(req) } | ||
| } | ||
| ``` | ||
|
|
||
| ### 12.3 Precedent | ||
|
|
||
| This matches Rust (`=>` for match arms, `->` for return types) and Scala (`=>` for case arms, `->` for function types). Both languages report that developers internalize the distinction quickly. | ||
|
|
||
| ### 12.4 Arrow Symbol Summary | ||
|
|
||
| | Symbol | Meaning | Example | | ||
| |--------|---------|---------| | ||
| | `->` | Function returns / produces | `(int) -> int`, `(x) -> x + 1` | | ||
| | `=>` | Pattern maps to result | `Some(x) => x`, `"add" => handler` | | ||
| | `:` | Has type / key-value | `x: int`, `{ key: value }` | | ||
|
|
||
| Three symbols, three meanings, no overlap. | ||
|
|
||
| --- | ||
|
|
||
| ## 13. Alternatives Considered | ||
|
|
||
| ### 13.1 `fn` Instead of `function` | ||
|
|
||
| ``` | ||
| fn add(x: int, y: int) -> int { x + y } | ||
| ``` | ||
|
|
||
| **Pros:** Shorter. Familiar from Rust.\ | ||
| **Cons:** Less familiar to the TypeScript/JavaScript audience we're targeting. Keyword brevity is a marginal benefit — modern editors auto-complete it. The full word `function` also acts as a stronger visual anchor when scanning code. | ||
|
|
||
| **Decision:** Use `function`. Revisit if community feedback strongly favors `fn`. | ||
|
|
||
| ### 13.2 `:` for Return Types (TypeScript Style) | ||
|
|
||
| ``` | ||
| function add(x: int, y: int): int { x + y } | ||
| ``` | ||
|
|
||
| **Pros:** Identical to TypeScript syntax. Zero learning curve for TS developers.\ | ||
| **Cons:** Creates the declaration/type inconsistency described in Section 2.2. The function type would need a different symbol (TypeScript uses `=>`), and we'd lose the "strip the name and body to get the type" property. This is the single biggest source of syntactic inconsistency in TypeScript and the primary motivation for this design. | ||
|
|
||
| **Decision:** Rejected. The consistency gains from `->` outweigh the familiarity cost. | ||
|
|
||
| ### 13.3 `=>` for Lambdas (TypeScript Style) | ||
|
|
||
| ``` | ||
| items.map((x) => x.name) | ||
| ``` | ||
|
|
||
| **Pros:** Familiar to every JavaScript developer.\ | ||
| **Cons:** If `=>` is used for lambdas, it can't also be used for pattern matching without ambiguity. And if we use `->` for declarations but `=>` for lambdas, we've reintroduced the two-arrow problem. The whole point of this design is one arrow for functions. | ||
|
|
||
| **Decision:** Rejected. `->` for all function contexts; `=>` reserved for `match`. | ||
|
|
||
| ### 13.4 Mandatory Braces on All Lambdas | ||
|
|
||
| ``` | ||
| items.map((x) -> { x.name }) | ||
| items.filter((x) -> { x.active }) | ||
| ``` | ||
|
|
||
| **Pros:** Maximally unambiguous. The parser never needs to distinguish type-vs-value from context because `{` always signals a body.\ | ||
| **Cons:** Adds visual noise to the most common lambda use case. Chained higher-order functions (map/filter/reduce) become noticeably harder to scan. Go gets away with verbose anonymous functions because its culture favors `for` loops over functional pipelines. If our language encourages functional composition — and our investment in higher-order function syntax suggests it does — the cost is too high. | ||
|
|
||
| **Decision:** Rejected. Allow braceless lambdas for single expressions. The type/value ambiguity is resolved by distinguishing type names from variable names (see Section 8.4). | ||
|
|
||
| ### 13.5 Trailing Closure Syntax (Kotlin/Swift Style) | ||
|
|
||
| ``` | ||
| items.filter { x -> x.active } | ||
| items.map { x -> x.name } | ||
| ``` | ||
|
|
||
| **Pros:** Very concise for single-lambda arguments. Eliminates nested parentheses.\ | ||
| **Cons:** Introduces a new syntax just for one pattern (last-argument lambdas). Adds complexity to the grammar. Our braceless lambda syntax already handles the common case well enough: `items.filter((x) -> x.active)`. | ||
|
|
||
| **Decision:** Deferred. This can be added later as sugar without breaking anything. We focus on getting the core right first. | ||
|
|
||
| ### 13.6 `->` for Match Arms (Unify Everything Under One Arrow) | ||
|
|
||
| ``` | ||
| match x { | ||
| 0 -> "zero" | ||
| n -> "other" | ||
| } | ||
| ``` | ||
|
|
||
| **Pros:** Only one arrow symbol in the language.\ | ||
| **Cons:** Ambiguity when match arms return functions or function types (see Section 12.1). Two identical symbols with different scoping rules on the same line is a readability failure. The cost of a second arrow symbol is low; the cost of visual ambiguity is high. | ||
|
|
||
| **Decision:** Rejected. `=>` for match, `->` for functions. | ||
|
|
||
| --- | ||
|
|
||
| ## 14. Summary | ||
|
|
||
| The complete syntax at a glance: | ||
|
|
||
| ``` | ||
| // ━━━ Named Functions ━━━ | ||
|
|
||
| function add(x: int, y: int) -> int { | ||
| x + y | ||
| } | ||
|
|
||
| function fetchData(url: string) -> Response throws NetworkError | TimeoutError { | ||
| ... | ||
| } | ||
|
|
||
|
|
||
| // ━━━ Lambdas ━━━ | ||
|
|
||
| // Braceless (expression body, types inferred) | ||
| (x) -> x + 1 | ||
| (a, b) -> a + b | ||
|
|
||
| // Braced (block body, optional type annotation) | ||
| (x: int) -> { x + 1 } | ||
| (x: int) -> int { x + 1 } | ||
| (x: int) -> int throws ParseError { parse(x) } | ||
|
|
||
|
|
||
| // ━━━ Function Types ━━━ | ||
|
|
||
| (int) -> int | ||
| (string, int) -> Response | ||
| (url: string, retries: int) -> Response throws NetworkError | ||
|
|
||
|
|
||
| // ━━━ Higher-Order Functions ━━━ | ||
|
|
||
| function apply(f: (int) -> int, value: int) -> int { | ||
| f(value) | ||
| } | ||
|
|
||
| function compose(f: (A) -> B, g: (B) -> C) -> (A) -> C { | ||
| (x) -> g(f(x)) | ||
| } | ||
|
|
||
|
|
||
| // ━━━ Pattern Matching ━━━ | ||
|
|
||
| match status { | ||
| 200 => "OK" | ||
| 404 => "Not Found" | ||
| code => "Unknown: " + code.toString() | ||
| } | ||
|
|
||
|
|
||
| // ━━━ Chaining ━━━ | ||
|
|
||
| items | ||
| .filter((x) -> x.active) | ||
| .map((x) -> x.name) | ||
| .sort((a, b) -> a.compareTo(b)) | ||
| ``` | ||
|
|
||
| ### The Core Rule | ||
|
|
||
| Everything follows from one pattern: | ||
|
|
||
| ``` | ||
| (params) -> [return type] [throws Errors] [{ body }] | ||
| ``` | ||
|
|
||
| Prepend `function name` for a declaration. Remove the body for a type. It's the same shape, everywhere, every time. |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Add language specifiers to fenced code blocks for proper syntax highlighting.
Throughout the document, many fenced code blocks lack language identifiers. For a language design proposal, proper syntax highlighting helps readers understand the examples. Consider adding language specifiers (e.g., ```ts, ```python, or a custom identifier for your language syntax).
The static analysis tool flagged 33 instances. While this is a minor issue, it affects readability and documentation quality.
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 222-222: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 274-274: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 294-294: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 326-326: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 339-339: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 346-346: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 356-356: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 364-364: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 371-371: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 381-381: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 382-382: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above
(MD022, blanks-around-headings)
[warning] 386-386: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 421-421: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 460-460: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 468-468: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 478-478: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 487-487: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 501-501: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 525-525: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 550-550: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 565-565: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 583-583: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 607-607: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 620-620: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 644-644: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 654-654: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 667-667: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 676-676: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 704-704: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 715-715: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 726-726: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 737-737: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 749-749: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 761-761: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 779-779: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 842-842: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
| (x: int) -> int { x + 1 } // expression with operator → this is a lambda | ||
| (x: int) -> T { x + 1 } // brace → this is a lambda with block body | ||
| ``` | ||
|
|
||
| ``` | ||
| array.map((x) -> string { x.name }) | ||
| array.map((x) -> { x.name }) | ||
| type foo = (x: string) -> x.name | ||
| array.map((x) -> x.name) | ||
|
|
||
|
|
||
| array.map((x): string -> x.name) | ||
| type foo = (x: string): x.name | ||
| array.map((x) -> x.name) | ||
| ``` |
There was a problem hiding this comment.
Clarify or revise the ambiguous example code.
This section contains example code that appears incomplete or unclear:
(x: int) -> int { x + 1 } // expression with operator → this is a lambda
(x: int) -> T { x + 1 } // brace → this is a lambda with block body
Then immediately followed by more examples:
array.map((x) -> string { x.name })
array.map((x) -> { x.name })
type foo = (x: string) -> x.name
array.map((x) -> x.name)
array.map((x): string -> x.name)
type foo = (x: string): x.name
array.map((x) -> x.name)
This code block lacks context and explanation. The second group of examples appears disconnected from the surrounding text and uses syntax (: in (x): string) that contradicts the proposal's -> convention.
Additionally, there's a formatting issue: the code block at line 371 is missing a blank line before the heading at line 382 (Section 8.5).
📝 Suggested revision
Consider either:
- Adding explanatory text connecting these examples to the disambiguation discussion, or
- Removing the second example group if it represents an alternative syntax not being proposed, or
- Clearly marking which examples are valid vs. invalid in this proposal
Also add a blank line before the "### 8.5 Typed Lambdas" heading.
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 371-371: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 381-381: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
| ```ts | ||
| function add(x: int, y: int): int { | ||
| x + y | ||
| } | ||
|
|
||
| function greet(name: string): string { | ||
| "Hello, " + name | ||
| } | ||
|
|
||
| function log(message: string) { | ||
| print(message) | ||
| } | ||
| ``` | ||
|
|
||
| When no return type is specified, the function is inferred to return `void`. An explicit `: void` annotation is permitted but not required. | ||
|
|
||
| The syntax is identical to TypeScript with two exceptions: primitive type names differ (`int` vs `number`), and the last expression in a block is its return value (see Section 4.1). | ||
|
|
||
| ### 3.2 Lambda Expressions | ||
|
|
||
| Lambda expressions use parenthesized parameters, the `=>` arrow, and either an expression body or a brace-delimited block body: | ||
|
|
||
| ```ts | ||
| // Expression body — single expression, no braces | ||
| (x: int) => x + 1 | ||
| items.map((x) => x.name) | ||
| items.filter((x) => x.active) | ||
|
|
||
| // Block body — multiple statements, braces required | ||
| (x: int, y: int) => { | ||
| let sum = x + y | ||
| sum * 2 | ||
| } | ||
| ``` | ||
|
|
||
| Parentheses around parameters are always required, including for single-parameter lambdas. The form `x => x + 1` (without parentheses) is a syntax error. See Section 4.3 for rationale. | ||
|
|
||
| ### 3.3 Typed Lambdas | ||
|
|
||
| TypeScript provides two mechanisms for annotating the return type of a lambda expression: | ||
|
|
||
| ```ts | ||
| // Inline annotation — return type placed between parameters and arrow | ||
| let f = (x: number): number => x + 1 | ||
| let g = (x: number): number => { return x + 1 } | ||
|
|
||
| // Contextual annotation — type placed on the binding, lambda infers from context | ||
| let f: (x: number) => number = (x) => x + 1 | ||
| ``` | ||
|
|
||
| This proposal retains both mechanisms: | ||
|
|
||
| ```ts | ||
| // Inline annotation | ||
| let f = (x: int): int => x + 1 | ||
| let g = (x: int): int => { | ||
| x + 1 | ||
| } | ||
|
|
||
| // Contextual annotation | ||
| let f: (x: int) => int = (x) => x + 1 | ||
| ``` | ||
|
|
||
| The inline form uses `:` for the return type (matching declaration syntax), while the contextual form uses `=>` (matching function type syntax). This is the same declaration/type notational split present in TypeScript; Section 5.1 discusses the tradeoffs of inheriting it. | ||
|
|
||
| In practice, most lambda expressions appear in callback position where parameter and return types are inferred from the enclosing function signature: | ||
|
|
||
| ```ts | ||
| // Types of x inferred from the signature of .map() | ||
| items.map((x) => x.name) | ||
| ``` | ||
|
|
||
| Explicit lambda type annotations are uncommon in idiomatic TypeScript and are expected to be uncommon in our language as well. | ||
|
|
||
| ### 3.4 Function Types | ||
|
|
||
| Function types use the `=>` arrow, matching TypeScript: | ||
|
|
||
| ```ts | ||
| (x: int, y: int) => int | ||
| (string) => bool | ||
| (url: string, retries: int) => Response | ||
| ``` | ||
|
|
||
| Parameter names in type position are optional and serve as documentation only. The following are structurally identical types: | ||
|
|
||
| ```ts | ||
| (x: int, y: int) => int | ||
| (a: int, b: int) => int | ||
| (int, int) => int | ||
| ``` | ||
|
|
||
| ### 3.5 Higher-Order Functions | ||
|
|
||
| Functions that accept or return other functions compose naturally: | ||
|
|
||
| ```ts | ||
| function apply(f: (int) => int, value: int): int { | ||
| f(value) | ||
| } | ||
|
|
||
| function compose(f: (int) => string, g: (string) => bool): (int) => bool { | ||
| (x) => g(f(x)) | ||
| } | ||
|
|
||
| function makeAdder(n: int): (int) => int { | ||
| (x) => x + n | ||
| } | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 4. Deviations from TypeScript | ||
|
|
||
| Each deviation addresses a documented class of bugs or developer friction. Deviations are enumerated explicitly so that the scope of change is clear. | ||
|
|
||
| ### 4.1 Implicit Return | ||
|
|
||
| **Problem.** In TypeScript, block-body functions require an explicit `return` statement. Omitting it causes the function to return `undefined`. Whether this produces a compile-time error depends on how types flow through the surrounding code: | ||
|
|
||
| ```ts | ||
| // No error — TypeScript infers names as void[], not string[] | ||
| const names = items.map((x) => { | ||
| const cleaned = x.name.trim() | ||
| cleaned.toUpperCase() | ||
| }) | ||
| // names is void[] — the bug propagates silently until something downstream fails | ||
| ``` | ||
|
|
||
| If the variable were explicitly annotated (`const names: string[] = ...`), TypeScript would catch the mismatch. But most TypeScript code relies on type inference, so the `undefined` propagates — sometimes through multiple layers — before surfacing as a type error far from the original mistake, or as a runtime failure. | ||
|
|
||
| The root cause is that TypeScript defines two body forms with different return semantics: expression bodies (implicit return) and block bodies (explicit return). Refactoring from expression body to block body — a routine edit when adding a statement to a callback — changes whether the computed value is returned or discarded. In well-annotated codebases the compiler catches this; in inference-heavy code it often does not. | ||
|
|
||
| This pattern is common enough to warrant four separate ESLint rules: `array-callback-return`, `consistent-return`, `no-useless-return`, and `arrow-body-style`. The existence of these rules suggests the type system alone does not reliably prevent this class of bug. | ||
|
|
||
| **Resolution.** In our language, the last expression in a block is its return value. The `return` keyword remains available for early exits but is never required at the end of a function body: | ||
|
|
||
| ```ts | ||
| items.map((x) => { | ||
| let cleaned = x.name.trim() | ||
| cleaned.toUpperCase() // this IS the return value | ||
| }) | ||
| ``` | ||
|
|
||
| This applies uniformly to `function` declarations and `=>` lambdas. Refactoring between expression body and block body requires only adding or removing braces — no change in return behavior. | ||
|
|
||
| ### 4.2 Two Function Forms | ||
|
|
||
| **Problem.** TypeScript provides six syntactically distinct ways to define a function (declaration, function expression, arrow with expression body, arrow with block body, class method, object method shorthand), each with different behavior regarding `this` binding, hoisting, and the `arguments` object. This produces linter rules such as `func-style`, `prefer-arrow-callback`, and `no-loop-func`. | ||
|
|
||
| **Resolution.** This language provides two forms: | ||
|
|
||
| | Form | Syntax | Use case | | ||
| |------|--------|----------| | ||
| | Declaration | `function f(x: int): int { ... }` | Named, top-level or nested functions | | ||
| | Lambda | `(x: int) => ...` | Anonymous, inline functions | | ||
|
|
||
| There is no function expression syntax (`const f = function() {}`). Both forms share identical semantics: same capture behavior, no `this` binding distinction, no hoisting. The choice between forms is purely syntactic — named vs. anonymous. | ||
|
|
||
| ### 4.3 Mandatory Parentheses | ||
|
|
||
| **Problem.** TypeScript permits omitting parentheses for single-parameter arrow functions (`x => x + 1`), producing the linter rule `arrow-parens` and inconsistent formatting across codebases. | ||
|
|
||
| **Resolution.** Parentheses are always required: `(x) => x + 1`. The form `x => x + 1` is a syntax error. | ||
|
|
||
| ### 4.4 Checked Error Types (`throws`) | ||
|
|
||
| **Addition.** TypeScript provides no mechanism to express in the type system that a function may throw. This proposal adds an optional `throws` clause: | ||
|
|
||
| ```ts | ||
| function divide(x: int, y: int): int throws DivideByZero { | ||
| if y == 0 { throw DivideByZero() } | ||
| x / y | ||
| } | ||
| ``` | ||
|
|
||
| The `throws` clause appears after the return type in both declarations and function types: | ||
|
|
||
| ```ts | ||
| // Declaration | ||
| function fetch(url: string): Response throws NetworkError | TimeoutError { | ||
| ... | ||
| } | ||
|
|
||
| // Function type | ||
| (string) => Response throws NetworkError | TimeoutError | ||
|
|
||
| // Higher-order function accepting a fallible callback | ||
| function withRetry( | ||
| f: (string) => Response throws NetworkError, | ||
| retries: int | ||
| ): Response throws TimeoutError { | ||
| ... | ||
| } | ||
| ``` | ||
|
|
||
| Multiple error types are separated by `|` (union syntax). | ||
|
|
||
| The `throws` clause is valid only on braced forms — declarations and block-body lambdas. Expression-body lambdas cannot carry `throws` annotations. Rationale: if a function is complex enough to have checked error types, it is complex enough to warrant braces. This restriction also avoids syntactic ambiguity in expression-body position. | ||
|
|
||
| ### 4.5 Pattern Matching | ||
|
|
||
| This proposal introduces `match` expressions using `=>` for pattern arms, following the precedent established by Scala and Rust: | ||
|
|
||
| ```ts | ||
| match status { | ||
| 200 => "OK" | ||
| 404 => "Not Found" | ||
| code => "Unknown: " + code.toString() | ||
| } | ||
| ``` | ||
|
|
||
| The `=>` token is used for both lambda expressions and match arms. Context always disambiguates: within a `match` block, `=>` separates a pattern from its result expression; outside a `match` block, `=>` introduces a lambda body. | ||
|
|
||
| When a match arm produces a lambda, both uses appear on the same line: | ||
|
|
||
| ```ts | ||
| match mode { | ||
| "add" => (x: int) => x + 1 | ||
| "sub" => (x: int) => x - 1 | ||
| } | ||
| ``` | ||
|
|
||
| This is the same nesting that Scala has supported since its introduction. The parenthesized parameter list of the lambda provides a clear visual boundary between the match arm separator and the lambda arrow. | ||
|
|
||
| --- | ||
|
|
||
| ## 5. Inherited Inconsistencies and Open Concerns | ||
|
|
||
| This section documents known syntactic inconsistencies inherited from TypeScript and presents both sides of the argument. These are the most substantive objections to this proposal and deserve rigorous treatment. | ||
|
|
||
| ### 5.1 The Declaration/Type Notational Split | ||
|
|
||
| Function declarations use `:` for return types. Function types use `=>`: | ||
|
|
||
| ```ts | ||
| // Declaration — colon | ||
| function add(x: int, y: int): int { x + y } | ||
|
|
||
| // Type — fat arrow | ||
| (int, int) => int | ||
| ``` | ||
|
|
||
| Under this proposal, the type of a function cannot be derived from its declaration by simply removing the name and body — the return type delimiter must also change from `:` to `=>`. The current `->` design does not have this problem. | ||
|
|
||
| **The case for accepting this inconsistency:** | ||
|
|
||
| 1. In over ten years of TypeScript usage, this split has produced no linter rules, no commonly reported bugs, and no significant developer complaints. | ||
| 2. The two forms appear in syntactically distinct positions (declaration vs. type annotation), so developers are unlikely to confuse them. | ||
| 3. Resolving the inconsistency requires changing either the declaration syntax (departing from TypeScript) or the type syntax (departing from all established convention). Both options impose a familiarity cost. | ||
|
|
||
| **The case against accepting it:** | ||
|
|
||
| The absence of linter rules in TypeScript does not prove the absence of friction — it may indicate that the friction is ambient and normalized. TypeScript developers never had a choice; they learned the two forms because there was no alternative. For a new language, this is an opportunity to avoid inheriting a known wart rather than rationalizing it. | ||
|
|
||
| The current syntax's property — "remove the name and body to get the type" — is genuinely pedagogically valuable. It means one concept to teach instead of two. This proposal lacks an equivalently concise teaching story. The closest formulation is: "declarations use `:`, types use `=>`, and you learn which is which from context" — which is accurate but not elegant. | ||
|
|
||
| **Assessment.** This is the strongest argument against this proposal. Whether it is decisive depends on how much weight one places on syntactic elegance and teachability vs. compatibility with established practice. This proposal places the weight on compatibility, but acknowledges that reasonable designers may disagree. | ||
|
|
||
| ### 5.2 The `=>` Overloading Problem | ||
|
|
||
| The `=>` token appears in three contexts: lambda expressions, function types, and match arms. While these are contextually unambiguous for the parser, the human readability cost is real. | ||
|
|
||
| Consider a match arm that returns a lambda: | ||
|
|
||
| ```ts | ||
| match mode { | ||
| "add" => (x: int) => x + 1 | ||
| "sub" => (x: int) => x - 1 | ||
| } | ||
| ``` | ||
|
|
||
| Two `=>` tokens on one line with different semantic roles. The current syntax avoids this entirely by reserving `=>` for match arms and `->` for functions: | ||
|
|
||
| ``` | ||
| match mode { | ||
| "add" => (x: int) -> x + 1 | ||
| "sub" => (x: int) -> x - 1 | ||
| } | ||
| ``` | ||
|
|
||
| In the current design, the arrow symbol itself tells the reader which construct they are looking at. In this proposal, the reader must rely on surrounding context (parenthesized parameters, `match` block boundaries) to distinguish the two uses. | ||
|
|
||
| Scala has the same `=>` overloading and has not found it to be a significant usability issue. However, the existence of precedent does not settle the question of whether a new language should inherit the pattern when an alternative is available. | ||
|
|
||
| ### 5.3 The Familiarity Argument Cuts Both Ways | ||
|
|
||
| This proposal argues that TypeScript familiarity is a significant asset. But this language is not TypeScript. It already departs in substantive ways: | ||
|
|
||
| - Implicit return (TypeScript requires `return`) | ||
| - No `this` binding distinction between forms | ||
| - No hoisting | ||
| - No `arguments` object | ||
| - Different primitive types (`int` vs `number`) | ||
| - `match` expressions | ||
| - `throws` clauses | ||
|
|
||
| A TypeScript developer encountering this language must already recalibrate their mental model. The question is whether `:` vs `->` for return types is a meaningful additional cost on top of all that — and it is difficult to argue that it is. | ||
|
|
||
| Meanwhile, this proposal asks the larger group (Python developers, two-thirds of users) to absorb the `:` return type convention when `->` would transfer directly from their Python experience. The proposal essentially asks the larger group to accommodate the smaller group's conventions. | ||
|
|
||
| **Counterpoint.** Many Python developers in our user base have prior TypeScript or JavaScript experience. The `:` convention is not alien to them — it is simply not their primary language's convention. And the structural changes Python developers must absorb (braces, `function`, type system) are large enough that the return type symbol is a marginal detail. | ||
|
|
||
| ### 5.4 The AI Code Generation Argument Is Time-Limited | ||
|
|
||
| This proposal's AI compatibility argument is pragmatically compelling today but has a limited shelf life. LLMs adapt to new syntax quickly given even modest training data. Rust, Kotlin, and Swift all receive reasonable code generation despite none of them using TypeScript syntax. Within a year or two of this language having published documentation, example repositories, and open-source code, the AI generation advantage is expected to diminish substantially. | ||
|
|
||
| Baking a permanent syntax decision into the language to optimize for a transient property of current models is a questionable tradeoff. The syntax will outlive the current generation of language models by many years. | ||
|
|
||
| **Counterpoint.** The advantage may be transient, but the cost of TS-aligned syntax is also low — it is not a bad syntax, merely a familiar one. The question is whether the short-term benefit justifies choosing a syntax that is otherwise acceptable. If the TS-aligned syntax were actively harmful, the time-limited nature of the AI argument would be decisive. Since it is merely "less elegant," the transient benefit still has value. | ||
|
|
||
| ### 5.5 Long-Term Trajectory | ||
|
|
||
| If this language adopts TypeScript syntax now for familiarity, and subsequently diverges further from TypeScript — as new languages invariably do — the result may be a language that inherits TypeScript's inconsistencies without the compensating benefit of proximity to TypeScript. The familiarity argument depreciates as the language develops its own identity. | ||
|
|
||
| The current design does not have this problem. Its consistency is an intrinsic property of the syntax, not a relational one. It remains consistent regardless of how far the language diverges from TypeScript. | ||
|
|
||
| **Counterpoint.** All design decisions are made in context. A future language evolution that renders the TS alignment moot can also introduce syntax changes at that time. Optimizing for the present user base and current adoption landscape is not unreasonable, provided the syntax is not actively harmful. | ||
|
|
||
| --- | ||
|
|
||
| ## 6. Cross-Language Comparison | ||
|
|
||
| The following tables compare function syntax across languages likely to be familiar to our user base. | ||
|
|
||
| ### 6.1 Declarations and Types | ||
|
|
||
| | Language | Declaration | Function Type | | ||
| | -------------- | -------------------------------------------- | ----------------------- | | ||
| | **TypeScript** | `function f(x: number): number { return x }` | `(x: number) => number` | | ||
| | **Python** | `def f(x: int) -> int: return x` | `Callable[[int], int]` | | ||
| | **Kotlin** | `fun f(x: Int): Int { return x }` | `(Int) -> Int` | | ||
| | **Scala** | `def f(x: Int): Int = x` | `Int => Int` | | ||
| | **Swift** | `func f(x: Int) -> Int { return x }` | `(Int) -> Int` | | ||
| | **Go** | `func f(x int) int { return x }` | `func(int) int` | | ||
| | **Ours** | `function f(x: int): int { x }` | `(int) => int` | | ||
|
|
||
| ### 6.2 Lambdas and Higher-Order Functions | ||
|
|
||
| | Language | Lambda | HOF Parameter | | ||
| | -------------- | ------------------------------------------ | -------------------------- | | ||
| | **TypeScript** | `(x) => x + 1` | `f: (x: number) => number` | | ||
| | **Python** | `lambda x: x + 1` | `f: Callable[[int], int]` | | ||
| | **Kotlin** | `{ x -> x + 1 }` | `f: (Int) -> Int` | | ||
| | **Scala** | `(x: Int) => x + 1` | `f: Int => Int` | | ||
| | **Swift** | `{ (x: Int) in x + 1 }` | `f: (Int) -> Int` | | ||
| | **Go** | `let foo = func(x int) int { return x+1 }` | `f func(int) int` | | ||
| | **Ours** | `(x) => x + 1` | `f: (int) => int` | | ||
|
|
||
| This proposal produces the shortest syntactic distance from TypeScript of any option considered. | ||
|
|
||
| ### 6.3 Match/When Arms | ||
|
|
||
| | Language | Match Arm Syntax | | ||
| |----------|-----------------| | ||
| | **Kotlin** | `x -> result` (in `when`) | | ||
| | **Scala** | `case x => result` | | ||
| | **Rust** | `x => result` | | ||
| | **Swift** | `case x: result` | | ||
| | **Ours** | `x => result` | | ||
|
|
||
| The use of `=>` for match arms follows Scala and Rust. | ||
|
|
||
| --- | ||
|
|
||
| ## 7. Tradeoff Analysis | ||
|
|
||
| ### 7.1 Arguments For This Proposal | ||
|
|
||
| **Familiarity.** TypeScript developers constitute approximately one-third of the current user base. This proposal requires zero syntactic adjustment from them for function declarations, lambdas, and function types. The only new concepts are implicit return, `throws`, and `match` — all of which are additive. | ||
|
|
||
| **Documentation economy.** The language's function syntax can be specified as "TypeScript with the following exceptions," reducing the documentation surface. Each shared convention is a convention that needs no explanation. | ||
|
|
||
| **AI code generation.** Language models produce TypeScript-patterned code by default. Syntactic alignment reduces error rates in generated code in the near term, though this advantage is expected to diminish as language-specific training data becomes available (see Section 5.4). | ||
|
|
||
| **Python developer adaptation.** Python developers must learn braces, `function`, and a structural type system regardless of arrow choice. The difference between `:` and `->` for return types is minor relative to these structural changes. Additionally, many Python developers in our user base have prior TypeScript experience. | ||
|
|
||
| **Tooling bootstrapping.** Syntax highlighting, tree-sitter grammars, TextMate scopes, and editor integrations can be adapted from TypeScript tooling rather than designed from first principles. | ||
|
|
||
| ### 7.2 Arguments For Keeping the Current (`->`) Syntax | ||
|
|
||
| **Syntactic consistency.** `->` everywhere means one concept to learn, one concept to teach. The type of a function is its declaration with the name and body removed. No symbol swapping, no contextual rules. | ||
|
|
||
| **Pedagogical clarity.** The current syntax has a one-liner teaching story: "remove the name and body — that's your type." This proposal's equivalent is "declarations use `:`, types use `=>`, and you learn which is which from context," which is accurate but not elegant. | ||
|
|
||
| **Readability in mixed contexts.** The current syntax's `->` for functions and `=>` for match arms gives the reader a visual signal about which construct they are in. This proposal's `=>` overloading requires the reader to rely on surrounding context. | ||
|
|
||
| **Better alignment with the larger user group.** Python developers (two-thirds of users) already know `->` for return types. The current syntax preserves this familiarity; this proposal does not. | ||
|
|
||
| **Long-term resilience.** The current syntax's consistency is an intrinsic property. It remains clean regardless of how far the language diverges from TypeScript. This proposal's familiarity advantage is relational and depreciates as the language develops its own identity. | ||
|
|
||
| **The burden of proof is backwards.** For a new language already making substantive semantic departures from TypeScript, the default should be to design clean syntax — not to inherit known inconsistencies and then justify them. The burden should be on accepting the declaration/type split, not on fixing it. | ||
|
|
||
| ### 7.3 Assessment | ||
|
|
||
| This is a genuine design tension with no objectively correct resolution. The two proposals optimize for different properties: | ||
|
|
||
| | Property | Current (`->`) | This proposal | | ||
| |----------|---------|---------------| | ||
| | Internal consistency | Strong | Weak (inherits TS split) | | ||
| | Teachability | One concept | Two notations for one concept | | ||
| | TS developer onboarding | Small cost | Zero cost | | ||
| | Python developer familiarity | `->` transfers | `:` does not | | ||
| | AI compatibility (near-term) | Requires adaptation | Compatible by default | | ||
| | Long-term resilience | Intrinsic consistency | Relational familiarity (depreciates) | | ||
| | Readability in `match` + lambda | Clear (`->` vs `=>`) | Ambiguous (`=>` vs `=>`) | | ||
|
|
||
| This proposal places the highest weight on near-term adoption and practical compatibility. It accepts known inelegances in exchange for a lower barrier to entry. Whether that tradeoff is correct depends on whether one believes this language's long-term success is more constrained by initial adoption friction or by accumulated syntactic debt. | ||
|
|
||
| --- | ||
|
|
||
| ## 8. Comparison with Current Syntax | ||
|
|
||
| | Aspect | Current (`->` unified) | This proposal (TS-aligned) | | ||
| |--------|------------------------|----------------------------| | ||
| | Declaration | `function f(x: int) -> int { x }` | `function f(x: int): int { x }` | | ||
| | Lambda | `(x) -> x + 1` | `(x) => x + 1` | | ||
| | Function type | `(int) -> int` | `(int) => int` | | ||
| | Match arm | `pattern => result` | `pattern => result` | | ||
| | Return type consistency | Same symbol everywhere (`->`) | `:` in declarations, `=>` in types | | ||
| | TS developer familiarity | Small adjustment required | No adjustment required | | ||
| | Python developer familiarity | `->` matches Python return types | `:` does not match | | ||
| | AI code generation | Requires adaptation | Compatible by default | | ||
| | Type derivable from declaration | Yes (remove name and body) | No (must also change `:` to `=>`) | | ||
| | Arrow symbols in language | Two (`->` for functions, `=>` for match) | One (`=>` for all) | | ||
|
|
||
| --- | ||
|
|
||
| ## 9. Open Questions | ||
|
|
||
| The following questions remain unresolved and should be addressed before this proposal advances beyond Draft status: | ||
|
|
||
| 1. **Quantifying the AI claim.** Has code generation error rate been benchmarked with `->` vs `=>` syntax? The assertion that TS-aligned syntax produces fewer AI errors is plausible but unsubstantiated. If the error rate difference is negligible, this proposal loses one of its primary arguments. | ||
|
|
||
| 2. **Teaching the declaration/type split.** How should documentation and tutorials explain the `:` vs `=>` split to beginners? The current syntax can say "remove the name and body — that's your type." This proposal needs an equivalently concise pedagogical framing. | ||
|
|
||
| 3. **Addressing the long-term trajectory.** If the language continues to diverge from TypeScript over time, the familiarity argument weakens. What is the plan for a future in which this language no longer closely resembles TypeScript but still carries its syntactic inconsistencies? | ||
|
|
||
| --- | ||
|
|
||
| ## 10. Specification Summary | ||
|
|
||
| ### 10.1 Complete Syntax | ||
|
|
||
| ```ts | ||
| // ━━━ Named Functions ━━━ | ||
|
|
||
| function add(x: int, y: int): int { | ||
| x + y | ||
| } | ||
|
|
||
| function fetchData(url: string): Response throws NetworkError | TimeoutError { | ||
| ... | ||
| } | ||
|
|
||
|
|
||
| // ━━━ Lambdas ━━━ | ||
|
|
||
| // Expression body (types inferred) | ||
| (x) => x + 1 | ||
| (a, b) => a + b | ||
|
|
||
| // Block body (implicit return) | ||
| (x: int) => { | ||
| let y = x * 2 | ||
| y + 1 | ||
| } | ||
|
|
||
|
|
||
| // ━━━ Function Types ━━━ | ||
|
|
||
| (int) => int | ||
| (string, int) => Response | ||
| (url: string, retries: int) => Response throws NetworkError | ||
|
|
||
|
|
||
| // ━━━ Higher-Order Functions ━━━ | ||
|
|
||
| function apply(f: (int) => int, value: int): int { | ||
| f(value) | ||
| } | ||
|
|
||
| function compose(f: (A) => B, g: (B) => C): (A) => C { | ||
| (x) => g(f(x)) | ||
| } | ||
|
|
||
|
|
||
| // ━━━ Pattern Matching ━━━ | ||
|
|
||
| match status { | ||
| 200 => "OK" | ||
| 404 => "Not Found" | ||
| code => "Unknown: " + code.toString() | ||
| } | ||
|
|
||
|
|
||
| // ━━━ Chaining ━━━ | ||
|
|
||
| items | ||
| .filter((x) => x.active) | ||
| .map((x) => x.name) | ||
| .sort((a, b) => a.compareTo(b)) | ||
| ``` | ||
|
|
||
| ### 10.2 Deviations from TypeScript | ||
|
|
||
| | # | Change | Rationale | | ||
| |---|--------|-----------| | ||
| | 1 | Implicit return (last expression is the block's value) | Eliminates silent `undefined` return bugs | | ||
| | 2 | Two function forms only (`function` and `=>`) | Removes behavioral variants (`this`, hoisting, `arguments`) | | ||
| | 3 | Parentheses always required on lambda parameters | Eliminates `arrow-parens` style inconsistency | | ||
| | 4 | `throws` clause (optional, braced forms only) | Enables checked error types in the type system | | ||
| | 5 | `match` expressions with `=>` arms | Adds pattern matching (Scala/Rust precedent) | | ||
|
|
||
| ### 10.3 Syntax Retained from TypeScript | ||
|
|
||
| All other function-related syntax — the `function` keyword, `:` for parameter and return type annotations, `=>` for lambdas and function types, brace-delimited bodies, and higher-order function composition — is adopted without modification. |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Add language specifiers to fenced code blocks.
Similar to BEP-008, this document has many fenced code blocks without language identifiers (27 instances flagged by static analysis). Adding language specifiers will improve syntax highlighting and readability.
Consider using ts for TypeScript examples and a custom identifier for your proposed syntax examples.
🧰 Tools
🪛 LanguageTool
[style] ~445-~445: Wordiness: Consider shortening this phrase.
Context: ... existence of precedent does not settle the question of whether a new language should inherit the patte...
(WHETHER)
[style] ~471-~471: This sentence construction might be considered wordy, consider shortening it.
Context: ...e short-term benefit justifies choosing a syntax that is otherwise acceptable. If the TS-aligned syntax were actively...
(A_VERY_ORDERED_SENTENCE)
🪛 markdownlint-cli2 (0.21.0)
[warning] 222-222: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 274-274: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 294-294: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 326-326: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 339-339: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 346-346: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 356-356: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 364-364: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 371-371: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 381-381: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 382-382: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above
(MD022, blanks-around-headings)
[warning] 386-386: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 421-421: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 460-460: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 468-468: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 478-478: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 487-487: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 501-501: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 525-525: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 550-550: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 565-565: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 583-583: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 607-607: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 620-620: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 644-644: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 654-654: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 667-667: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 676-676: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
| "sub" => (x: int) => x - 1 | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Add blank line before heading.
The code block ending at line 381 needs a blank line before the Section 4.5 heading at line 382, as flagged by the markdown linter.
📝 Proposed fix
"sub" => (x: int) => x - 1
}
+4.5 Pattern Matching
</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
"sub" => (x: int) => x - 1
}
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 381-381: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 382-382: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above
(MD022, blanks-around-headings)
| <tr> | ||
| <td><a href="./proposals/BEP-008-function-and-lambda-syntax-design/"><strong>BEP-008</strong>: Function and Lambda Syntax Design</a> <img src="https://img.shields.io/badge/Status-Draft-lightgrey" alt="Draft"><br><br><br><br><span style='font-size:0.8em; color:gray'>Shepherd(s): Language Design Team</span></td> | ||
| </tr> | ||
| <tr> | ||
| <td><a href="./proposals/BEP-009-function-and-lambda-syntax-design-ts-aligned/"><strong>BEP-009</strong>: Function and Lambda Syntax Design (TS-aligned)</a> <img src="https://img.shields.io/badge/Status-Proposed-yellow" alt="Proposed"><br><br><br><br><span style='font-size:0.8em; color:gray'>Shepherd(s): Language Design Team</span></td> | ||
| </tr> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Add descriptions for BEP-008 and BEP-009.
Both new BEP entries have empty descriptions (showing only <br><br> placeholders), while several other BEPs in the table include helpful summary text. For two competing proposals on function/lambda syntax, descriptions would help readers quickly understand the key differences:
- BEP-008: Could note "Proposes unified
->syntax for declarations, lambdas, and types" - BEP-009: Could note "Proposes TypeScript-aligned
:for declarations and=>for lambdas/types"
Since the table header indicates this is auto-generated (line 7: "Do not edit the table below by hand"), you may need to either:
- Add description metadata to the BEP README frontmatter or content
- Update the generation script to extract summaries
- Manually add descriptions if auto-generation isn't fully implemented yet


Summary
->syntax for all function contexts — declarations, lambdas, and types. Core property: the type of a function is its declaration with the name and body removed.:for return types in declarations,=>for lambdas and function types) with targeted fixes for known TS footguns.Both proposals agree on implicit return, two function forms, mandatory lambda parentheses,
throwsclauses, andmatchwith=>arms. The disagreement is narrow: which arrow symbol to use, and whether declarations and types should share the same notation.BEP-009 includes a detailed tradeoff analysis presenting arguments for both approaches, cross-language comparison tables, and open questions.
Test plan
->vs=>/:tradeoff🤖 Generated with Claude Code
Note
Low Risk
Documentation-only changes (new proposal texts and index update) with no runtime or tooling behavior changes.
Overview
Adds two new language-design BEPs: BEP-008 (Draft) documents a unified
->syntax for function declarations, lambdas, and function types (withthrowsclauses andmatchusing=>), and BEP-009 (Proposed) presents a TypeScript-aligned alternative using:for declaration return types and=>for lambdas/types with a detailed tradeoff analysis.Updates
beps/docs/README.mdto include BEP-008 and BEP-009 in the auto-generated BEP index.Written by Cursor Bugbot for commit 46fafe7. This will update automatically on new commits. Configure here.
Summary by CodeRabbit