DEV Community

Apiumhub
Apiumhub

Posted on • Originally published at apiumhub.com on

Intro to lenses in Swift: Immutability of objects

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…

Functional Javascript: Lenses

iOS snapshot testing

iOS Objective-C app: sucessful case study

Mobile app development trends of the year

Banco Falabella wearable case study

Mobile development projects

Viper architecture advantages for iOS apps

Why Kotlin ?

Software architecture meetups

Pure MVP in Android

Java vs Kotlin: comparisons and examples


The post Intro to lenses in Swift: Immutability of objects appeared first on Apiumhub.

Top comments (0)