Optional is far more than just a simple conditional statement. It’s a great type, that has a lot up its sleeve. Lets take a look at how you could actually use Optionals without using conditionals.
Hello there my sweet fellow developers,
After I shared my innocent posts about avoiding duplication, iflets and guards being unreadable and how bad implicit optionals and force casts are I received a massive ~butth…~ feedback from the community of fellow reddittors. Some of them even claimed that I am incompetent, inconsiderate towards others, rapist and made calls to genocidal actions. Aside from those overexaggerations, what I noticed is that most of comments either labeled Optional as the entity used in conditional statements or the entity, that should lead to crashes. Another important stance I heard as an excuse for avoiding language features used for deduplication, is that the code would become less comprehensible.
I was a bit puzzled to say the least, as conditionals is the most difficult and lengthy way to use the Optionals. That’s because we have loads of functions in the standard library used specifically for optional processing. I’m not even mentioning SwiftZ, SwiftX and the likes, which turn an already powerful instrument into something truly amazing. And yep, they are incomprehensible in case you don’t know the basics behind them, as the methods for Optional handling originate from maths. But that’s not the reason to avoid them, that’s the reason to learn something new and improve.
So, lets start from the beginning. Optional, in case you don’t know, is the type, that represents availability or absence of value. As simple, as that.
Now, lets take a look some conventional Swift code written by the average dev:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
func conditionalCharacterStrings(from lhs: Character, through rhs: Character) -> [String] { let charToUInt: (Character) -> UInt16? = { String($0).utf16.first } guard let lhs = charToUInt(lhs), let rhs = charToUInt(rhs) else { return [] } let isReversed = min(lhs, rhs) != lhs let range = stride(from: lhs, through: rhs, by: isReversed ? -1 : 1) var result = [String]() range.forEach { if let scalar = UnicodeScalar($0) { result.append(String(scalar)) } } return result } |
Disclaimer: This is a sample code outlining the approach, same applies to other snippets. Pardon me, all my readers, who do actually understand that. You’d be shocked at how many people didn’t get, that my articles describe general approach, not specific use cases.
It’s pretty obvious, what the function does, but I’ll still explain. You pass two characters as bounds and receive the array of 1 character long strings in the range between those two characters.
The only thing I wrote in here, that is rarely used is charToUInt, as I wanted to avoid a bit of duplication. charToUInt is your typical function written in a closure way, as declaring such a small and easy to grasp function using func is a bit of an overkill in my opinion. You can read more about that kind of deduplication in my article about first order functions.
Most of you, my dear readers, have either written or seen such code. But have you ever thought, that you could improve that code, make it more extensible flexible and less lengthy?
Now, before we proceed any further, I strongly suggest reading this awesome article about Monads, Functors and Applicatives. While I won’t be using all of the concepts of the article to the fullest with SwiftZ, you’ll still do yourself a favor, as you would surely become better SDE. Moreover it will simplify reading my article.
As a quick reference:
- Monads implement
func flatMap<T>(f: (T) -> M<T>) -> M<T>
, where M is Monad, wrapper around some value T; - Functors implement
func map<T>(f: (T) -> T) -> F<T>
, where F is Functor, wrapper around some value T.
So, with all that knowledge in mind, lets take a look at how we could apply that knowledge to conditionalCharacterStrings:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func averageCharacterStrings(from lhs: Character, through rhs: Character) -> [String] { let bounds = [lhs, rhs].flatMap { String($0).utf16.first } if bounds.count < 2 { return [] } let min = bounds.min() ?? bounds[0] let isReversed = bounds[0] != min let range = stride(from: bounds[0], through: bounds[1], by: isReversed ? -1 : 1) return range.flatMap { UnicodeScalar($0) } .map { String($0) } } |
If you are ready to burst with swearing, it’s the perfect time to close the article and write a comment about how horrible I am.
All the remaining readers must understand one important thing. It’s a different way of solving problems. It’s based on the concepts you are most probably not used to (otherwise, why would you read that article?). Once you understand them, you’ll understand them for life and you’ll’ be able to read all the code like that with ease. Why? Because map and flatMap are higher order functions from math, where you can process a function with another function and return a function as the result. They are as simple, as arithmetics. Remember, when you were still a schoolgirl/boy? Multiply and divide back then were also quite difficult, but now you use them without any problems. So, don’t be afraid.
Now, lets figure out line by line, what the hell is going on.
Bounds is just the way of applying charToUInt from the previous example to function inputs and unwrapping the Optional result of first call in the process. Why is it unwrapped? Its signature is: func flatMap<ElementOfResult>(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult]
. This means, that flatMap in arrays, when the result of function passed as the parameter is Optional, automatically unwraps non-nil values and returns the new array with those values.
It could happen, that our inputs could have no representation in utf16, so we return an empty array in case one or both of the inputs could not be transformed in UInt16.
func min in Array returns Optional as well. It’s because the Array could be empty. In that case there would be no minimum value in it. So, I just unwrap it with default value. Because of the count conditional check done previously the default would never be used, so it’s just a way to avoid implicit unwrapping.
range is of type UInt16. It’s a numeric representation of our result, where each value corresponds to some character.
And now on to the meat of it all. range.flatMap behaves the same way, as Array, when the function passed to it as the parameter, returns an Optional. UnicodeScalar($0) actually returns us an Optional, so the first line of return statement just creates an Array<UnicodeScalar>
of all the numbers in range, that could be presented as the UnicodeScalar.
The second line of return statement transforms Array<UnicodeScalar>
into Array<String>
by applying a transformer function to each of the elements of Array<UnicodeScalar>
.
There are several things I dislike about that solution:
- Unisolated subscript usage could lead to crashes, if the code changes (out of bounds exception is not a joke), so extensive and exhaustive testing of all the cases is necessary;
- Subscripts shadow the intentions behind the code;
- A preemptive exit point breaks the processing flow;
- Default value doesn’t look nice.
So, in order to tackle these problems, lets improve the approach we have:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
extension Array { public func toTuple() -> (Element, Element)? { if self.count != 2 { return nil } return (self[0], self[1]) } } func characterStrings(from lhs: Character, through rhs: Character) -> [String] { let bounds = [lhs, rhs].flatMap { String($0).utf16.first } return bounds.toTuple().map { lhs, rhs in stride(from: lhs, through: rhs, by: lhs == min(lhs, rhs) ? 1 : -1) .flatMap { UnicodeScalar($0) } .map { String($0) } } ?? [] } |
func toTuple
is a mere hack, as tuples in Swift are really limited as of now. They can’t conform to protocols, they can’t have functions of their own. In real world, the code generation approach used in SwiftZ for Kinds and Curry is preferred. Sadly, that’s the only way as of now. This function just returns a tuple of 2 first elements of Array, if the Array count is 2, otherwise it returns nil.
As for the return statement, it essentially does the same, as the previous example, with a few quirks:
- The result of processing is optional, so we default to empty result in case we couldn’t transform inputs into UInt16 or there’s no min value in bounds;
- bounds.toTuple().map expects a function, that returns a value to be wrapped in Optional by map itself.
The solution is hardly comprehensible in case you meet such approach for the first time. On the other hand, it is much more readable, when you are used to the approach, and is extensible and robust. It clearly expresses the intentions in terms of data processing. It avoids all the unnecessary duplication. The solution as a whole is a processing chain with no early fallbacks. The only unsafe code is isolated in a separate function, which needs extensive and exhaustive testing and could then be reused everywhere. It could even be written as a single chainable return statement, if swiftc wasn’t too dumb to solve the expression in a reasonable time.
So, it’s basically, up to you, if you would risk trying the approach. If you do, you’ll be amazed at how simple and short your code becomes, how simple it is to handle, modify and abstract out in case duplication arises.
That’s all, folks. Have a great day and stay DRY, no matter, where you are.