Type Erasure in Swift*
I found a number of great articles about associated types and type erasure, but had a really hard time to apply them to my own situation, as most of them deal with Pokemons, Foo/Bar or animals. With that in mind, I figured I should write about my problem and how I managed to solve it.
My situation
I’m currently working on an application that contains a few basic forms. I just started working on the first one, so I have only two kinds of field: a classic text field and a date picker. I started describing my form field with a protocol. The idea was to think about what I needed from a form field: a method I could invoke to validate the content of my form and a value. Because my value would be a String
in my TextField and a NSDate
for my date picker, I used an associated type.
enum ValidationResult {
case Valid
case Invalid(errorMessage: String)
}
protocol FormFieldType {
associatedtype Type
var value: Type? { get set }
func validate() -> ValidationResult
}
Concrete types
Once I had my protocol, I could use them on my 2 form fields. Note that I’m not actually validating my fields and always returning .Valid
because I want to keep things simple. I used an enum when I could have returned a Bool
but that’s because, as I wrote on my company’s blog, I really love enums.
class TextField: UIView, FormFieldType {
typealias Type = String
var value: String?
func validate() -> ValidationResult {
return .Valid
}
}
class DatePicker: UIControl, FormFieldType {
typealias Type = NSDate
var value: NSDate?
func validate() -> ValidationResult {
return .Valid
}
}
Validation
To validate my form, the idea was to put all my fields in a collection and perform a reduce to end up with a single Bool
telling me if my form was valid or not. Even if you haven’t been doing Swift for a long time, you may have aready ran into this error message that doesn’t tell you much.
let firstName = TextField()
let lastName = TextField()
let date = DatePicker()
let fields: [FormFieldType] = [firstName, lastName, date]
// ⚠️ Protocol 'FormFieldType' can only be used as a generic constraint
// because it has Self or associated type requirements
In this situation, the compiler is telling you that you can’t create a collection of FormFieldType
items because this protocol has an associated type. That’s exactly our sitation here, we are trying to create a collection with 3 elements, 2 of which declared String
as their associated type while the last one uses NSDate
.
Solution
https://twitter.com/merowing_/status/766755746356822016
Our problem here comes from the associated type and the solution is a very pragmatic one: we need to ignore the associated type because all we care about is the validate
method. So, let’s create an object that does just that!
struct AnyField {
private let _validate: () -> ValidationResult
init<Field: FormFieldType>(_ field: Field) {
self._validate = field.validate
}
func validate() -> ValidationResult {
return _validate()
}
}
One of the great features of Swift is that instances’ methods are actually curried functions. That’s a whole other concept so I would suggest you go and read this article by Ole Begemann. It allows us to keep a reference of the validate
method of the FormFieldType and invoke it when we need to. We can now create a Collection of AnyField
objects to validate the whole form.
let fields = [AnyField(firstName), AnyField(lastName), AnyField(datePicker)]
if fields.reduce(true, combine: { ($1.validate() == .Valid) && $0 }) {
print("Form is valid 🎉")
}
I titled this post with a * because like many concepts out there, the way I solved my problem may not be actual type erasure (but then again, the type has been erased). What is sure is that I now have a way to validate a form by dealing with very simple protocols. Should I decide to take it to the next level and separate my field from the view (and I probably will), it wouldn’t take a lot of work.