Functions & Closures

Functions in BioLang are first-class values. They can be assigned to variables, passed as arguments, and returned from other functions. BioLang supports named function declarations, anonymous lambdas, and closures that capture their environment.

Function Declarations

Functions are declared with the fn keyword. The return value is the last expression in the body — no explicit return needed:

# Simple function
fn greet(name) {
  println(f"Hello, {name}!")
}
greet("BRCA1")   # Hello, BRCA1!

# The last expression is the return value
fn add(a, b) {
  a + b
}
println(add(3, 4))   # 7

# Multi-statement function
fn describe_seq(seq) {
  let gc = gc_content(seq)
  let size = len(seq)
  f"Length={size}, GC={gc}"
}
println(describe_seq(dna"ATCGATCG"))

Implicit Return

The last expression in a function body is its return value. No return keyword is needed unless you want an early exit:

fn square(x) { x * x }
fn cube(x) { x * x * x }

println(square(5))   # 25
println(cube(3))     # 27

fn average(values) {
  sum(values) / len(values)
}
println(average([10, 20, 30]))   # 20

Lambda Expressions

Lambdas are anonymous functions written with pipe-delimited parameters. They are used extensively with higher-order functions like map, filter, and reduce:

# Single-parameter lambda
let double = |x| x * 2
println(double(21))   # 42

# Multi-parameter lambda
let add = |a, b| a + b
println(add(3, 4))    # 7

# Lambdas in pipe chains
let results = [10, 25, 50, 75, 90]
  |> map(|s| s / 100.0)
  |> filter(|s| s >= 0.5)
println(results)   # [0.5, 0.75, 0.9]

Multi-line Lambdas

For lambdas that need more than one expression, use a block body with braces:

let classify = |score| {
  if score >= 90 { "excellent" }
  else if score >= 70 { "good" }
  else { "needs work" }
}

println(classify(95))   # excellent
println(classify(75))   # good
println(classify(50))   # needs work

# Multi-line lambda in a pipe
let data = [1, 2, 3, 4, 5]
let result = data |> map(|x| {
  let squared = x * x
  let label = if squared > 10 then "big" else "small"
  f"{x}^2={squared} ({label})"
})
each(result, |r| println(r))

Closures

Lambdas capture variables from their enclosing scope, forming closures:

# Closure captures 'threshold'
fn make_filter(threshold) {
  |value| value >= threshold
}

let above_50 = make_filter(50)
let above_80 = make_filter(80)

let scores = [30, 55, 72, 88, 95]
println(scores |> filter(above_50))   # [55, 72, 88, 95]
println(scores |> filter(above_80))   # [88, 95]

# Closure captures 'multiplier'
fn make_scaler(multiplier) {
  |x| x * multiplier
}
let triple = make_scaler(3)
println([1, 2, 3] |> map(triple))   # [3, 6, 9]

Recursion

Named functions can call themselves recursively:

# Factorial
fn factorial(n) {
  if n <= 1 then 1
  else n * factorial(n - 1)
}
println(factorial(10))   # 3628800

# Fibonacci
fn fib(n) {
  if n <= 1 then n
  else fib(n - 1) + fib(n - 2)
}
println(fib(10))   # 55

# GCD (Euclidean algorithm)
fn gcd(a, b) {
  if b == 0 then a
  else gcd(b, a % b)
}
println(gcd(48, 18))   # 6

Early Return

Use return for explicit early returns when you need to bail out before the end of the function:

fn find_first_long(items, min_len) {
  for item in items {
    if len(item) >= min_len {
      return item
    }
  }
  return "none found"
}

let genes = ["TP53", "BRCA1", "A", "EGFR"]
println(find_first_long(genes, 4))   # BRCA1

Functions as Values

Functions are first-class: you can store them in variables, lists, and records, and pass them as arguments:

# Store functions in variables
let ops = [|x| x + 1, |x| x * 2, |x| x * x]

# Apply each operation to a value
let val = 3
for op in ops {
  println(op(val))
}
# 4
# 6
# 9

# Pass functions as arguments
fn apply_twice(f, x) {
  f(f(x))
}
println(apply_twice(|x| x + 10, 5))    # 25
println(apply_twice(|x| x * 2, 3))     # 12

Functions with Pipes

Functions work naturally with the pipe operator. The data flows left to right:

# Define reusable pipeline stages
fn keep_long(items, min_len) {
  items |> filter(|x| len(x) >= min_len)
}

fn to_upper_list(items) {
  items |> map(|x| upper(x))
}

# Compose into a pipeline
let genes = ["brca1", "tp53", "a", "egfr", "ab"]
let result = genes
  |> keep_long(3)
  |> to_upper_list()
  |> sort()
println(result)   # [BRCA1, EGFR, TP53]

Higher-Order Patterns

# compose: combine two functions into one
fn compose(f, g) {
  |x| f(g(x))
}
let shout = compose(upper, trim)
println(shout("  hello  "))   # HELLO

# Apply a function n times
fn apply_n(f, x, n) {
  let result = x
  for i in range(n) {
    result = f(result)
  }
  result
}
println(apply_n(|x| x * 2, 1, 10))   # 1024