Callers might want to handle nil
differently
The power of Optional
is that it gives users to handle nil
in many different ways.
- They can substitute a default value using the nil-coalescence operator (
??
)
- They can assert that it's not
nil
, using the force unwrap operator (!
)
- They can transform the value, using
map
or flatMap
There's no universally correct approach. So any determination you make within your callee-side handling is likely to be wrong for at least some callers. That's why we should let callers handle nil
themselves.
It would make a total mess
Thinking about this practically, if it was decided that "callees should handle nil
for callers", than all Swift code would be pervasively littered with optionals. That's not very great.
There's already a tool for this job
What you would find is a lot of repetition of the same code that you just wrote "check if x
is nil
, and if it's not, transform it by passing it through f
, otherwise just pass the nil
along quietly". When we identify repetition in code like this, we should aim to extract it out using some kind of abstraction. In this case, it already exists, in the form of Optional.map(_:)
.
Given these two functions:
func makeUserHandlingOptionals(from apiUser: APIUser?) -> User? { ... }
func makeUser(from apiUser: APIUser) -> User { ... }
One can be trivially implemented in terms of the other:
func makeUserHandlingOptionals(from apiUser: APIUser?) -> User? {
return apiUser.map(makeUser(from:))
}
It's so trivial, in fact, that you shouldn't declare makeUserHandlingOptionals(from:)
at all. If a caller wants to transform a APIUser?
, just let them call map
themselves.
A similar story in Java
In the past, I've had a similar temptation occur to me. I was writing Java before 1.8 (which introduced the stream APIs). I had a function that took an A
and returned a B
. But I had an ArrayList<A>
, and it was very tedious to convert it to an ArrayList<B>
. I had to do it myself every time:
B convertAtoB(A a) { ... }
ArrayList<A> inputOfAs = ...
ArrayList<B> outputOfBs = new ArrayList();
for (A inputA : inputOfAs) {
outputOfBs.add(convertAtoB(inputA));
}
I needed to do that a lot, and that was annoying. I've had a similar temptation as you did, perhaps I should introduce a second overload version of convertAtoB
, which took an ArrayList<A>
and returned an ArrayList<B>
. But then that seems annoying, so maybe the right choice is to only have the array version, and pass it single-element arraylists when I need to transform a single element.
I didn't have a good solution, until I eventually learned about streams and "functional style" constructs like map
. map
completely solves this problem, by abstracting away the details of making an (A) -> B
function able to work as if it were a ([A]) -> [B]
function. That way, convertAtoB
can stay focused on converting A to B, and not fiddle around with array lists.
Switching to map
even improved performance. At the time, I didn't realize that repeaded ArrayList.add
calls would cause occasional resizing operations. When the current array got full, ArrayList
would have to allocate a new larger one, and spend O(n)
time copying the old elements over. Only to do so again momentarily later. The implementer of map
would know that the input and output arrays always have the same size (by the definition of the map
). Thus they could pre-allocate an output array that's precisely large enough to fit all the new B elements, without resizing and without excess. That's something I hadn't thought about at the time, and got "for free" in the process.
Conclusion
TL;DR: Your function should stay focused on doing the transformations it's responsible for.