Skip to content

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

def add(a: Int, b: Int): Int = a + b

val sum: Int = add(5, 3)
println(sum)  // 8

Function with No Parameters

def getCurrentTime: String = System::currentTimeMillis() as String

println(getCurrentTime())

Function with No Return Value

Functions that don't return a value implicitly return nothing:

def printMessage(msg :String) {
  println("Message: " + msg)
}

printMessage("Hello")

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:

def max(a :Int, b :Int) :Int {
  if a > b {
    return a
  }
  return b
}

Expression Body

For concise functions, prefer an expression body using =:

def square(x: Int): Int = x * x

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):

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

val result: Int = double(5)  // 10

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

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

When no explicit function type is provided, the return type is inferred:

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

Lambda with Multiple Parameters

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

val sum: Int = add(3, 7)  // 10

Lambda with No Parameters

val sayHello: () -> String = () -> { println("Hello!"); return "done"; }

sayHello()

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