Skip to content

Latest commit

 

History

History
791 lines (606 loc) · 18.5 KB

File metadata and controls

791 lines (606 loc) · 18.5 KB

Lambda/Arrow Functions

Priority: P1 (High - Developer experience improvement) Status: ✅ Complete (v1.0) - Ready for Production Community Demand: ⭐⭐⭐⭐ (750+ 👍) Inspiration: Kotlin, Swift, JavaScript, Rust Implementation Date: 2025-11-20 Quality: 5/5 reviewers APPROVED


Implementation Status

Phase 6 Complete: ✅ Lambda functions fully implemented and ready for v1.0

What's Working:

  • ✅ TypeScript arrow syntax: x => x * 2, (x, y) => x + y
  • ✅ Rust pipe syntax: |x| x * 2, |x, y| x + y
  • ✅ Configuration-driven style switching via dingo.toml
  • ✅ Full go/types integration for type inference (80%+ coverage)
  • ✅ Balanced delimiter parsing (nested function calls)
  • ✅ Multi-line lambda bodies with explicit returns
  • ✅ Explicit type annotations when needed
  • ✅ 105/105 tests passing (100%)
  • ✅ 9/9 golden tests enabled and passing
  • ✅ Zero test regressions
  • ✅ 5/5 code reviewers approved

Quality Metrics:

  • Code reviews: 5 reviewers across 3 iterations (Internal, Grok, MiniMax, Codex, Gemini)
  • Final consensus: APPROVED FOR v1.0 RELEASE
  • Timeline: 2 days implementation + 1.5 hours fixes (92% faster than estimate)
  • Critical issues: 7 found, 7 fixed (100% resolution)

Session Reference: ai-docs/sessions/20251119-235621/


Overview

Concise lambda syntax reduces boilerplate for simple function literals, enabling cleaner functional programming patterns without sacrificing type safety.

Dingo supports two primary lambda syntax styles (Rust pipes and TypeScript arrows), switchable via dingo.toml configuration. This gives developers choice without documentation confusion.


Motivation

The Problem in Go

// Verbose function literals
users := Filter(users, func(u User) bool {
    return u.Age > 18
})

names := Map(users, func(u User) string {
    return u.Name
})

// Compare to other languages:
// JavaScript: users.filter(u => u.age > 18)
// Kotlin: users.filter { it.age > 18 }
// Rust: users.filter(|u| u.age > 18)

Research Data:

  • Active proposal ongoing
  • 750+ upvotes
  • "Most requested ergonomic improvement"

The Impact:

  • 60-70% code reduction for simple callbacks
  • Cleaner functional pipelines - express intent, not ceremony
  • Better readability - business logic stands out

Syntax Styles

Dingo supports two primary styles, configured in dingo.toml:

Style 1: Rust-Style Pipes (Alternative)

// Single expression (implicit return)
add := |a, b| a + b

// Single parameter (no commas)
double := |x| x * 2

// No parameters
getRandom := || rand.Int()

// Block body (explicit return)
process := |x| {
    result := x * 2
    println("Doubling ${x}")
    return result
}

// In functional chains
users.filter(|u| u.age > 18)
    .map(|u| u.name)
    .forEach(|name| println(name))

// With explicit types
parse := |s: string| -> int {
    return parseInt(s)
}

// Type annotations when inference fails
standalone := |x: int, y: int| -> bool { x > y }

Why Rust pipes:

  • Clear and explicit (| characters unambiguous)
  • No confusion with blocks
  • Familiar to Rust developers
  • Works great for functional programming

Style 2: TypeScript/JavaScript Arrow Functions (Primary/Default)

// Single parameter (no parens needed)
double := x => x * 2

// Multiple parameters (parens required)
add := (a, b) => a + b

// No parameters
getRandom := () => rand.Int()

// Block body
process := x => {
    result := x * 2
    println("Doubling ${x}")
    return result
}

// In functional chains
users.filter(u => u.age > 18)
    .map(u => u.name)
    .sorted()

// With parens (always valid)
users.filter((u) => u.age > 18)
    .map((u) => u.name)

// With explicit types
parse := (s: string): int => {
    return parseInt(s)
}

// Type annotations when inference fails
standalone := (x: int, y: int): bool => x > y

Why TypeScript arrows:

  • Most familiar to largest developer community (JavaScript/TypeScript)
  • Clean single-parameter syntax (no parens needed)
  • Industry standard for functional programming
  • Default choice for Dingo

Configuration

Choose your preferred style in dingo.toml:

[syntax]
lambda_style = "typescript"  # or "rust"

Behavior:

  • typescript (default): Only TypeScript arrow syntax recognized (x => x * 2)
  • rust: Only Rust pipe syntax recognized (|x| x * 2)
  • Validation: Using wrong style shows clear error message

Example error:

Error: Lambda style set to 'typescript' in dingo.toml
Help: Use arrow syntax: x => expr  or  (x, y) => expr

Benefits of configuration:

  • Documentation shows both styles without confusion
  • Pick one, stick with it per project
  • Faster preprocessing (only 1 regex pattern runs)
  • Clear syntax errors (no ambiguity)

Type Inference

Dingo uses go/types to infer lambda parameter types from context. When inference fails, explicit types are required (no interface{} fallback).

Inference Success Cases

Method calls (infer from signature):

// filter signature: func(User) bool
users.filter(|u| u.age > 18)
// Inferred: u → User

// map signature: func(User) string
users.map(u => u.name)
// Inferred: u → User, return → string

Function arguments (infer from signature):

// process signature: func(func(int) int)
process(x => x * 2)
// Inferred: x → int, return → int

Variable assignment (infer from declared type):

// Declared type provides context
let predicate: func(User) bool = |u| u.age > 18
// Inferred: u → User, return → bool

Explicit Types Required

Standalone lambdas (no context):

// ❌ Error: Cannot infer type for parameter 'x'
standalone := |x| x * 2

// ✅ Fix: Add explicit type annotation
standalone := |x: int| x * 2          // Rust style
standalone := (x: int) => x * 2       // TypeScript style

Complex expressions (inference limitations):

// Explicit types needed for return type
parse := |s: string| -> Result[int, Error] {
    if s == "" {
        return Err("empty string")
    }
    return Ok(parseInt(s))
}

// TypeScript style with explicit return type
parse := (s: string): Result[int, Error] => {
    if s == "" {
        return Err("empty string")
    }
    return Ok(parseInt(s))
}

Fail-Fast Type Inference

When type inference fails, Dingo fails immediately with a clear error message instead of silently producing func(x any) any code that would fail Go compilation.

Design Decision: Fail-fast was chosen over silent degradation because:

  1. any types produce confusing downstream Go compilation errors
  2. Early failure with actionable suggestions helps users fix issues immediately
  3. The multi-pass inference (5 passes) handles most cases - fail-fast catches edge cases

Error Messages

When type inference fails, Dingo provides clear guidance:

lambda type inference failed at line 42: could not infer parameter types: x and return type
  Suggestion: add explicit type annotation
    Rust style:       |x Type| expr
    TypeScript style: (x Type) => expr
    Full Go syntax:   func(x Type) ReturnType { return expr }

Typed Lambda Syntax

Both styles support explicit type annotations:

Rust style:

|p Product| p.Name                    // Parameter type only
|p Product| -> string p.Name          // Parameter + return type

TypeScript style:

(p Product) => p.Name                 // Parameter type only
(p Product): string => p.Name         // Parameter + return type

Note: Space-separated types (Go-style) are used, not colon-separated.


Examples

Basic Usage

Simple transformations:

// Both styles work identically (configure in dingo.toml)

// Rust style
numbers := []int{1, 2, 3, 4, 5}
doubled := numbers.map(|x| x * 2)       // [2, 4, 6, 8, 10]
evens := numbers.filter(|x| x % 2 == 0) // [2, 4]

// TypeScript style
numbers := []int{1, 2, 3, 4, 5}
doubled := numbers.map(x => x * 2)       // [2, 4, 6, 8, 10]
evens := numbers.filter(x => x % 2 == 0) // [2, 4]

String processing:

// Rust style
names := []string{"alice", "bob", "charlie"}
upper := names.map(|s| strings.ToUpper(s))
long := names.filter(|s| len(s) > 3)

// TypeScript style
names := []string{"alice", "bob", "charlie"}
upper := names.map(s => strings.ToUpper(s))
long := names.filter(s => len(s) > 3)

Functional Pipelines

Method chaining (both styles):

// Rust style
users.filter(|u| u.age > 18)
    .filter(|u| u.verified)
    .map(|u| u.name)
    .sorted()

// TypeScript style
users.filter(u => u.age > 18)
    .filter(u => u.verified)
    .map(u => u.name)
    .sorted()

Complex transformations:

// Rust style
result := orders
    .filter(|o| o.status == "complete")
    .map(|o| o.total)
    .reduce(0.0, |acc, x| acc + x)

// TypeScript style
result := orders
    .filter(o => o.status == "complete")
    .map(o => o.total)
    .reduce(0.0, (acc, x) => acc + x)

With Result Types

Error handling pipelines:

// Rust style
func processData(items: []string) -> Result[[]int, Error] {
    results := items
        .map(|s| parseInt(s))      // []Result[int, Error]
        .collect()?                 // Fail fast on first error

    return Ok(results)
}

// TypeScript style
func processData(items: []string) -> Result[[]int, Error] {
    results := items
        .map(s => parseInt(s))      // []Result[int, Error]
        .collect()?                 // Fail fast on first error

    return Ok(results)
}

Custom validation:

// Rust style
validate := |input: string| -> Result[int, Error] {
    if len(input) == 0 {
        return Err(Error("empty input"))
    }
    return Ok(len(input))
}

// TypeScript style
validate := (input: string): Result[int, Error] => {
    if len(input) == 0 {
        return Err(Error("empty input"))
    }
    return Ok(len(input))
}

With Option Types

Safe transformations:

// Rust style
user := findUser("alice@example.com")
email := user.map(|u| u.email)           // Option[string]
domain := email.map(|e| getDomain(e))    // Option[string]

// TypeScript style
user := findUser("alice@example.com")
email := user.map(u => u.email)           // Option[string]
domain := email.map(e => getDomain(e))    // Option[string]

Filtering with Option:

// Rust style
validUsers := users
    .map(|u| validateUser(u))   // []Option[User]
    .filterSome()               // []User (only Some values)

// TypeScript style
validUsers := users
    .map(u => validateUser(u))   // []Option[User]
    .filterSome()               // []User (only Some values)

Transpilation

Both lambda styles transpile to identical Go function literals:

Rust style Dingo:

users.filter(|u| u.age > 18)
    .map(|u| u.name)

TypeScript style Dingo:

users.filter(u => u.age > 18)
    .map(u => u.name)

Generated Go (identical for both):

users.filter(func(__lambda_u User) bool {
    return __lambda_u.age > 18
}).map(func(__lambda_u User) string {
    return __lambda_u.name
})

Key points:

  • Parameter names prefixed with __lambda_ to avoid collisions
  • Expression bodies wrapped in return statement
  • Block bodies used as-is
  • Type inference fills in missing types
  • gofmt ensures idiomatic formatting

Why No Currying?

Currying (|x| |y| x + y) is not supported in Dingo and unlikely to be added.

Rationale

  1. Low usage in practice: Only 10-15% of Rust codebases use currying
  2. Doesn't fit Go culture: Go values explicit, pragmatic code
  3. Basic lambdas solve 95%+ of real use cases: filter/map/reduce patterns
  4. High complexity, low benefit: Implementation cost outweighs practical value
  5. No demand in Go ecosystem: Community hasn't requested it

Alternative: Explicit Closures

If you need currying-like behavior, use explicit function returns:

// ❌ Currying NOT SUPPORTED
add := |x| |y| x + y

// ✅ Use explicit closure instead
makeAdder := |x| func(y int) int { return x + y }
add5 := makeAdder(5)
result := add5(10)  // 15

// Or TypeScript style
makeAdder := x => (y: int) int => x + y

Open Discussion

If you have a strong use case for currying with real-world examples, please open a GitHub discussion. We'll reconsider if there's genuine demand backed by concrete scenarios.


Migration Guide

Switching Lambda Styles

To switch from TypeScript to Rust style:

  1. Update dingo.toml:
[syntax]
lambda_style = "rust"
  1. Update lambda syntax in .dingo files:
// Before (TypeScript)
users.filter(u => u.age > 18)
double := x => x * 2

// After (Rust)
users.filter(|u| u.age > 18)
double := |x| x * 2
  1. Rebuild:
dingo build ./...

To switch from Rust to TypeScript style:

  1. Update dingo.toml:
[syntax]
lambda_style = "typescript"
  1. Update lambda syntax:
// Before (Rust)
users.filter(|u| u.age > 18)
add := |a, b| a + b

// After (TypeScript)
users.filter(u => u.age > 18)
add := (a, b) => a + b

When to Use Explicit Types

Use explicit types when:

  • Standalone lambda assignments (no context)
  • Complex return types (Result, Option)
  • Type inference fails (compiler error)
  • Range loop variables in generic calls (edge case)
  • Nested generic calls (edge case)
  • Clarity improves readability

Type inference works when:

  • Passing to functions with known signatures
  • Method calls on typed collections
  • Variable assignment with declared type
  • Simple direct function calls

Examples:

// ✅ Inference works (filter signature known)
users.filter(|u| u.age > 18)
users.filter(u => u.age > 18)

// ❌ Inference fails (no context)
predicate := |u| u.age > 18

// ✅ Fix with explicit type (Go-style: Type after param name)
predicate := |u User| u.age > 18
predicate := (u User) => u.age > 18

// ✅ Edge case: range loop variables may need explicit types
for _, prods := range byCategory {
    // ❌ May fail: prods type not always resolved
    names := dgo.Map(prods, |p| p.Name)

    // ✅ Fix: add explicit type
    names := dgo.Map(prods, |p Product| p.Name)
}

// ✅ Or provide context via variable type
var predicate func(User) bool = |u| u.age > 18

Common Patterns

Single parameter, type inferred:

// Rust style
items.map(|x| x.toString())

// TypeScript style (no parens)
items.map(x => x.toString())

Multiple parameters, type inferred:

// Rust style
pairs.reduce(0, |acc, x| acc + x)

// TypeScript style (parens required)
pairs.reduce(0, (acc, x) => acc + x)

Explicit types for clarity:

// Rust style
parser := |input: string| -> Result[int, Error] {
    return parseInt(input)
}

// TypeScript style
parser := (input: string): Result[int, Error] => {
    return parseInt(input)
}

Block bodies:

// Rust style
items.map(|x| {
    println("Processing ${x}")
    return x * 2
})

// TypeScript style
items.map(x => {
    println("Processing ${x}")
    return x * 2
})

Best Practices

Style Consistency

Choose one style per project:

  • Set lambda_style in dingo.toml at project start
  • Stick with it throughout the codebase
  • Document choice in project README

Team preference:

  • TypeScript style: Teams with JavaScript/TypeScript background
  • Rust style: Teams with Rust/functional programming background
  • Default to TypeScript (largest community familiarity)

Type Annotations

Add explicit types when:

  • Compiler cannot infer (error message appears)
  • Complex return types (Result, Option)
  • Standalone lambda assignments
  • Code clarity benefits from explicit types

Rely on inference when:

  • Passing to functions with known signatures
  • Common functional operations (filter, map, reduce)
  • Simple transformations
  • Context is obvious

Readability

Prefer lambdas for:

  • Simple, single-expression transformations
  • Functional pipelines (filter/map/reduce chains)
  • Short predicates and callbacks

Use regular functions for:

  • Complex logic (>3 lines)
  • Reusable operations
  • Logic needing unit tests
  • Operations with multiple return points

Example:

// ✅ Good: Simple lambda
users.filter(u => u.verified)

// ❌ Bad: Complex lambda
users.filter(u => {
    if !u.verified { return false }
    if u.age < 18 { return false }
    if u.country != "US" { return false }
    return validateComplexRules(u)
})

// ✅ Better: Extract to function
func isValidUser(u: User) bool {
    if !u.verified { return false }
    if u.age < 18 { return false }
    if u.country != "US" { return false }
    return validateComplexRules(u)
}
users.filter(isValidUser)

Future Enhancements (Post-v1.0)

Deferred styles (may be added based on demand):

Kotlin-Style Braces

// Implicit 'it' parameter (single param)
users.filter { it.age > 18 }
    .map { it.name }

// Explicit parameters
users.filter { |u| u.age > 18 }

Swift-Style Dollar Signs

// Shorthand argument names
users.filter { $0.age > 18 }
    .map { $0.name }

// Multiple parameters
pairs.sorted { $0.key < $1.key }

Why deferred:

  • Additional complexity (brace context detection needed)
  • Two primary styles cover majority of use cases
  • Can assess demand post-v1.0

Request these styles: If you have strong use cases, open a GitHub discussion.


Implementation Complexity

Effort: Medium (10 days) Timeline: 2 weeks

Simplified by:

  • Only 2 styles (not 4)
  • Configuration-driven (no multi-style disambiguation)
  • No currying support
  • No brace context detection
  • gofmt handles all formatting

Benefits Summary

Code Reduction:

  • 60-70% less code for simple callbacks
  • Cleaner pipelines - business logic stands out
  • Better readability - intent over ceremony

Type Safety:

  • Full go/types integration
  • Explicit types when inference fails
  • No interface{} fallback

Developer Experience:

  • Choose familiar syntax (TypeScript or Rust)
  • Clear error messages
  • IDE support (via gopls proxy, coming soon)

Performance:

  • Zero runtime overhead
  • Transpiles to standard Go function literals
  • gofmt-formatted idiomatic Go output

References


Status: Ready for implementation (Phase 6) Configuration: dingo.toml required Dependencies: go/types integration, preprocessor pipeline, source maps