First-Class Functions: Now Boarding
A function is just a value
Int is a type. String is a type. (Int, Int) -> Int is also a type, and this is the moment the series changes gears, so let's say it plainly: a function is a value. Not a ceremony. Not a declaration bolted to a file. Not a special citizen with special paperwork that requires a visa to breathe the same air as you and me. Not a contractor in the company who performs the exact same work as their colleague, but can't use the bathroom because it's only for employees (true story). A value, as ordinary as 5, storable in a constant exactly the way 5 is:
let op: (Int, Int) -> Int = (+)
op(2, 3) // 5(+) isn't being called there. An infix operator is like a bowl of lasagna fresh out of the oven: a dish like any other, but hot as hell, that's why we're wearing oven mitts there. Safely inside its parentheses, the operator is an ordinary value, and op now holds it. Calling op(2, 3) calls whatever op happens to hold: today addition, tomorrow whatever you reassign... actually no, it's a let. Addition forever. Delicious.
Take a moment of silence for the other languages. C would only let you point at a function, from a safe distance, with syntax that looks like a cat walking on the keyboard. Java made its functions dress up as anonymous inner classes for nineteen years before letting them outside in 2014, and even then, chaperoned. Swift's functions were born first-class: they board first, they get the warm towel, the 5cm of extra legroom, the complimentary wine. They go wherever values go and nobody asks to see their papers.
Several functions, one array
If a function is a value, an array of functions is just an array. Line them up like suspects, and keep an eye on (*), it has priors:
let operations: [(Int, Int) -> Int] = [(+), (-), (*)]
for operation in operations {
print(operation(10, 4))
}
// 14
// 6
// 40Same two numbers, three different verdicts, each function pulled out of the lineup and applied. Nothing here does anything an [Int] loop doesn't already do; the elements just happen to have opinions about what should be done with 10 and 4. And I'm glad the same old Math is still standing after all this time.
Anywhere an Int can go
The array was just the warm-up. Every door Int can walk through, a function type walks through too: generic parameters, optionals, dictionary values, associated values in an enum:
struct Box<T> { let value: T }
let boxed: Box<(Int, Int) -> Int> = Box(value: (+)) // generics don't blink
let maybe: ((Int, Int) -> Int)? = (+) // optionals hold functions
let menu: [String: (Int, Int) -> Int] = ["add": (+), "sub": (-)] // dictionary values
enum Strategy {
case constant(Int)
case computed((Int) -> Int) // enum associated values
}
typealias Operation = (Int, Int) -> Int // typealiases, naturally
let named: Operation = (+)Even a protocol's associated type can be witnessed by a function type: a conforming make() returning (Int, Int) -> Int satisfies associatedtype Output without anyone raising an eyebrow. Generic parameter, payload, value, witness: full mobility, any gate, any terminal. The white zone is for the loading and unloading of values only. Remember this list. There will be a test later.
Passing a function in
Once functions are values, a function that takes another function as a parameter is the same species as any other function, just with a function-shaped hole instead of an Int-shaped one. The industry calls these higher-order functions, which sounds like a priesthood but mostly means "has interesting friends":
func applyTwice(_ transform: (Int) -> Int, to value: Int) -> Int {
transform(transform(value))
}
func double(_ x: Int) -> Int { x * 2 }
applyTwice(double, to: 3) // 12applyTwice neither knows nor cares that it was handed double. It calls whatever shows up, twice, because once is rarely enough. Swap in any other (Int) -> Int and applyTwice keeps working without changing a line: it isn't attached to any particular function, it just has a type it's into. We'll politely call that flexibility.
Where baby functions come from
The same reasoning runs the other way. A function that returns a function isn't returning anything exotic either, this is simply how baby functions are made. The PG-13-rated version, anyway. When a function and an argument love each other very much, the function ends up with a baby function in its belly, or, how to say this politely... in its output:
func adder(_ amount: Int) -> (Int) -> Int {
{ value in value + amount }
}
let addFive = adder(5)
addFive(10) // 15adder(5) runs once and delivers { value in value + 5 }, straight into addFive. Note what happened to amount on the way: Swift calls it captured, and it's never coming back. It lives inside addFive now, remembered on every call, no ransom demanded and none offered. Captured values tend to develop a deep fondness for their captor; computer science calls it a closure, psychiatry calls it something else.
And because addFive is an ordinary value, it goes straight back into the higher-order function from before, the newborn meeting the family:
applyTwice(addFive, to: 0) // 10applyTwice still has no idea where addFive came from: a top-level declaration, born seconds ago inside adder, fished out of an array. Values don't carry birth certificates. That's first-class: a parameter or a return type that happens to be a function is as unremarkable as one that happens to be Int. Case closed, cabin crew, prepare for landing... except. Someone in row one has been reading the fine print on the boarding pass.
Give the wine back
Time for the test we promised. Int conforms to protocols: Equatable, Hashable, Codable, the whole society. Int accepts extensions; you can teach new tricks to an old Int, and from your own module. Surely a type with this much mobility can do those perfectly ordinary value things too:
extension Int {
var doubled: Int { self * 2 } // sure, why not
}
extension (Int) -> Int {}
// error: non-nominal type '(Int) -> Int' cannot be extended
let a: (Int) -> Int = double
let b: (Int) -> Int = double
a == b
// error: binary operator '==' cannot be applied to two '(Int) -> Int' operands
let ops: Set<(Int, Int) -> Int> = []
// error: type '(Int, Int) -> Int' does not conform to protocol 'Hashable'
extension Operation {}
// error: non-nominal type 'Operation' (aka '(Int, Int) -> Int') cannot be extended
extension Operation: Equatable {}
// error: same message, the alias changes nothingIt can't, and don't call me Shirley. Non-nominal. The compiler's own word: a type with no name, anonymous if you will. Where have we heard that before. And notice the typealias attempt at the end, the obvious loophole, closed. Operation has a name now, but a nickname isn't a birth certificate: the error message even unmasks it, *aka (Int, Int) -> Int*, like security reading the real passport under the sticker. No extensions, no protocol conformances, no == (comparing two functions for equal behaviour is mathematically undecidable, so this one isn't even Swift's fault), which also means no Set of functions, no functions as dictionary keys (values yes, keys no), and no Codable, because nobody has invented a JSON encoding for behaviour. So: give the wine back. The warm towel too. The seat pitch returns to economy. Our first-class citizen, it turns out, holds a passport that several rather important countries decline to stamp.
Here's your chaperone, Java
There is a fix, and after the things we said earlier it costs some pride to type it: wrap the function in a struct. Yes. A wrapper. Looks like we picked the wrong week to quit wrapping functions. Here's your damn chaperone, Java, laugh away. But look closely at what the chaperone charges, and what it pays:
struct Adjust {
let run: (Int) -> Int
func callAsFunction(_ value: Int) -> Int {
run(value)
}
}
let doubled = Adjust { $0 * 2 }
doubled(21) // 42, callAsFunction: still walks and quacks like a functioncallAsFunction keeps the call site honest: doubled(21), no ceremony, you'd never know there's a struct at the controls. Otto would be proud. And the struct has the one thing the bare function never had: a name. Nominal at last, it can join society:
extension Adjust {
func map(_ transformOutput: @escaping (Int) -> Int) -> Adjust {
Adjust {
let originalOutput = run($0)
let mappedOutput = transformOutput(originalOutput)
return mappedOutput
}
}
}
doubled.map { $0 + 1 }(10) // 21, a function that grew a method
extension Adjust: CustomStringConvertible {
var description: String { "a function in a nice suit" }
}A function with methods. A function conforming to protocols. And Adjust was the timid version: wrap the shape generically and the trick scales to every function that will ever exist:
struct Fn<A, B> {
let run: (A) -> B
func callAsFunction(_ value: A) -> B {
run(value)
}
}
extension Fn {
func map<C>(_ next: @escaping (B) -> C) -> Fn<A, C> {
Fn<A, C> { next(run($0)) }
}
}
let length = Fn<String, Int> { $0.count }
let isLong = length.map { $0 > 10 } // Fn<String, Bool>, the type transformed too
isLong("lasagna") // false
isLong("referential transparency") // truemap was written once, and every Fn<A, B> in the universe now has it. And because the wrapper is nominal, constraints work too, so abilities can be granted to just a sub-family:
extension Fn where B: Numeric {
func doubled() -> Fn<A, B> {
map { $0 * 2 }
}
}
length.doubled()("mitts") // 10, only functions returning numbers get thisTry attaching either of those to a bare (String) -> Int. This little pattern (one struct, one stored function, one name) will carry a surprising amount of the series ahead, because naming a function's shape turns whole categories of problems into values. Put an (Environment) -> Value in the box and you have dependency injection without a framework, inversion of control held in a let. Put a ((B) -> Void) -> Void in the box (a function that takes a callback) and you have a promise: asynchronous work you can store, hand around, and map before it ever runs. Same move every time: give the shape a name, and the shape gains powers. You'll meet it so often in the coming articles that you'll start greeting it by name. The chaperone turned out to be a promotion.
So, the final verdict: in Swift, functions are first-class citizens with an asterisk, full mobility but no memberships. They go anywhere a value goes, they just can't join a protocol without putting on the suit. Most of the time the bare function is everything you need, which is what everything ahead runs on: Currying: One Argument at a Time's returned closures, composition's wiring, eventually map itself. And when you do need the suit, it's one struct away, worn by choice, which, Java, is the difference between a tailor and a dress code. I just want to say good luck. We're all counting on you. From here on, functions aren't just what the code does. They're what the code is made of.