The last feature I have implemented on the NowTV app is a cache that stores a complex data structure, basically a subset of a JSON response containing multiple and nested models.
Let’s consider a basic JSON like the following one:
This JSON in translated in something like
Lately I’m studying theory behind Functional Programming and I was wondering how to access/update data stored in the cache in a functional way. Thanks to manub I ended up in Lenses and even though I’m not expert at all about FP, the aim of this post is just to share what I learned, from a lens newbie perspective :)
As we know, in the real world, a Lens is a tool to focus into a part of an image, used alone or in combination with other Lenses (i.e. think to a telescope).
In FP, a Lens is a first-class value that let you having a partial view (just referred as view V) on the internal part of a complex data structure (container C) and it could be combined with other Lenses to create more complex queries over the container.
Basically a Lens is a functional and combinable getter/setter over a complex data structure.
Lenses have been called “jQuery for data types”: they give you a way to poke around in the guts of some large aggregate structure. Lenses compose (so you can make big lenses by gluing together little ones), and they exploit type classes to allow a remarkable degree of flexibility (get, set, fold, traverse, etc). (*)
A Lens on a data type [C, V] is made up by:
get(V): C => V
set(V, C): [V, C] => C
The meaning of these two functions is that you can query on the container C to get a view V and when you set a new value for a particular V in C, you get an updated copy of C (this is one of the FP core concepts: immutability).
Every Lens<C, V> should satisfy these three rules:
1. Lens.get(Lens.set(Va, C)) == Va2. Lens.set(Lens.get(Va), C) == C3. Lens.set(Vb, Lens.set(Va, C)) == Lens.set(Vb, C)
- set a value followed by a get always returns the value previously set;
- getting a view from the container and setting it again returns the same container;
- applying N consecutive set functions on C is equal to set the last value on the initial C;
In this way we have at least a baseline to develop our Lenses without side effects.
What we want to implement is a data structure that implements the get/set previously defined as
get(V): C => V
set(V, C): [V, C] => C
That is easily translated in the following struct:
get and set are two computed properties that precisely define our functions.
Now what we’d need is the concrete and accessibile of a Lens for every attribute we want to inspect/update (that will be used later in downloaded models via JSON):
Now let’s implement one Lens to expose actors name and one Lens to focus on actors included in a movie:
What should look clear from the two extensions just implemented is that the getter focus on a specific field in a really expressive way (i.e. Actor.Lenses.name) and that the setter always return a copy of the data structure.
The Lenses are the used in the following way:
This implementation satisfies two of three rules previously described:
1. set a value followed by a get always returns the value previously set;
let actor01 = Actor(name: "Ennio", surname: "Masi")Actor.Lenses.name.get(Actor.Lenses.name.set(actor01, "Pippo Foo")) // → Pippo Foo
2. getting a value and setting it again returns the set value itself;
let actor01 = Actor(name: "Ennio", surname: "Masi")Actor.Lenses.name.set(Actor.Lenses.name.get(a), a).name
// → "Ennio"
3. applying N consecutive set functions on C is equal to set the last value on the initial C;
For this latest criteria we need another function within the Lens: compose.
The idea behind a compose function is that apply Lens(A, B) & Lens (B, C) is equal to apply Lens (A, C).
get(A, B) => get(B, C) = get(A, C)
set(A, B) => set(B, C) = set(A, C)
Let’s create the compose operator as an infix swift operator:
This could look weird but basically we have implemented two functions:
get<A, B, C>((A,B), (B,C)) = get(get(A,B))1. get(A, B) → B2. get(get(A, B)) → C
In this way we are going to create a pipe of operations using two Lenses and we can use it in the following way:
Through the Lens updatedMovie we have been able to update the actor surname (1 level deep) with a single call, nice no? :)
Probably what I’ve explained in this article just cover a minimal part of the theories about Lenses but I hope that I’ve been clear explaining the abstraction provided by them.
The brilliant idea is to create a toolkit to focus on a (or more than one) part of a larger data structure, providing an elegant way to query over this structure using the algebra for compose these Lenses.
The cons about Lenses in Swift is definitely the boilerplate code related to Lenses implementation.
Thanks for reading! 👽