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.
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.
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.
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:
Recibe actualizaciones de los últimos descubrimientos tecnológicos
Acerca de Apiumhub
Apiumhub reúne a una comunidad de desarrolladores y arquitectos de software para ayudarte a transformar tu idea en un producto potente y escalable. Nuestro Tech Hub se especializa en Arquitectura de Software, Desarrollo Web & Desarrollo de Aplicaciones Móviles. Aquí compartimos con usted consejos de la industria & mejores prácticas, basadas en nuestra experiencia.