Let’s say you have a structure with two very similar properties and you need to choose one of the two. You don’t know which one at compile time, so you need to have a parameter to change that at runtime. This post shows this in a very simple and contrived example, given the data structures:
The Person (the input) has two names: firstName and lastName, and when you need to convert it to a PersonId (the output) you need to select one of the names. How do you do that?
A Bool parameter
The simplest option to implement a switch between two options is to use a Bool parameter. Even in languages where the function parameters can be named, as in swift, it is not very readable to use true/false for two cases — which value maps to which case? You have to read the parameter name to find out:
12345678910111213
extensionPersonId{// Version 0, using a `Bool` togglestaticfuncfrom(person:Person,useFirstName:Bool)->PersonId{letname=useFirstName?person.firstName:person.lastNamereturnPersonId(name:name,age:person.age)}}letjohnDoe_v0_firstName=PersonId.from(person:johnDoe,useFirstName:true)letjohnDoe_v0_lastName=PersonId.from(person:johnDoe,useFirstName:false)print("v0: \(johnDoe_v0_firstName), \(johnDoe_v0_lastName)")// v0: PersonId(name: "John", age: 42), PersonId(name: "Doe", age: 42)
It works, but not entirely clear. For example, how do you pick which case is represented by true? And when you’re reading useFirstName: false, it’s clear that it’s not using the first name and you need to apply a tiny bit of your mental energy to recollect what the other case is — the last name here.
An enum parameter
It’s better to use a custom enum (a sum type) with the cases exactly describing what to select:
123456789101112131415161718192021222324
extensionPersonId{enumPersonNameSwitch{casefirstNamecaselastName}// Version 1, using a custom-crafted `enum`staticfuncfrom(person:Person,usingnameSwitch:PersonNameSwitch)->PersonId{letname:String={switchnameSwitch{case.firstName:returnperson.firstNamecase.lastName:returnperson.lastName}}()returnPersonId(name:name,age:person.age)}}letjohnDoe_v1_firstName=PersonId.from(person:johnDoe,using:.firstName)letjohnDoe_v1_lastName=PersonId.from(person:johnDoe,using:.lastName)print("v1: \(johnDoe_v1_firstName), \(johnDoe_v1_lastName)")// v1: PersonId(name: "John", age: 42), PersonId(name: "Doe", age: 42)
The using: .lastName syntax in the example is much cleaner and easier to understand. This enum is isomorphic to the Bool from the previous section, which means that you can convert between those two types back and forth without losing any information. It’s trivial to write the converters:
So in effect, true = firstName and false = lastName where the = sign means “also known as” (this is a terribly unmathematical definition).
Also note that I’m using an inline lambda where I pick the right field. This is a workaround for the fact that the switch is a statement in swift, not an expression. Briefly, an expression returns a value, whereas a statement does not. If it were an expression, the code would be nicer:
I believe this is what the Open/Closed Principle is about: the implementation of the PersonId.from(person:using:) function is closed for modification, and yet it is open for extension by providing an alternative PersonNameSelector parameter.
A few suggested enhancements follow.
Using properties as functions
We can simplify the calling syntax by using properties as functions, even though we have to declare them manually because swift doesn’t generate those for us:
Also, if we curry our factory function PersonId.from(person:using:), we can partially apply the first argument and get a function that only takes a Person:
1234567891011121314
extensionPersonId{// Version 3, which is manually curried version 2staticfuncfromPerson(usingnameSelector:@escapingPersonNameSelector)->(Person)->PersonId{return{personinreturnPersonId(name:nameSelector(person),age:person.age)}}}letfromPersonUsingFirstName=PersonId.fromPerson(using:Person.firstName)letjohnDoe_v2_curried_firstName=fromPersonUsingFirstName(johnDoe)print(johnDoe_v2_curried_firstName)// PersonId(name: "John", age: 42)
This allows us to reuse the fromPersonUsingFirstName function in multiple places which only know about Persons and shouldn’t care about which specific PersonNameSelector to use.
Conclusion
Using a Bool parameter is a very generic way because its two values true and false are very generic and don’t match our domain model. You have to work around that by naming the function parameter of type Bool. A better idea is to introduce an enum designed for the domain model, whose values are more readable, but are still limiting. A function is a more abstract way of selecting a property than an enum, which also allows easier extension.