Consider the Following – Part 2
Van Simmons
|
3 min readLast time we explored the notion of “code isomorphisms” – that is to say, code that is functionally identical but uses a different syntax to express the particular idea under consideration, i.e. we’re talking about the pros and cons of different ways of writing the same code. In particular, we pointed out that Swift has three broadly different ways to express the idea of a function of a single variable:
- a plain function declaration
- an `init` on the return type
- a computed `var` on the argument type
And in our discussion we used the following examples:
func doubler(_ anInt: Int) -> String {
" \(anInt * 2) "
}
extension String {
init(doubler anInt: Int) {
self = " \(anInt * 2) "
}
}
extension Int {
var doubler: String {
let anInt = self
return " \(anInt * 2) "
}
}
and pointed out that the program fragment:
" \(anInt * 2) "
was identical in all three examples and what Swift allowed us to do here was to compose that program fragment into larger structures, freely mixing all three styles. I promised last time that I would demonstrate using this code in that way. So…
Consider the following…
let arr = [1, 2 , 3, 4]
let result1 = arr.map(doubler)
print(result1)
let result2 = arr.map(String.init(doubler:))
print(result2)
let result3 = arr.map(\.doubler)
print(result3)
If you run those lines of code, what you will discover is that result in all three cases is precisely the same:
[" 2 ", " 4 ", " 6 ", " 8 "]
This style of code is referred to as “point-free”. The Wikipedia article on Tacit Programming does a pretty good job of explaining the point-free style:
Tacit programming, also called point-free style, is a programming paradigm in which function definitions do not identify the arguments (or “points”) on which they operate. Instead the definitions merely compose other functions, among which are combinators that manipulate the arguments.
Note that in the code above, the argument that is passed into doubler
, String.init(doubler:)
, and \.doubler
is, as Wikipedia says, never mentioned.
This is generally a very difficult thing to pick up for programmers who are coming to Swift from languages where functions are not first-class. In those languages, it is very complex to pass a function as an argument to another function. And so you very rarely do. Composition in those settings is done not by passing functions as arguments but by writing functions that invoke other functions in their body.
In the run-up to the introduction of Swift, Apple provides a way in ObjC to pass functions as arguments. These are called “blocks” and their syntax is so difficult to remember that it sparked websites with humorous names devoted completely to helping people remember the syntax.
Very likely, even if you are comfortable with higher-order functions (functions which accept other functions as arguments or which return functions as their return value) you are much more familiar with what is called the “pointed” style of writing this code. This is largely because Apple encourages that style in much of its documentation. For example in the pointed style, the code above would look like this:
let result4 = arr.map { anInt in " \(anInt * 2) " }
print(result4)
Which again produces precisely the same result as the first 3 examples and which, again, incorporates precisely the same program fragment, letter-for-letter, word-for-word.
So a perfectly rational question that arises is: if these are all identical, how do I decide which one I want to use at any particular time? Shouldn’t I standardize on one particular form when writing my code so that I don’t end up with a mishmash of coding styles thereby harming comprehensibility and readability?
Next time, we’ll discuss the advantages and disadvantages of the point-free style versus the pointed style.