Lambdas

Lambda expressions

A lambda expression consists of:

  • The of keyword
  • A destructuring; the same syntax as for locals
  • An indented block

subscript calls the lambda.

main void() f = of x nat x * 10 log f[1], f[2]

An optional return type annotation goes after of.

main void() f = of::nat x nat x * 10 log f[1], f[2]

A lambda can be squeezed onto one line using then.

main void() f = of x nat then x * 10 log f[1], f[2]

Lambda parameters

All lambdas take a single parameter.
If nothing is written after the of, it means the parameter is void.

main void() f = of log "OK" f[]

As with locals, comma-separated names destructure a single tuple parameter.

main void() f = of x nat, y string log repeat y, x f[3, "foo"]

Lambda types

A lambda type consists of:

  • The return type
  • The mutability (data, shared, or mut)
  • A destructuring in parentheses.

This syntax is used for the type of f below:

main void() f nat mut(x nat) = of x x * 10 log f[1], f[2]

If the lambda expression (of x above) has an expected type, it doesn't need to re-declare the parameter type (nat).

Mutability

The code in a lambda can access and even modify variables outside the lambda. This is called a closure.

main void() cur mut = 0 f = of cur +:= 1 cur log f[] log f[]

There are different kinds of lambdas for different mutability levels. (See Mutability.)
The closure of a lambda affects its mutability:

  • A mut lambda has no restrictions. This is the default for lambdas with an inferred type.
  • A shared lambda can only have shared (or data) types in its closure.
    It can't reference mut locals (even just to read them) regardless of their type.
  • A data lambda can only have data types in its closure.
    It also can't call any global functions. (See Global side effects.)

Most lambdas are mut when there is no need to restrict them.
shared lambdas are used in parallel code. (See Parallelism.)
data lambdas are rarely used, but may be useful in situations where the lambda must be stateless. For example, a comparator lambda used for sorting might be data.

"it"

There is a shorthand syntax for simple lambdas.

main void() a nat[] = 1, 2, 3, 4 log filter::nat[] a, it.is-even # Equivalent: log filter::nat[] a, of x then x.is-even log map::nat[] a, it * 2 # Equivalent: log map::nat[] a, of x then x * 2

The tightly-packed expression containing the it keyword is the body of a lambda.
Expressions that work in an it lambda are:

  • Dotted function calls: it.is-even
  • Prefix operators: -it
  • Binary operators: it * 2
  • subscript calls: it[0]
  • Type annotations: it::nat
  • Option forcing: it!

The it lambda can contain any number of these simple operations:

import keen/math/math main void() a nat[] = 2, 8, 18, 32 log map::float[] a, (it * 2).to::float.square-root / 10

… though the above is just to prove a point and would be more readable as an of lambda.

Most expressions can not be written as an it lambda.
is-multiple-of it, 2 would not work, because that looks like the first argument of is-multiple-of is a lambda.
it is intended for simple expressions; for other cases, use an of lambda.

An it lambda must contain at least one operation.
For an identity lambda, use identity.

main void() a nat[] = 1, 3, 2, 3, 1 log map::(nat set) a, it.identity

"new" interfaces

Defining an interface implicitly defines a new function that takes lambdas corresponding to each interface method.

main void() a i = name: of then "keen" number: of then 1 log a.name, a.number i interface name string() number nat()

"with" blocks

Many functions that take a lambda do some preparation, call the lambda once, and then return.
To make that intention clear, there is a with keyword providing a syntax for it. with blocks provide a syntax to make that intention clear.

A common use of with is to build a collection using build.

main void() xs nat[] = with out in build out ~= 1 out ~= 2 log xs

The above is equivalent to calling with-block build, of out ...

A type annotation on the with keyword applies to the return type of with-block.

main void() log with::nat[] out in build out ~= 1 out ~= 2

A with-block function can be defined like so:

main void() log with in log-start-and-end "a" log "computing a" "result a" log with in log-start-and-end "b" log "computing b" "result b" log-start-and-end record(name string) with-block[t] t(a log-start-and-end, f t mut()) log "start {a.name}" finally log "end {a.name}" f[]

It's not unusual to define a type whose only purpose is to be the first argument to with-block. log-start-and-end above is an example.