Slide 31 of 74

First-class functions

A bare reference, and map
func double(n: Int): Int { return n * 2 }

proc main(): void {
	f := double
	echo "{f(21)} {map([1, 2, 3], double)}"
}
Partial application, and a variant with holes
type Msg {
	Changed { text: Str }
}

func add(a: Int, b: Int): Int { return a + b }

proc main(): void {
	mut n := 5
	addN := add(n, _)       // _ becomes the parameter; n is taken now
	n = 100
	echo addN(10)           // 15 — what was supplied was kept as it was

	make := Msg.Changed(_)  // a function that builds the variant
	echo make("typed")
}
Writing the type down
func double(n: Int): Int { return n * 2 }
func shout(s: Str): Str { return s + "!" }

func applyTwice(f: func(Int): Int, n: Int): Int {   // as a parameter
	return f(f(n))
}

func picked(loud: Bool): func(Str): Str {            // as a return
	return shout
}

proc main(): void {
	func(Int): Int step = double                      // as a variable's type
	echo applyTwice(step, 5)
	echo picked(true)("hive")
}
A proc that asks for a mutex, as a value
proc grow(names: mut Str[dyn], name: Str): void {
	append(names, name)
}

proc twice(step: proc(mut Str[dyn]): void, names: mut Str[dyn]): void {
	step(names)
	step(names)
}

proc main(): void {
	mut Str[dyn] names = ["ada"]
	twice(grow(_, "bee"), names)   // the mutex stays a hole
	echo names
}
What a slot takes
proc third(v: Str[3]): void { echo v[2] }
proc anyLength(v: Str[]): void { echo len(v) }

proc callWithThree(f: proc(Str[3]): void): void {
	f(["a", "b", "c"])
}

proc main(): void {
	callWithThree(third)        // fine: the slot promises three
	callWithThree(anyLength)    // fine: any length takes three too
	// proc(Str[]): void g = third   // compile error: g may be called with one
}

A callable is a value, made one of three ways:

  • a bare name: f := double;
  • a partial application, a call with _ holes: add(5, _) is a function of one argument, each hole becoming a parameter in the order written. What is supplied is worked out once, where it is written, and kept: changing the variable afterwards changes nothing the function holds;
  • a variant with holes: Msg.Changed(_) is a function that builds the variant. Msg.Changed on its own is not a value.

Its type is written like a declaration with the names dropped — func(Int): Int, proc(mut Str[dyn], Str): void — and it serves as a parameter, a return type and a variable's own type. A func value fills a proc slot, never the other way round, and a call through any function value is held to its type: how many arguments and what each is, by position, since a value's parameters have no names. A mut position in a partial application stays a hole, because what is supplied is kept as a copy that writes would never reach.

A function value fills a slot only when it takes everything the slot's calls can hand it: a proc(Str[3]): void cannot go where a proc(Str[]): void is wanted, since that slot may be called with any length, while the reverse is fine. A builtin, a library call and a generic callable cannot become values at all — wrap one in a func of your own.