Skip to content

Functions

Functions in Do are first-class values. They close over their lexical scope and can be stored in variables, passed as arguments, and returned from other functions.

def

Define named functions with def:

def greet name
  echo "Hello, $name!"

greet Alice
# prints: Hello, Alice!

Functions with no arguments must have () after their name:

def hello()
  echo Hello, world!

Implicit Return

A function returns the result of its final statement:

Statement Result
command function return value
let value of right-hand side
bind value of scrutinee
if (with final else) result of branch
if (without final else) nil
try/catch result of try (no error) or invoked catch

All other statements have a nil result.

Explicit Return

Use return for early exit:

def abs x
  if (x < 0)
    return (-x)
  x

Parameters

Positional Parameters

def add a b
  (a + b)

Keyword Parameters

Keyword parameters use key: param syntax in the definition:

def create_user name age: user_age
  echo "$name is $user_age"

create_user Alice age: 30

Ditto Key Shorthand

The :key shorthand declares a key parameter bound to a variable of the same name:

def create_user :name :age
  echo "$name is $age years old"

create_user name: Alice age: 30

The shorthand also works at call sites to pass a variable as a key argument:

let name = Alice
let age = 30
create_user :name :age
# equivalent to: create_user name: $name age: $age

Default Values

Both positional and key parameters support defaults:

def greet name = "World"
  echo "Hello, $name!"

greet()        # Hello, World!
greet Alice    # Hello, Alice!

def connect :host = localhost :port = 8080
  echo "Connecting to $host:$port"

connect()                      # localhost:8080
connect port: 3000             # localhost:3000
connect host: example.com      # example.com:8080

Defaults are evaluated on every invocation of the function, so the following function always returns a fresh empty array if called with no arguments:

def default_empty arg = []
  arg

Defaults are evaluated left-to-right and may reference earlier parameters:

def window start end = (start + 10)
  start..end

assert_eq (window 5) (5..15)
assert_eq (window 5 20) (5..20)

Variadic Parameters

Use ... to accept extra arguments:

def log level ...rest
  echo "[$level]" ...rest

log INFO hello world
# prints: [INFO] hello world

The rest parameter receives an immutable argument pack. It is iterable, but is not itself an iterator, so iterating or spreading it does not consume it. Iteration yields positional and key arguments in invocation order as key/value tuples, where the key is the positional argument index (0-origin) for positional arguments.

def echo_all ...args
  for k v = args
    echo "$k: $v"
echo_all foo bar: 1 baz
# prints:
# 0: foo
# bar: 1
# 1: baz

Argument Spreading

Spread collections or iterators into a call:

let args = [1, 2, 3]
func ...args
# equivalent to: func 1 2 3

let kwargs = {name: "Alice", age: 30}
func ...kwargs
# equivalent to:
# func name: "Alice" age: 30

Vertical Parameter Layout

Parameters in def can use vertical layout:

pub def build
  :from
  :pull = true
  :tag
  ...args
do
  echo "Building $tag from $from"

do Blocks

Anonymous functions (blocks and lambdas) are created with do:

Statement Context

In statement context, do without a following newline creates a one-statement block:

let greet = do echo "hello"
greet()  # prints: hello

With parameters:

let double = do |x| echo (x * 2)
double 5  # prints: 10

If an immediate newline and indented block follows, it creates a multi-statement block:

let process = do |x|
  let doubled = (x * 2)
  echo "Result: $doubled"
  doubled

Expression Context

In expression context, do creates a lambda where the body is an expression:

assert_eq ((do |x| x * 2) 5) 10

Non-Local Flow Control

break, continue, and return work through do blocks:

  • break exits the innermost enclosing loop
  • continue skips to the next iteration of the innermost enclosing loop
  • return exits the innermost enclosing def

This allows natural flow control in callbacks and higher-order functions:

def validate_record record
  for field = ["id", "name", "email"]
    record.get field else: do
      return {valid: false, missing: field}
  {valid: true}

A non-local branch will only be effective for the duration of the statement that introduces its containing closure; after this it will propagate a runtime error. The compiler requires that a closure containing non-local branches be in argument position to reduce the likelihood of this sort of error:

# Valid: closure is passed as an argument
def validate record
  record.get "name" else: do
    return false
  true

# Invalid: closure is bound to a variable first
def bad_example()
  # Compiler will reject this line
  let closure = do return false
  record.get "name" else: $closure

Public Functions

Use pub def to export a function from a module:

pub def helper x
  (x + 1)

See Modules for details on the module system.

Decorators

Function defs may be preceded by decorators:

#[memoize]
def fib n
  if (n < 2)
    n
  else
    (fib(n - 1) + fib(n - 2))

See Decorators for syntax, ordering, and examples.