Proposal: Positional Lambdas for Single-Parameter Functions
Status: Approved Approved: 2026-01-28 Author: Claude (with Eric) Created: 2026-01-28
Summary
When a function has exactly one parameter and the argument is a lambda, allow omitting the parameter name:
// Current (required)
items.map(transform: x -> x * 2)
items.filter(predicate: x -> x > 0)
// Proposed (allowed)
items.map(x -> x * 2)
items.filter(x -> x > 0)
Motivation
The Problem
Ori requires named arguments for clarity:
send_email(to: alice, subject: title, body: content) // Clear
But for higher-order functions taking a single lambda, the name adds ceremony without clarity:
items.map(transform: x -> x * 2) // "transform" obvious from context
items.filter(predicate: x -> x > 0) // "predicate" obvious from context
items.find(where: x -> x.id == id) // "where" obvious from context
tasks.any(predicate: t -> t.done) // "predicate" obvious from context
The lambda itself is visually distinct from regular values. When you see x -> x * 2, you know it’s a transformation. The parameter name is redundant.
Prior Art
Every mainstream language with lambdas allows this:
// JavaScript
items.map(x => x * 2)
// Python
list(map(lambda x: x * 2, items))
// Rust
items.iter().map(|x| x * 2)
// Swift
items.map { $0 * 2 }
// Kotlin
items.map { it * 2 }
// Haskell
map (*2) items
Ori is the only language requiring items.map(transform: x -> x * 2).
Why This Matters
Higher-order functions are idiomatic in Ori:
// Current: verbose
users
.filter(predicate: u -> u.active)
.map(transform: u -> u.name)
.find(where: n -> n.starts_with(prefix: "A"))
// Proposed: clean
users
.filter(u -> u.active)
.map(u -> u.name)
.find(n -> n.starts_with(prefix: "A"))
The named arguments add 40+ characters without improving clarity.
Design
The Rule
When ALL of the following are true:
- Function has exactly one explicit parameter (excluding
selffor methods) - The argument expression is a lambda
THEN: The parameter name may be omitted.
What Counts as a Lambda?
A lambda expression is:
x -> expr(single parameter)(a, b) -> expr(multiple parameters)() -> expr(no parameters)(x: int) -> int = expr(typed lambda)
A lambda expression is NOT:
- A variable holding a function:
let f = x -> x + 1; list.map(f)— named arg required - A function reference:
list.map(double)— named arg required - Any other expression type
Examples
Allowed (single param + lambda):
items.map(x -> x * 2)
items.filter(x -> x > 0)
items.find(x -> x.id == id)
items.fold(0, (acc, x) -> acc + x) // NOT allowed: 2 params
tasks.any(t -> t.done)
tasks.all(t -> t.valid)
Not allowed (not a lambda literal):
let double = x -> x * 2
items.map(double) // Error: named arg required
items.map(transform: double) // OK
@double (n: int) -> int = n * 2
items.map(double) // Error: named arg required
items.map(transform: double) // OK
Not allowed (multiple params):
items.fold(0, (acc, x) -> acc + x) // Error: 2 params
items.fold(initial: 0, op: (acc, x) -> acc + x) // OK
Named always works:
items.map(transform: x -> x * 2) // Still valid
items.filter(predicate: x -> x > 0) // Still valid
Edge Cases
Chained methods:
items
.filter(x -> x > 0) // OK: single param lambda
.map(x -> x * 2) // OK: single param lambda
.fold(initial: 0, op: (acc, x) -> acc + x) // Named required: 2 params
Nested lambdas:
matrix.map(row -> row.map(x -> x * 2)) // OK: both single param lambda
Lambda returning lambda:
@curry (f: (int, int) -> int) -> (int) -> (int) -> int
curry((a, b) -> a + b) // OK: single param is lambda `(a, b) -> a + b`
Rationale
Why Only Lambdas?
Lambdas are visually distinct. When you see:
items.map(x -> x * 2)
The x -> x * 2 syntax immediately signals “this is a transformation function.” No label needed.
But for regular values:
send(message) // What is message? A recipient? A channel?
send(to: message) // Ah, it's the destination
Variables don’t carry their purpose in their syntax. Named arguments provide that context.
Why Not Allow Function References?
@double (n: int) -> int = n * 2
items.map(double) // Why require name here?
Function references look like any other identifier. Without IDE support, you can’t tell if double is a function, a value, or a type. The named argument transform: double provides context.
But x -> x * 2 is unambiguous — it can only be a function.
Why Single Parameter Only?
Multiple parameters benefit from names:
items.fold(0, (acc, x) -> acc + x) // Which is initial? Which is op?
items.fold(initial: 0, op: (acc, x) -> acc + x) // Clear
Single parameter has no ambiguity:
items.map(x -> x * 2) // Only one thing it could be
Why Not Trailing Closure Syntax?
Some languages (Swift, Kotlin) have special “trailing closure” syntax:
// Swift
items.map { $0 * 2 }
This requires new syntax. The proposal reuses existing lambda syntax — just removes the label requirement in specific cases.
Comparison with Anonymous Parameters Proposal
The Anonymous Parameters proposal allows function authors to opt-in to positional arguments via _ name syntax:
@map (_ transform: (T) -> U) -> [U] // Explicit opt-in
This proposal is complementary — it’s automatic based on call-site syntax:
| Approach | Scope | Opt-in | Applies to |
|---|---|---|---|
| Anonymous params | Any function | Author declares _ | Any argument type |
| This proposal | Single-param functions | Automatic | Lambda literals only |
Both could coexist. This proposal handles the common HOF case automatically; anonymous params handle other cases explicitly.
Resolution order: When both features apply (single-param function with _ param and a lambda argument), this proposal’s automatic rule takes precedence — the lambda can be passed positionally regardless of whether the function declared the parameter as anonymous.
Implementation
Parser Changes
No grammar changes needed. The parser already accepts:
call_expression := expression "(" argument_list ")"
argument := identifier ":" expression | expression
The second form (positional) is currently rejected by the type checker for direct calls.
Type Checker Changes
In call resolution (compiler/ori_typeck/src/infer/call.rs):
When resolving a call with a positional argument:
1. If callee has exactly 1 parameter
2. AND argument is a LambdaExpr
3. THEN allow positional
4. ELSE require named argument (existing error E2011)
Error Messages
When the rule doesn’t apply:
error[E2011]: named arguments required for direct function calls
--> src/main.ori:5:12
|
5 | items.map(double)
| ^^^^^^
|
= help: use named argument syntax: `map(transform: double)`
= note: positional arguments are only allowed for inline lambda
expressions, not function references
Files to Update
compiler/ori_typeck/src/infer/call.rs— Add lambda-literal checkdocs/ori_lang/v2026/spec/09-expressions.md— Document exceptionCLAUDE.md— Update call syntax section
Examples
Before and After
Iterator chains:
// Before
users
.filter(predicate: u -> u.active && u.verified)
.map(transform: u -> u.email)
.filter(predicate: e -> e.ends_with(suffix: "@company.com"))
// After
users
.filter(u -> u.active && u.verified)
.map(u -> u.email)
.filter(e -> e.ends_with(suffix: "@company.com"))
Option/Result methods:
// Before
maybe_user
.map(transform: u -> u.profile)
.and_then(transform: p -> p.avatar)
.unwrap_or(default: $default_avatar)
// After
maybe_user
.map(u -> u.profile)
.and_then(p -> p.avatar)
.unwrap_or(default: $default_avatar)
Event handlers:
// Before
button.on_click(handler: () -> save_document())
input.on_change(handler: value -> update_state(value: value))
// After
button.on_click(() -> save_document())
input.on_change(value -> update_state(value: value))
Parallel operations:
// Before (parallel has multiple params, so names still required)
parallel(
tasks: [
() -> fetch_users(),
() -> fetch_posts(),
() -> fetch_comments(),
],
max_concurrent: 3,
)
// After (unchanged — multiple params)
parallel(
tasks: [
() -> fetch_users(),
() -> fetch_posts(),
() -> fetch_comments(),
],
max_concurrent: 3,
)
Tradeoffs
| Benefit | Cost |
|---|---|
| Cleaner HOF chains | Slightly less consistent (named args sometimes required) |
| Matches industry conventions | Special case in type checker |
| No new syntax needed | Lambda vs function ref distinction matters |
| Backward compatible | - |
When Names Are Still Valuable
Even with this feature, authors may prefer explicit names for documentation:
// Self-documenting
items.sort(by: (a, b) -> a.name.compare(other: b.name))
items.group(by: item -> item.category)
items.partition(where: item -> item.active)
Named arguments remain valid and are encouraged when they add clarity.
Summary
Allow omitting parameter names when:
- Function has exactly one parameter
- Argument is an inline lambda expression
This eliminates the most common source of verbosity (items.map(transform: x -> x * 2) → items.map(x -> x * 2)) while preserving named arguments everywhere they add value.
The feature is:
- Automatic — no author opt-in required
- Narrow — only lambdas, only single-param functions
- Backward compatible — named arguments always work
- Industry-aligned — matches JavaScript, Python, Rust, Swift, Kotlin, etc.