Functional Programming Examples¶
Examples demonstrating functional programming features in Onion.
Lambda Expressions¶
Basic lambda syntax:
// Simple lambda
val double: (Int) -> Int = (x: Int) -> { return x * 2; }
println(double.call(5)) // 10
// Lambda with multiple parameters
val add: (Int, Int) -> Int = (x: Int, y: Int) -> { return x + y; }
println(add.call(3, 7)) // 10
// Lambda with no parameters
val greet: () -> String = () -> { println("Hello!"); return "done"; }
greet.call()
Closures¶
Lambdas that capture variables:
File: LineFilter.on
import {
java.io.BufferedReader;
java.io.FileReader;
java.io.StringReader;
}
var i: Int = 0
val filter: (String) -> String = (line: String) -> {
i = i + 1
return line + " (" + i + ")";
}
val reader: BufferedReader = new BufferedReader(
new StringReader("First\nSecond\nThird")
)
var line: String = null
while (line = reader.readLine()) != null {
println(filter.call(line))
}
Output:
Topics:
- Closure capturing variable i
- Lambda returning modified string
- Post-increment in closure
Counter Factory¶
Creating multiple independent counters:
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
println(counter2.call()) // 2
Each counter maintains its own count variable.
Recursion¶
File: Factorial.on
import { java.lang.NumberFormatException; }
def fact(n :Int) :Int {
if n < 0 {
println("Illegal argument")
return 0
}
if n == 0 {
return 1
}
return n * fact(n - 1)
}
var line: String = null
while (line = IO::readln("Enter number: ")) != null {
try {
val value: Int = JInteger::parseInt(line)
val result: Int = fact(value)
println("Factorial: " + result)
} catch e :NumberFormatException {
println("Invalid number")
}
}
Topics: - Recursive function calls - Base case handling - Try-catch for error handling
File Line Counter with Recursion¶
File: LineCounter.on (simplified)
import {
java.io.File;
java.io.FileReader;
java.io.BufferedReader;
}
def countLines(file :File) :Int {
if file == null {
return 0
}
if file.isDirectory() {
var total: Int = 0
val files: File[] = file.listFiles()
if files != null {
foreach f :File in files {
total = total + countLines(f)
}
}
return total
}
if !file.name.endsWith(".on") {
return 0
}
val reader: BufferedReader = new BufferedReader(
new FileReader(file)
)
var count: Int = 0
var line: String = null
while (line = reader.readLine()) != null {
count = count + 1
}
reader.close()
println(file.name + ": " + count)
return count
}
// Usage
val dir: File = new File("src")
val total: Int = countLines(dir)
println("Total lines: " + total)
Topics: - Recursive directory traversal - File operations - Conditional logic
Filter Function¶
Higher-order function that filters a list:
import {
java.util.ArrayList;
java.util.List;
}
def filter(items: List[String], predicate: (String) -> Boolean): ArrayList[String] {
val result: ArrayList[String] = new ArrayList[String]
foreach item: String in items {
if predicate.call(item) {
result << item
}
}
return result
}
// Usage
val logs: List[String] = [
"INFO: Started",
"ERROR: Failed",
"INFO: Processing",
"ERROR: Timeout",
"INFO: Complete"
]
val isError: (String) -> Boolean = (line: String) -> { return line.startsWith("ERROR"); }
val errors: ArrayList[String] = filter(logs, isError)
foreach error: String in errors {
println(error)
}
Output:
Map Function¶
Transform each element in a collection:
import { java.util.ArrayList; }
def map(items: java.util.List[String], transform: (String) -> String): ArrayList[String] {
val result: ArrayList[String] = new ArrayList[String]
foreach item: String in items {
result << transform.call(item)
}
return result
}
// Usage
val words: java.util.List[String] = ["hello", "world", "onion"]
val toUpper: (String) -> String = (s: String) -> { return s.toUpperCase(); }
val upper: ArrayList[String] = map(words, toUpper)
foreach word: String in upper {
println(word)
}
Output:
Reduce Function¶
Accumulate values:
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
}
// Sum
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("Sum: " + total) // 15
// Product
val product: (Int, Int) -> Int = (acc: Int, n: Int) -> { return acc * n; }
val result: Int = reduce(numbers, product, 1)
println("Product: " + result) // 120
Composing Functions¶
Combine multiple operations:
def compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int {
return (x: Int) -> { return f.call(g.call(x)); }
}
// Define functions
val addTen: (Int) -> Int = (x: Int) -> { return x + 10; }
val double: (Int) -> Int = (x: Int) -> { return x * 2; }
// Compose: double then add 10
val composed: (Int) -> Int = compose(addTen, double)
println(composed.call(5)) // (5 * 2) + 10 = 20
Currying¶
Transform multi-parameter functions:
def add(x: Int): (Int) -> Int = (y: Int) -> { return x + y; }
val add5: (Int) -> Int = add(5)
val add10: (Int) -> Int = add(10)
println(add5.call(3)) // 8
println(add10.call(3)) // 13
Practical: Log Analyzer¶
Complete example combining functional concepts:
import {
java.io.BufferedReader;
java.io.FileReader;
java.util.ArrayList;
}
def analyzeLog(filename :String): void {
val reader: BufferedReader = new BufferedReader(
new FileReader(filename)
)
var errorCount: Int = 0
var warningCount: Int = 0
var infoCount: Int = 0
val isError: (String) -> Boolean = (line: String) -> { return line.startsWith("ERROR"); }
val isWarning: (String) -> Boolean = (line: String) -> { return line.startsWith("WARNING"); }
val isInfo: (String) -> Boolean = (line: String) -> { return line.startsWith("INFO"); }
var line: String = null
while (line = reader.readLine()) != null {
if isError.call(line) {
errorCount = errorCount + 1
} else if isWarning.call(line) {
warningCount = warningCount + 1
} else if isInfo.call(line) {
infoCount = infoCount + 1
}
}
reader.close()
println("Errors: " + errorCount)
println("Warnings: " + warningCount)
println("Info: " + infoCount)
}
analyzeLog("app.log")
Do Notation¶
Chain monadic operations with clean syntax.
Option Chaining¶
def parseNumber(s: String): Option[Int] {
try {
return Option::some(JInteger::parseInt(s));
} catch e: NumberFormatException {
return Option::none();
}
}
// Without do notation
val result1: Option[Int] = parseNumber("10").flatMap((x: Int) -> {
return parseNumber("20").map((y: Int) -> {
return x + y;
});
})
// With do notation - much cleaner!
val result2: Option[Int] = do[Option] {
x <- parseNumber("10")
y <- parseNumber("20")
ret x + y
}
println(result2.getOrElse(0)) // 30
Result Error Handling¶
def divide(a: Int, b: Int): Result[Int, String] {
if b == 0 {
return Result::err("Division by zero");
}
return Result::ok(a / b);
}
val calculation: Result[Int, String] = do[Result] {
x <- divide(100, 1) // 100
y <- divide(x, 5) // 20
z <- divide(y, 4) // 5
ret z * 2 // 10
}
if calculation.isOk() {
println("Result: " + calculation.get())
} else {
println("Error: " + calculation.getError())
}
Nested Do Blocks¶
val nested: Option[Int] = do[Option] {
x <- Option::some(10)
inner <- do[Option] {
y <- Option::some(20)
ret y * 2
}
ret x + inner // 10 + 40 = 50
}
Asynchronous Programming with Future¶
Creating Futures¶
// Already completed Future
val immediate: Future[Int] = Future::successful(42)
// Async computation
val async: Future[String] = Future::async(() -> {
Thread::sleep(1000L);
return "Hello after 1 second";
})
// From throwing operation
val risky: Future[Int] = Future::asyncThrowing(() -> {
if Math::random() < 0.5 {
throw new RuntimeException("Bad luck!");
}
return 100;
})
Transforming Futures¶
val numbers: Future[Int] = Future::successful(10)
// Map: transform the value
val doubled: Future[Int] = numbers.map((x: Int) -> { return x * 2; })
// FlatMap: chain async operations
val chained: Future[String] = numbers.flatMap((x: Int) -> {
return Future::async(() -> {
return "Number is: " + x;
});
})
// Filter: fail if predicate not met
val positive: Future[Int] = numbers.filter((x: Int) -> { return x > 0; })
Error Handling¶
val failing: Future[Int] = Future::failed(new RuntimeException("Oops!"))
// Recover with a default value
val recovered: Future[Int] = failing.recover((error: Throwable) -> {
println("Error: " + error.getMessage());
return 0;
})
// Recover with another Future
val retried: Future[Int] = failing.recoverWith((error: Throwable) -> {
return Future::successful(42);
})
Combining Futures¶
val f1: Future[Int] = Future::async(() -> { Thread::sleep(100L); return 1; })
val f2: Future[Int] = Future::async(() -> { Thread::sleep(200L); return 2; })
// Wait for all
val all: Future[Object[]] = Future::all(f1, f2)
all.onSuccess((results: Object[]) -> {
println("Results: " + results[0] + ", " + results[1]);
})
// Race: first to complete wins
val race: Future[Int] = f1.race(f2)
race.onSuccess((winner: Int) -> {
println("Winner: " + winner); // Usually f1
})
// Zip two futures
val zipped: Future[Object[]] = f1.zip(f2)
Callbacks¶
val future: Future[String] = Future::async(() -> {
return "Async result";
})
future
.onSuccess((value: String) -> { println("Success: " + value); })
.onFailure((error: Throwable) -> { println("Failed: " + error); })
Blocking (Use Sparingly)¶
val future: Future[Int] = Future::async(() -> { return 42; })
// Block until complete
val result: Int = future.await()
// Block with timeout (in milliseconds)
val timed: Int = future.awaitTimeout(5000L)
// Get with default on failure
val safe: Int = future.getOrElse(0)
Future with Do Notation¶
def fetchUser(id: Int): Future[String] {
return Future::async(() -> { return "User" + id; });
}
def fetchProfile(name: String): Future[String] {
return Future::async(() -> { return name + "'s profile"; });
}
val profile: Future[String] = do[Future] {
user <- fetchUser(42)
profile <- fetchProfile(user)
ret profile.toUpperCase()
}
profile.onSuccess((p: String) -> { println(p); })
// Prints: USER42'S PROFILE
Practical: Parallel API Calls¶
def fetchFromApi(url: String): Future[String] {
return Future::async(() -> {
// Simulate network request
Thread::sleep((Math::random() * 1000L) as Long);
return "Data from " + url;
});
}
// Fire off multiple requests in parallel
val api1: Future[String] = fetchFromApi("/users")
val api2: Future[String] = fetchFromApi("/posts")
val api3: Future[String] = fetchFromApi("/comments")
// Wait for all to complete
Future::all(api1, api2, api3).onSuccess((results: Object[]) -> {
println("Users: " + results[0])
println("Posts: " + results[1])
println("Comments: " + results[2])
})
// Or use do notation with zip
val combined: Future[String] = do[Future] {
pair1 <- api1.zip(api2)
data3 <- api3
ret pair1[0] as String + " | " + pair1[1] as String + " | " + data3
}
Trailing Lambda Syntax¶
Kotlin-style trailing lambda for cleaner method calls:
// Traditional syntax
list.map((x: Int) -> { return x * 2; })
// Trailing lambda - more concise
list.map { x => x * 2 }
// Works with any method taking a function last
future.onSuccess { result =>
println("Got: " + result)
}
// With other arguments before the trailing lambda
api.request("GET", "/users") { response =>
println(response.body())
}
Primitive Generics with Java Functional Interfaces¶
Onion can convert lambdas to Java functional interfaces even when the type
arguments are primitive. The compiler boxes the type parameter internally and
generates the bridge method, so you can write Int parameters naturally:
import {
java.util.Comparator
java.util.function.Predicate
}
val numbers = Colls::mutableListOf(3, 1, 4, 1, 5, 9, 2, 6)
// Comparator[Int] with primitive Int parameters
Collections::sort(numbers, (a: Int, b: Int) -> a - b)
println(numbers)
// Predicate[Int] with a primitive parameter
val isEven: Predicate[Int] = (n: Int) -> n % 2 == 0
numbers.removeIf(isEven)
println(numbers)
This also works for Supplier[Int], Function[Int, Int], and other
single-abstract-method interfaces.
Next Steps¶
- Lambda Expressions Guide - Detailed lambda documentation
- Functions Guide - Function definitions
- Standard Library - Option, Result, Future reference
- Basic Examples - Simpler programs