Nanyx

Higher-order functions

Passing and returning functions

A higher-order function takes a function as an argument or returns one. Nanyx treats functions as values, so higher-order programming is idiomatic.

Passing functions

The simplest higher-order pattern is passing a named function to another function. This keeps reusable logic (add1) separate from traversal logic (map) and makes call sites easier to scan.

def add1 = { x -> x + 1 }

def numbers = [1, 2, 3]

def biggerNumbers = numbers \map(add1)

Trailing Lambdas

When the final argument is a function, Nanyx supports a trailing-lambda style. Use the full form when you want explicit types (see Annotating lambda parameters), and the inline form for short local transformations.

-- Full lambda

def f = { (x: int): int -> x * 2 }

-- Inline lambda

def biggerNumbers = numbers \map { x -> x + 1 }

Shorthand lambdas

The shorthand form omits the parameter and lifts members into scope:

def names = people \map { .name }

def increment = numbers \map { + 1 }

Pattern matching in lambdas

Lambdas can use pattern-matching syntax directly, which is useful when transforming tagged unions. This keeps branching logic close to the point where values are mapped.

things \map {
  | #none -> 0
  | #some(_) -> 1
} \sum

Contextual lambdas

Higher-order functions can accept lambdas that require a context. This lets APIs parameterize the capability available inside the callback.

List.select: (list(a), [Yield(b)] (a -> ())) -> list(b)

In the type above, the callback (a -> ()) runs with a Yield(b) context, so it can call yield while processing each element.

def names: list(Person) -> list(string) = { people ->
  people \List.select { person ->
    if person.isActive then yield(person.name)
  }
}

This pattern is called a contextual lambda: the higher-order function decides which context to provide, while the lambda body uses that context implicitly.

Parametric contexts

Sometimes, a higher-order function wants to be flexible about what kinds of contexts.

For example, what context does a List.map function run in? It could be a pure function, but it could also be a stateful computation or an effectful operation. To support all of these, List.map can be defined with a parametric context:

List.map: [c] (list(a), [c] a -> b) -> list(b)

This function takes a context parameter, written as a single lowercase character (just like an automatically introduced type variable, which must also be one character). This means that the caller can choose any context c for the callback, and List.map will work with it. The callback can be a pure function, a stateful computation, or an effectful operation, depending on the caller’s needs.

def f: list(int) -> list(int) = { numbers ->
  -- Error: List.map inherits the [Console] context requirement from `println`, but `f` does not declare it
  numbers \List.map { n ->
    println(n)
    n + 1
  }
}

If we remove the context parameter and write List.map with a concrete context, like [Console], then any lambda passed to List.map would require the Console context. This would make it impossible to use List.map with pure functions or functions that require different contexts, which is why parametric contexts are important for higher-order functions.

def f: list(int) -> list(int) = { numbers ->
  -- Ok, because the lambda passed to `List.map` is pure (i.e. has no context requirements)
  numbers \List.map { n ->
    n + 1
  }
}
def f: [Console] list(int) -> list(int) = { numbers ->
  -- Also okay, because `f` declares the [Console] context requirement that `List.map` inherits from `println`
  numbers \List.map { n ->
    println(n)
    n + 1
  }
}