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:
If the lambda has no explicit function type, the return type is inferred from its body:
When no target function type is available, parameter types must be explicit.
Calling Lambdas¶
Function values are called directly with function-call syntax:
.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:
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¶
- Functions - Named functions and methods
- Classes and Objects - Object-oriented programming
- Examples - Functional programming examples