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.call(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.call("First")) // "First (line 1)"
println(filter.call("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.call(value)
return f.call(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: - Only direct self-recursion is optimized (not mutual recursion) - The recursive call must be in return position (no operations after the call)
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.call(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.call(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