Do you hate Swift iflet and guard verbose syntax? I have a solution for you.
I have a confession to make. I’m really tired of writing iflets and guards all over the place in my code. There are some really basic problems that I see:
- You are struggling with Swift instead of expressing, what needs to be done;
- You don’t have a central access point to the casting code;
- You are not robust to Swift API changes;
- You have to write more code and autocomplete won’t save you$
- You don’t use type inference.
First of all, let me introduce the casting function I use:
1 2 3 4 |
func cast<Type, Result>(_ value: Type) -> Result? { return value as? Result } |
Yep, that’s just a wrapper above as?. Why is it good? It allows casting to types without considering the preprocessing limitations of Swift, that were created for the mere reason of helping out the devs, that are clueless about the types. A perfect example is an Int?? type (yep, optional wrapped in optional, you didn’t misread it) that we want to cast to Int?. And guess what, compiler won’t let you do that with as?:
1 2 3 4 5 |
let integer: Int?? = 1 if let casted = integer as? Int? { print(casted) // I know it's incorrect, but I'm too lazy } |
Compiler fails with Downcast from 'Int??' to 'Int?' only unwraps optionals; did you mean to use '!'?
. This means, that the compiler can do that, but, it thinks it knows better, what the programmer wanted, so it tries to help by stressing, that by unwrapping Int?? into Int? we receive Optional<Int>
as the result. Oh thank you so much, Captain Obvious, we couldn’t have guessed it without your valuable help.
So, another solution is to use generic types. You see, from the point of view of Swift itself (if we could have disabled sanity checks for programmers), the type in generic could be any type, including function, optional, array or anything else. And, you guessed it, Int?? as well as Int? are also separate types. Moreover, as? itself could be used for unwrapping (although, the reasons behind why casting could result in unwrapping are behind my understanding and both AST and SIL don’t provide any clues about such a behavior). So, if we take a look at the cast function above, if we wanted to cast and unwrap from Int?? type to Int?, we could just write:
1 2 3 4 5 |
let integer: Int?? = 1 if let casted: Int? = cast(integer) { print(casted + 1) // I know it's incorrect, but I'm too lazy } |
This would print us: Optional(1)
. If, on the other hand we try to cast Int?? to Int type system would behave as expected and wouldn’t print anything:
1 2 3 4 |
if let casted: Int = cast(integer) { print(casted + 1) } |
However, such a function, although being type inference – enabled and chainable, isn’t particularly useful in terms of the amount of code you have to write. This could be alleviated by using functors, applicative or monads. I can confess, that I’m making my first steps into functional programming, so I’ll cover those types in a later blog posts, after I manage to get a hold of the ideas behind FP and how to mix them with OOP changeable state with the help of a friendly functional community (kudos to all lispers and haskellers, who answer my stupid questions and write free open-source books explaining difficult concepts in a simple manner, you are the best, guys). For now, lets just define those types in simple incorrect manner:
- Functor – type, who implements map;
- Applicative – type, who implements apply;
- Monads – type, who implements flatMap.
So, lets take a real – life problem instead. Suppose we have an Int property and we want to put Any in it, if its value is castable to Int:
1 2 3 4 5 6 7 8 9 10 11 |
class IntHolder { var value = 1 func output() { print("holder value is \(self.value)") } } let holder = IntHolder() let value: Any = 1 |
We can’t directly assign the Any value into Int property, so we should use either iflet or guard for unwrapping:
1 2 3 4 |
if let unwrapped = value as? Int { holder.value = unwrapped } |
The amount of code for the person with objc background, who is used to nil being a valid value, is horrific. Moreover, don’t forget, that you can’t send messages to values wrapped into Optional directly, unlike objc, where it’s a valid operation (unless you send the messages, where the result is a struct). How would our cast function help in that case?
1 2 |
cast(value).map { holder.value = $0 } |
That’s much better and as long, as you remember, that cast returns an Optional, this code is much more expressive. Overall, even if you don’t, the code is still expressive, as long, as you at least remember what a map is (apply a function to a wrapped value, if the value is legitimate). All the types in that case are inferred by compiler without any external guidance, so we can write a simpler more light-weight typesafe code with less verbose syntax.
Still, just imagine writing such casts for each cell type, as in our previous example. The duplication and struggling with the verbose inexpressive syntax is what awaits us. And that’s far from DRY we all try to abide (we do try that, don’t we?). Moreover, there are is another obvious flaw: cast is unchainable, unless you bloat your code.
The result of the above cast + map + assignment is ()? (or Void?, if you are more used to it). This means, that the second map in a row wouldn’t do us any good, as we can’t actually do anything useful with unwrapped ().
But that’s the tale for another day. We’ll dive into making the casting chainable in one of my next articles.
cast function is available at cocoapods IDPCastable and is available on github as well: IDPCastable.
That’s all, folks. Have a great day and stay DRY, no matter, where you are.