Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Defining Functions

Functions let you name a sequence of actions and reuse it. Bop has first-class functions — you can store them in variables, pass them to other functions, return them from other functions, and stash them in arrays.

Declaring a function

fn greet() {
  print("Hello!")
}

greet()    // "Hello!"
greet()    // "Hello!"

Parameters

Functions can take parameters — values you pass in when calling:

fn repeat_string(text, times) {
  let result = ""
  repeat times {
    result += text
  }
  return result
}

print(repeat_string("ha", 3))    // "hahaha"

Parameters are positional. There are no default values or type annotations.

Return values

Use return to send a value back from the function:

fn double(x) {
  return x * 2
}

let result = double(5)
print(result)    // 10

return with no value (or reaching the end of the function) returns none:

fn do_something() {
  print("Working...")
  // no return — returns none
}

let result = do_something()
print(result)    // none

Early return

return exits the function immediately, even from inside loops or conditionals:

fn find_first_big(numbers, threshold) {
  for n in numbers {
    if n > threshold {
      return n
    }
  }
  return none
}

let result = find_first_big([3, 7, 1, 15, 4], 10)
print(result)    // 15

Calling functions

Parentheses are always required, even with no arguments:

greet()          // correct
// greet         // error — 'greet' is a function, call it with greet()

Practical example: sum of squares

fn sum_of_squares(n) {
  let total = 0
  for i in range(1, n + 1) {
    total += i * i
  }
  return total
}

let result = sum_of_squares(5)
print("Sum of squares: {result}")    // Sum of squares: 55

Recursion

Functions can call themselves. Bop caps recursion depth to prevent runaway stacks (also bounded by the step limit):

fn factorial(n) {
  if n <= 1 {
    return 1
  }
  return n * factorial(n - 1)
}

print(factorial(5))    // 120

First-class functions

A named fn is a value just like anything else — you can assign it to a variable, pass it as an argument, return it from another function, or store it in a collection:

fn double(x) { return x * 2 }
let f = double
print(f(7))          // 14

fn apply(f, x) { return f(x) }
print(apply(double, 21))    // 42

Function expressions (lambdas)

fn(...) { ... } — without a name — is an expression that produces a function value. Use it when you want a one-off function inline:

let square = fn(x) { return x * x }
print(square(6))     // 36

let mul = fn(a, b) { return a * b }
print([mul(2, 3), mul(4, 5)])    // [6, 20]

Closures

Function expressions capture variables from the enclosing scope. The capture is a snapshot taken at the moment the closure is built — mutating the outer variable afterwards doesn’t change what the closure sees:

let n = 5
let add_n = fn(x) { return x + n }
n = 100
print(add_n(3))      // 8, not 103

The classic “factory returning a specialised function” pattern works:

fn make_adder(n) {
  return fn(x) { return x + n }
}

let add5 = make_adder(5)
let add10 = make_adder(10)
print(add5(3))       // 8
print(add10(3))      // 13

Recursion in lambdas

An anonymous fn(...) can’t see itself by name. If a lambda needs to recurse, assign it to a named fn instead — named fns are visible inside their own body:

fn fib(n) {
  if n < 2 { return n }
  return fib(n - 1) + fib(n - 2)
}
print(fib(10))       // 55