Table of Contents
To understand lenses in Swift, we should first review what we refer to when talking about immutability of an object.
We understand as an immutable object that object which can not be modified once it is created.
Its use gives us great advantages such as the reliability that our object has not undergone changes throughout the execution of the program, as well as its quality of thread-safe allowing us to access them concurrently without consequences.
Intro to lenses in Swift: Immutability of objects
The Lenses
The lenses provide an elegant way to update immutable states of an object. A lens, as the name suggests, allows us to zoom in a particular part of the structure of an object
to obtain, modify or enter a new value.
We could define them as functional getters and setters.
In the implementation of the lenses in Swift we will see that we have a whole object ( Whole ) and a part of this object ( Part ), they are the equivalent of the implementation with the generic A and B .
struct Lens <Whole,Part> {
let from: (Whole) -> Part
let to: (Part, Whole) -> Whole
}
struct Lens <A,B> {
let from: (A) -> B
let to: (B, A) -> A
}
As we can see in the implementation, the getter of the lens returns a specific part of it, and the setter modifies a value and returns the whole object ( Whole ) with the new modified value, always talking about immutable objects.
Case study
It will be based on a fictional case of a library.
struct Library {
let name: String
let address: Address
let books: [Book]
}
struct Address {
let street: String
let city: String
}
struct Book {
let name: String
let isbn: String
}
First we are going to create our library object, based on the magnificent library of the city of Oporto, Livraria Lello , which served as an inspiration to J. K. Rowling for her Harry Potter novels.
let hp1 = Book(name: "Harry Potter and the Philosopher's Stone", isbn: "0-7475-3269-9")
let hp2 = Book(name: "Harry Potter and the Chamber of Secrets", isbn: "0-7475-3849-2")
let lelloBookstore = Library(name: "Livraria Lello",
address: Address(street: "R. das Carmelitas 144",
city: "Barcelona"),
books: [hp1,hp2])
As we can see, the location of the city of the library is wrong.
If we wanted to change the city address of the object * Address *, which in turn is part of our library object, we could not change it by directly accessing the value * city * within our object, because we are working with completely immutable objects.
lelloBookstore.address.city = "Oporto" //Compiler error
To change the value of the city we will first create the getter and setters of the library.
Getters y setters
extension Library {
func getAddress() -> Address {
return self.address
}
func setAddress(address: Address) -> Library {
return Library(name: self.name, address: address, books: self.books)
}
}
The method getAddress simply returns all the object Address and the setAddres returns an entire object of Library with the new address.
We still have to create the setters and getters of the Address object in order to access the city attribute.
extension Address {
func getCity() -> String {
return self.city
}
func setCity(city: String) -> Address {
return Address(street: self.street, city: city)
}
}
Finally, we can change the value of the city of our library.
let newLibrary = lelloBookstore.setAddress(address: lelloBookstore.getAddress().setCity(city: "Barcelona"))
print(newLibrary.getAddress().getCity()) // Will print "Barcelona"
As we can see, our code has been composed in several levels.
We easily find cases like this with multiple levels of depth in our data structures.
Lenses to the rescue
We will create a lens for the address attribute of our library, and another lens for the name attribute of the Address object.
To create lenses in Swift, we only have to indicate the types of input / output and define their getters and setters.
let addressLens = Lens<Library, Address>(
get: { library in library.address },
set: { address, library in Library(name: library.name, address: address, books: library.books) }
)
let cityLens = Lens<Address, String>(
get: { address in address.city },
set: { city, address in Address(street: address.street, city: city) }
)
Now that we have the lenses, we will use them to try to repeat the same action we have done before, change the city of the library to Barcelona.
addressLens.set(
cityLens.set("Barcelona", addressLens.get(lelloBookstore)
), lelloBookstore
)
The set of lenses returns a new library with the changed city, but our code is still less readable than using the getters and setters without lenses.
However, if we return to the code of our Lens, we can see that the output value of the first lens is the same input value of the second lens.
This gives us a clue, whenever we find cases where the output parameter of a function is the same type as the input parameter of another, we can benefit from the composition of functions.
Function composition
We are going to define a compose function that will help us compose functions.
func compose<A,B,C>(_ lhs: Lens<A, B>,_ rhs: Lens<B,C>) -> Lens<A, C> {
return Lens<A, C>(
get: { a in rhs.get(lhs.get(a)) },
set: { (c, a) in lhs.set(rhs.set(c, lhs.get(a)),a)}
)
}
Now we can create a new lens that unites the previous two.
let addressCityLens = compose(addressLens, cityLens)
Using this new lens we can directly modify the city.
let newLibrary = addressCityLens.set("Barcelona", lelloBookstore)
newLibrary.address.city // Print Barcelona
Operators
Let’s simplify it even more with the use of operators.
func * <A, B, C>(_ lhs: Lens<A, B>,_ rhs: Lens<B,C>) -> Lens<A, C> {
return compose(lhs, rhs)
}
We see how using the operator we can now join the two lenses.
(addressLens * cityLens).set("Barcelona", lelloBookstore)
Now we can move the lens code inside their respective objects using their extension, just as we did with the getters and setters.
extension Library {
static let addressLens = Lens<Library, Address>(
get: { $0.address },
set: { a, l in Library(name: l.name, address: a, books: l.books) }
)
}
We would do the same for Address, and finally we could compose our lenses where we need them to get a deeper focus on our objects.
let newLibrary = (Library.addressLens * Address.cityLens).set("Barcelona", lelloBookstore)
newLibrary.address.city //Print Barcelona
If you are interested in receiving more tips for Lenses in Swift or movile development in general, I highly recommend you to subscribe to our monthly newsletter here.
If you found this article about lenses in Swiftinteresting, you might like…
iOS Objective-C app: sucessful case study
Mobile app development trends of the year
Banco Falabella wearable case study
Viper architecture advantages for iOS apps
Java vs Kotlin: comparisons and examples
Author
-
More than 10 years on software development field, and working on mobile development since 2013. Experience creating apps from scratch and working on big applications with several dependencies. Used to work with the latest technologies and take care of architectural decisions. Able to lead small iOs teams, always from the tech side.
View all posts