Skip to content

Lambda Expressions

Lambda expressions provide a concise way to create anonymous functions in Onion.

Lambda Syntax

Lambdas are written (parameters) -> body, where the body is either an expression or a block:

// Expression body
val double = (x: Int) -> x * 2

// Bare single parameter (type inferred from the expected function type)
val triple: Int -> Int = x -> x * 3

// Multiple parameters
val add = (a: Int, b: Int) -> a + b

// Block body for multiple statements
val greet: () -> String = () -> { println("Hello!"); return "done"; }

Type Inference

When the target function type is known, parameter types can be omitted:

val add: (Int, Int) -> Int = (x, y) -> { return x + y; }

If the lambda has no explicit function type, the return type is inferred from its body:

val upper = (s: String) -> { return s.toUpperCase(); }

When no target function type is available, parameter types must be explicit.

Calling Lambdas

Function values are called directly with function-call syntax:

val double = (x: Int) -> x * 2
println(double(21))   // 42

.call() also works:

val square: (Int) -> Int = (x: Int) -> { return x * x; }

val result: Int = square.call(5)  // 25
println(result)

Function Types

Lambdas can be typed using the arrow type syntax (A, B) -> R. For a single parameter, parentheses are optional (A -> R):

// Function with 0 parameters
val func0: () -> Int = () -> { return 42; }
val value: Int = func0.call()

// Function with 1 parameter
val func1: Int -> Int = (x: Int) -> { return x * 2; }
val doubled: Int = func1.call(10)

// Function with 2 parameters
val func2: (Int, Int) -> Int = (x: Int, y: Int) -> { return x + y; }
val sum: Int = func2.call(3, 7)

Void-Returning Functions

For side-effect-only lambdas, the return type can be written as void or Unit. At runtime these erase to Object, so the lambda body returns null:

def repeat(n: Int, block: () -> Unit): void {
  for var i: Int = 0; i < n; i = i + 1 {
    block.call()
  }
}

repeat(3, () -> { println("tick") })

Java Functional Interfaces (SAM Conversion)

Lambdas convert to any Java interface with a single abstract method:

val r: Runnable = () -> println("ran")
new Thread(r).start()

val cmp: Comparator[Integer] = (a, b) -> (b as Int) - (a as Int)
Collections::sort(xs, cmp)

// Argument position works too
Collections::sort(xs, (a, b) -> (a as Int) - (b as Int))

Primitive Type Arguments

Onion boxes primitive type arguments at the generic-interface level, so Comparator[Int] is represented as Comparator[Integer] internally. You can still write the lambda parameters with their primitive types:

import { java.util.Comparator }

val cmp: Comparator[Int] = (a: Int, b: Int) -> a - b
Collections::sort(xs, cmp)

// The same lambda can be written directly at argument position.
Collections::sort(xs, (a: Int, b: Int) -> a - b)

The compiler matches the primitive parameter types against the boxed interface signature and generates the necessary bridge method automatically. This also works for primitive return types:

import { java.util.function.Supplier }

val s: Supplier[Int] = () -> 42

Closures

Lambdas can capture variables from their enclosing scope:

Simple Closure

val multiplier: Int = 10
val multiply: (Int) -> Int = (x: Int) -> { return x * multiplier; }

println(multiply.call(5))  // 50

Mutable Closures

Closures can modify captured variables:

var count: Int = 0
val increment: () -> Int = () -> {
  count = count + 1
  return count;
}

println(increment.call())  // 1
println(increment.call())  // 2
println(increment.call())  // 3

Counter Factory

def makeCounter(): () -> Int {
  var count: Int = 0
  return () -> {
    count = count + 1
    return count;
  };
}

val counter1: () -> Int = makeCounter()
val counter2: () -> Int = makeCounter()

println(counter1.call())  // 1
println(counter1.call())  // 2
println(counter2.call())  // 1
println(counter1.call())  // 3

Higher-Order Functions

Functions that accept lambdas as parameters:

Filter Function

import {
  java.util.ArrayList;
  java.util.List;
}

def filter(items: List[String], predicate: (String) -> Boolean): List[String] {
  val result: ArrayList[String] = new ArrayList[String]()

  foreach item: String in items {
    if predicate.call(item) {
      result << item
    }
  }

  return result
}

val lines: List[String] = [
  "INFO: System started",
  "ERROR: Connection failed",
  "INFO: Processing data",
  "ERROR: Timeout"
]

val isError: (String) -> Boolean = (line: String) -> { return line.startsWith("ERROR"); }

val errors: List[String] = filter(lines, isError)
foreach error: String in errors {
  println(error)
}
// Output:
// ERROR: Connection failed
// ERROR: Timeout

Map Function

import {
  java.util.ArrayList;
  java.util.List;
}

def map(items: List[String], transform: (String) -> String): List[String] {
  val result: ArrayList[String] = new ArrayList[String]()

  foreach item: String in items {
    result << transform.call(item)
  }

  return result
}

val words: List[String] = ["hello", "world", "onion"]
val toUpper: (String) -> String = (s: String) -> { return s.toUpperCase(); }

val upper: List[String] = map(words, toUpper)
foreach word: String in upper {
  println(word)
}
// Output:
// HELLO
// WORLD
// ONION

Reduce Function

import { java.util.List; }

def reduce(items: List[Int], operation: (Int, Int) -> Int, initial: Int): Int {
  var accumulator: Int = initial

  foreach item: Int in items {
    accumulator = operation.call(accumulator, item)
  }

  return accumulator
}

val numbers: List[Int] = [1, 2, 3, 4, 5]
val sum: (Int, Int) -> Int = (acc: Int, n: Int) -> { return acc + n; }

val total: Int = reduce(numbers, sum, 0)
println(total)  // 15

Practical Examples

Line Filtering

import {
  java.io.BufferedReader;
  java.io.FileReader;
}

def filterFile(filename: String, predicate: (String) -> Boolean) {
  val reader: BufferedReader = new BufferedReader(
    new FileReader(filename)
  )

  var line: String = null
  while (line = reader.readLine()) != null {
    if predicate.call(line) {
      println(line)
    }
  }

  reader.close()
}

// Filter lines starting with ERROR
val errorFilter: (String) -> Boolean = (line: String) -> { return line.startsWith("ERROR"); }

filterFile("logfile.txt", errorFilter)

Custom Sort Comparator

import {
  java.util.ArrayList;
  java.util.Collections;
  java.util.Comparator;
}

class LambdaComparator <: Comparator[Object] {
  val compareFunc: (Object, Object) -> Int

  public:
    def this(func: (Object, Object) -> Int) {
      this.compareFunc = func
    }

    def compare(a: Object, b: Object): Int = this.compareFunc.call(a, b)
}

val list: ArrayList[String] = new ArrayList[String]()
list << "banana"
list << "apple"
list << "cherry"

val alphabetical: (Object, Object) -> Int = (a: Object, b: Object) -> {
  val s1: String = (a as String)
  val s2: String = (b as String)
  return s1.compareTo(s2);
}

val comparator: LambdaComparator = new LambdaComparator(alphabetical)
Collections::sort(list, comparator)

foreach item :Object in list {
  println((item as String))
}
// Output:
// apple
// banana
// cherry

Event Handlers

import {
  javax.swing.JButton;
  java.awt.event.ActionListener;
  java.awt.event.ActionEvent;
}

class LambdaActionListener <: ActionListener {
  val handler: (ActionEvent) -> Int

  public:
    def this(h: (ActionEvent) -> Int) {
      this.handler = h
    }

    def actionPerformed(event :ActionEvent) {
      this.handler.call(event)
    }
}

val button: JButton = new JButton("Click me")

val onClick: (ActionEvent) -> Int = (event: ActionEvent) -> {
  println("Button was clicked!")
  return 0
}

val listener: LambdaActionListener = new LambdaActionListener(onClick)
button.addActionListener(listener)

Lambda Best Practices

Keep Lambdas Short

// Good: Simple, focused lambda
val isEven: (Int) -> Boolean = (n: Int) -> { return n % 2 == 0; }

// Bad: Complex lambda (use named function instead)
val complex: (Int) -> Int = (n: Int) -> {
  val temp: Int = n * 2
  val result: Int = temp + 10
  if result > 100 {
    return result / 2;
  } else {
    return result * 3;
  }
}

Use Descriptive Variable Names

// Good
val filterErrors: (String) -> Boolean = (logLine: String) -> { return logLine.startsWith("ERROR"); }

// Bad
val f: (String) -> Boolean = (x: String) -> { return x.startsWith("ERROR"); }

Avoid Side Effects When Possible

// Good: Pure function
val double: (Int) -> Int = (x: Int) -> { return x * 2; }

// Less ideal: Side effect
var count: Int = 0
val incrementCounter: () -> Int = () -> {
  count = count + 1  // Modifies external state
  return count
}

Next Steps