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

Functions and Lambdas

Signatures

fn clamp_to(x: Int, lo: Int, hi: Int) -> Int
  ex clamp_to(5, 0, 3) == 3
  ex clamp_to(-1, 0, 3) == 0
= if x < lo then lo else if x > hi then hi else x

A signature names every parameter’s type and the return type. The return type can only be left out when it is Unit. There is no overloading and there are no default arguments, so one name means one function.

ex lines are examples. They belong to the signature, are hashed with the function, and run as tests: sspur test counts clamp_to.ex1 and clamp_to.ex2 with the other tests. Use them for the two or three cases that explain what a function does.

Calling with a dot

x.f(a) is the same call as f(x, a). You can write your own functions and call them like methods, and chain them like the built-in ones:

fn shout(s: Str, times: Int) -> Str
= s.upper.repeat(times)

test ufcs = "hi".shout(2) == "HIHI" and shout("hi", 2) == "HIHI"

x.f without parentheses is a field if x has a field f, and otherwise a call with no other arguments, like s.upper above.

Lambdas

x => e is a function value. Several parameters are written (a, b) => e and none () => e. A function-typed parameter is written A -> B, or (A, B) -> C.

A parameter can name its type, (x: Int) => x + 1, which helps when nothing else tells the checker what it is, as for a lambda bound to a name. _ inside a call argument is a shorter lambda: _ * 2 means x => x * 2. Every _ in that argument is the same parameter, so xs.map(_.price * _.qty) multiplies two fields of one element.

fn apply_twice(f: Int -> Int, x: Int) -> Int
= f(f(x))

test lambdas = do
  add3 = x => x + 3
  apply_twice(add3, 1) == 7 and apply_twice(_ * 2, 5) == 20

test placeholder = [1, 2, 3].map(_ * 10) == [10, 20, 30] and ["b", "a"].sort_by(_) == ["a", "b"]

Block lambdas

When a lambda needs more than one line, write do after => and indent the block deeper than the line the lambda starts on. The last line is the value, and the closing ) follows it directly:

fn grades(scores: List[Int]) -> List[Str]
= scores.map(s => do
  pct = s * 100 / 50
  if pct >= 90 then "A" else if pct >= 70 then "B" else "C")

test block = grades([48, 40, 20]) == ["A", "B", "C"]

A block lambda captures variables, performs effects and is optimized like a one-line lambda. return isn’t allowed inside one (E_RETURN_IN_LAMBDA), because it would be unclear whether it leaves the lambda or the function; make the last line an if instead, or move the logic into a named function.

Functions as values

A function’s name is a value of its type. So is a trait method, once the parameter type is known (Generics and Traits).

fn is_small(n: Int) -> Bool
= n < 10

test fn_values = [3, 30, 4].filter(is_small) == [3, 4] and [1, 2, 3].fold(0, (acc, x) => acc + x) == 6

Local functions

A do block can define a helper. It sees the enclosing variables and can call itself, but it can’t be generic:

fn digits(n: Int where _ >= 0) -> List[Int]
= do
  fn go(m: Int, acc: List[Int]) -> List[Int]
  = if m < 10 then acc.push_front(m) else go(m / 10, acc.push_front(m % 10))
  go(n, [])

test local = digits(507) == [5, 0, 7]
$ sspur test functions.ssp
8 passed, 0 failed

Six tests and two examples. Next: Records and Sum Types.