Functions¶
Functions in Onion allow you to encapsulate reusable code. Onion supports both named functions (methods) and anonymous functions (lambdas).
Function Definitions¶
Basic Function¶
Define functions with the def keyword:
def greet(name: String): String = "Hello, " + name + "!"
val message: String = greet("Alice")
println(message) // "Hello, Alice!"
Function with Multiple Parameters¶
Function with No Parameters¶
Function with No Return Value¶
Functions that don't return a value implicitly return nothing:
Default Parameter Values and Named Arguments¶
Parameters can declare defaults; call sites may omit them or pass arguments by name (in any order). Both work for methods and constructors:
def greet(name: String, greeting: String = "Hello"): String {
return greeting + ", " + name
}
greet("kota") // Hello, kota
greet("kota", "Yo") // Yo, kota
greet(greeting = "Hi", name = "kota")
Varargs¶
A trailing Type... parameter collects extra arguments into an array;
passing an array directly also works:
def join(parts: String...): String {
var r = ""
foreach p: String in parts { r = r + p }
return r
}
join("a", "b", "c") // "abc"
join(existingArray) // array passes through
Extension Methods¶
extension blocks add methods to existing types — including Java
classes — resolved statically at the call site:
extension String {
def shout(): String { return this.toUpperCase() + "!" }
}
println("hello".shout()) // HELLO!
Return Statements¶
Explicit Return¶
Use return to exit a function early:
Expression Body¶
For concise functions, prefer an expression body using =:
Lambda Expressions¶
Lambda Syntax¶
Anonymous functions use the (params) -> { body } syntax. Function values can be invoked with f(args), which is shorthand for f(args):
If the target function type is known, parameter types can be omitted:
When no explicit function type is provided, the return type is inferred:
Lambda with Multiple Parameters¶
Lambda with No Parameters¶
Closures¶
Lambdas can capture variables from their enclosing scope:
def makeCounter(): () -> Int {
var count: Int = 0
return () -> {
count = count + 1
return count;
};
}
val counter: () -> Int = makeCounter()
println(counter()) // 1
println(counter()) // 2
println(counter()) // 3
Capturing Loop Variables¶
var i: Int = 0
val filter: (String) -> String = (line: String) -> {
i = i + 1
return line + " (line " + i + ")";
}
println(filter("First")) // "First (line 1)"
println(filter("Second")) // "Second (line 2)"
Function Types¶
You can either use the Function0 through Function10 interfaces, or the arrow type syntax (A, B) -> R (for a single parameter, parentheses are optional: A -> R):
// Function with 1 parameter
val func1: Int -> Int = (x: Int) -> { return x * 2; }
// Function with 2 parameters
val func2: (Int, Int) -> Int = (x: Int, y: Int) -> { return x + y; }
// Function with no parameters
val func0: () -> Int = () -> { return 42; }
The number indicates the parameter count:
- Function0 - No parameters
- Function1 - One parameter
- Function2 - Two parameters
- ... up to Function10 - Ten parameters
Higher-Order Functions¶
Functions that accept or return other functions:
def applyTwice(f: (Int) -> Int, value: Int): Int {
val temp: Int = f(value)
return f(temp)
}
val increment: (Int) -> Int = (x: Int) -> { return x + 1; }
val result: Int = applyTwice(increment, 5) // 7
Recursive Functions¶
Functions can call themselves:
def factorial(n :Int) :Int {
if n <= 1 {
return 1
}
return n * factorial(n - 1)
}
println(factorial(5)) // 120
Tail Recursion¶
Tail Call Optimization: Onion automatically optimizes tail-recursive functions by converting them to loops. This prevents stack overflow for deep recursion.
When compiling with --verbose, the compiler will log which methods are being optimized for tail recursion.
Tail-recursive functions can handle large recursion depths without stack overflow:
// Tail-recursive factorial with accumulator
def factorialTail(n: Int, acc: Int): Int {
if (n <= 1) {
return acc
}
return factorialTail(n - 1, n * acc) // Tail call - optimized to loop
}
def factorial(n: Int): Int {
return factorialTail(n, 1)
}
println(factorial(5)) // 120
println(factorial(1000)) // Works without stack overflow!
What is tail recursion? A recursive call is in "tail position" when it's the last operation before returning. The compiler automatically detects this pattern and converts the recursion to an efficient loop.
Benefits:
- Prevents StackOverflowError for deep recursion
- Constant stack space usage
- Performance equivalent to loops
Limitations: - Direct self-recursion is optimized automatically; mutual recursion (two or more methods calling each other in tail position) is not detected automatically - The recursive call must be in return position (no operations after the call)
Mutual Recursion¶
Two methods that tail-call each other aren't picked up by the automatic detection above — each call targets a different method, so it never looks like "a method calling itself". Mark every method in the group @TailRecursive to opt in; the compiler merges them into a single state-machine method instead of a loop:
class Parity {
private:
@TailRecursive
def isEven(n: Int): Boolean {
if n == 0 { return true }
return isOdd(n - 1)
}
@TailRecursive
def isOdd(n: Int): Boolean {
if n == 0 { return false }
return isEven(n - 1)
}
public:
def check(n: Int): Boolean = isEven(n)
}
println(new Parity().check(100000)) // true, no stack overflow
The group is only optimized when every member is private, shares the same return type and parameter types, and every tail call inside the group targets another member of the group. An annotated group that fails these requirements is not an error — the annotation is simply ineffective, and the compiler emits warning W0016 to flag it at compile time instead of an eventual StackOverflowError. See Tail Call Optimization for the full requirements.
Method Overloading¶
Classes can have multiple methods with the same name but different parameter types:
class Calculator {
public:
def add(a: Int, b: Int): Int = a + b
def add(a: Double, b: Double): Double = a + b
def add(a: String, b: String): String = a + b
}
val calc: Calculator = new Calculator
println(calc.add(5, 3)) // 8
println(calc.add(2.5, 3.7)) // 6.2
println(calc.add("Hello", "!")) // "Hello!"
Static Methods¶
Methods can be static (class-level) rather than instance-level:
class MathUtils {
public:
static def square(x: Int): Int = x * x
static def cube(x: Int): Int = x * x * x
}
// Call static methods with ::
println(MathUtils::square(5)) // 25
println(MathUtils::cube(3)) // 27
Function Examples¶
Filter Function¶
import {
java.util.ArrayList;
java.util.List;
}
def filterLines(lines: List[String], predicate: (String) -> Boolean): List[String] {
val result: ArrayList[String] = new ArrayList[String]()
foreach line: String in lines {
if predicate(line) {
result << line
}
}
return result
}
val startsWithError: (String) -> Boolean = (line: String) -> { return line.startsWith("ERROR"); }
val lines: List[String] = ["INFO: OK", "ERROR: Failed", "ERROR: Timeout"]
val errors: List[String] = filterLines(lines, startsWithError)
Map Function¶
import {
java.util.ArrayList;
java.util.List;
}
def mapLines(lines: List[String], transform: (String) -> String): List[String] {
val result: ArrayList[String] = new ArrayList[String]()
foreach line: String in lines {
result << transform(line)
}
return result
}
val toUpper: (String) -> String = (s: String) -> { return s.toUpperCase(); }
val lines: List[String] = ["hello", "world"]
val upper: List[String] = mapLines(lines, toUpper)
Best Practices¶
Single Responsibility¶
Each function should do one thing well:
// Good: Each function has a single purpose
def readFile(path :String) :String { ... }
def parseData(content :String) :Data { ... }
def validateData(data :Data) :Boolean { ... }
// Bad: Function does too much
def processFile(path :String) :Boolean {
// Reads, parses, validates, and saves
...
}
Descriptive Names¶
Use clear, descriptive function names:
// Good
def calculateTotalPrice(items :Item[]) :Double { ... }
def isValidEmail(email :String) :Boolean { ... }
// Bad
def calc(arr :Item[]) :Double { ... }
def check(s :String) :Boolean { ... }
Keep Functions Short¶
Aim for functions that fit on one screen:
def processOrder(order :Order) :Boolean {
if !validateOrder(order) {
return false
}
if !chargePayment(order) {
return false
}
if !shipOrder(order) {
return false
}
true
}
Next Steps¶
- Classes and Objects - Methods in classes
- Lambda Expressions - Deep dive into lambdas
- Examples - Functional programming examples