Para entender las lentes en Swift, primero debemos repasar a qué nos referimos cuando hablamos de inmutabilidad de un objeto.

Entendemos como objeto inmutable aquel que no puede ser modificado una vez creado.

Su uso nos aporta grandes ventajas como la fiabilidad de que nuestro objeto no haya sufrido cambios a lo largo de la ejecución del programa, así como su cualidad de thread-safe permitiéndonos acceder a ellos de manera concurrente sin consecuencias. 

 

Las Lentes en Swift: inmutabilidad de objetos

 

Las Lentes

Las lentes nos proporcionan una manera elegante de actualizar estados inmutables de un objeto. Una lente, como su nombre indica, nos permite hace zoom en una parte concreta de la estructura de un objeto para obtener, modificar o introducir un nuevo valor.

Podríamos definirlas como getters y setters funcionales.

En la implementación de las lentes veremos que tenemos un objeto entero (Whole) y una parte de este objeto (Part), son el equivalente de la implementación con los genéricos A y 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
}

Como podemos ver en la implementación, el getter de la lente devuelve una parte concreta de esta, y el setter modifica un valor y retorna el objeto entero ( Whole ) con el nuevo valor modificado, siempre hablando de objetos inmutables.

 

Caso práctico

Nos vamos a basar en un caso ficticio de una librería.


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
}

Primero vamos a crear nuestro objeto librería, basándonos en la magnifica librería de la ciudad de Oporto, Livraria Lello, la cual sirvió de inspiración a J. K. Rowling para sus novelas de Harry Potter.


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])

Como vemos, la ubicación de la ciudad de la librería es errónea.

Si quisiéramos cambiar la dirección de la ciudad del objeto Address, que a su vez forma parte de nuestro objeto librería, no podríamos mutarlo accediendo directamente al valor city dentro de nuestro objeto, debido a que estamos trabajando con objetos completamente inmutables.


lelloBookstore.address.city = "Oporto" //Compiler error


Para cambiar el valor de la ciudad vamos a crear primero los getter y setters de la librería.

 

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)
    }
}


El método getAddress simplemente nos devuelve todo el objeto Address y el setAddres nos devuelve un objeto entero de Library con el nuevo address.

Aún nos queda crear los setters y getters del objeto de Address para poder acceder al atributo de ciudad.


extension Address {
    
    func getCity() -> String {
        return self.city
    }
    
    func setCity(city: String) -> Address {
        return Address(street: self.street, city: city)
    }
}

Finalmente podemos cambiar el valor de la ciudad de nuestra librería.


let newLibrary = lelloBookstore.setAddress(address: lelloBookstore.getAddress().setCity(city: "Barcelona"))
        
print(newLibrary.getAddress().getCity()) // Will print "Barcelona"

Como podemos ver, nuestro código ha quedado compuesto en varios niveles.

Fácilmente nos encontramos con casos como este con múltiples niveles de profundidad en nuestras estructuras de datos.

 

Lentes al rescate

Vamos a crear una lente para el atributo address de nuestra librería, y otra lente para el atributo nombre del objeto Address.

Para crear las lentes en Swift, solamente deberemos indicar los tipos de entrada/salida y definir sus getters y 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) }
) 

Ahora que tenemos las lentes, vamos a usarlas para intentar repetir la misma acción que hemos realizado antes, cambiar la ciudad de la librería a Barcelona.


addressLens.set(
    cityLens.set("Barcelona", addressLens.get(lelloBookstore)
    ), lelloBookstore
)

El conjunto de lentes devuelven una nueva librería con la ciudad cambiada, pero nuestro código aún es menos legible que usando los getters y setters sin lentes.

Sin embargo si volvemos al código de nuestras Lens, podemos ver que el valor de salida de la primera lente, es el mismo valor de entrada de la segunda lente.

Esto nos da una pista, siempre que nos encontramos con casos donde el parámetro de salida de una función es del mismo tipo que el parámetro de entrada de otra, podemos ayudáranos de la composición de funciones.

 

Function composition

Vamos a definir una función compose que nos ayudará a componer funciones.


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)}
    )
}

Ahora podemos crear una nueva lente que unifique las dos anteriores.


let addressCityLens = compose(addressLens, cityLens)

Usando esta nueva lente podemos modificar directamente la ciudad.


let newLibrary = addressCityLens.set("Barcelona", lelloBookstore)
newLibrary.address.city // Print Barcelona

 

Operators

Vamos a simplificarlo aún más con el uso de operadores.


func * <A, B, C>(_ lhs: Lens<A, B>,_ rhs: Lens<B,C>) -> Lens<A, C> {
    return compose(lhs, rhs)
}

Vemos como usando el operador ahora podemos unir las dos lentes.


(addressLens * cityLens).set("Barcelona", lelloBookstore)

Ahora ya podemos mover el código de las lentes dentro de sus respectivos objetos usando su extensión, tal y como lo hicimos con los getters y 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) }
    )
}

Haríamos lo mismo para Address, y finalmente podríamos componer nuestras lentes allá dónde las necesitemos para obtener un enfoque más profundo a nuestros objetos.


let newLibrary = (Library.addressLens * Address.cityLens).set("Barcelona", lelloBookstore)
newLibrary.address.city //Print Barcelona

 

Suscríbete a nuestro newsletter para estar al día de desarrollo de aplicaciones móviles e lentes en Swift en concreto! 

 

Si este artículo sobre Lentes en Swift te gustó, te puede interesar:

 

Functional Javascript: Lenses

iOS Snapshot tests

F-Bound en Scala

Tendencias en aplicaciones móviles

Patrón MVP en iOS

Debugging con Charles Proxy en Android emulator

Por qué Kotlin?   

Integración Continua en iOS usando Fastlane y Jenkins  

Cornerjob – iOS objective-C app un caso de éxito