Functional Lenses: an exploration in Swift

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:

{
"movies": [{
"title": "Total Recall",
"year": "1990",
"actors": [{
"name": "Arnold",
"surname": "Schwarzenegger"
}, {
"name": "Sharon",
"surname": "Stone"
}]
}, {
"title": "From Dusk till Dawn",
"year": "1996",
"actors": [{
"name": "George",
"surname": "Clooney"
}, {
"name": "Quentin",
"surname": "Tarantino"
}]
}]
}

This JSON in translated in something like

struct Actor {
let name: String
let surname: String
}
struct Movie {
let name: String
let year: String
let actors: [Actor]
}

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

Functional Lenses

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)) == Va
2. Lens.set(Lens.get(Va), C) == C
3. Lens.set(Vb, Lens.set(Va, C)) == Lens.set(Vb, C)
  1. set a value followed by a get always returns the value previously set;
  2. getting a view from the container and setting it again returns the same container;
  3. 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.

Swift implementation

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:

public struct Lens<Container, View> {
  public let get: (Container) -> View
public let set: (Container, View) -> Container
}

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:

extension Actor {
struct Lenses {
static let name = Lens<Actor, String>(
get: { $0.name },
set: { (value, me) in me.name = value }
)
}
}
extension Movie {
struct Lenses {
static let actors = Lens<Movie, [Actor]>(
get: { $0.actors },
set: { (value, me) in me.actors = value) }
)
}
}

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:

let actor01 = Actor(name: "Sharon", surname: "Stone")
let actor02 = Actor(name: "George", surname: "Clooney")
let movie = Movie(name: "movie01", year: "1995", actors: [actor01, actor02])
Movie.Lenses.actors.get(movie) // → ["Sharon Stone", "George Clooney"]
Movie.Lenses.actors.set(movie, actors.filter { $0.name == "Sharon" }) // → ["Sharon Stone"]

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)

Compose operator

Let’s create the compose operator as an infix swift operator:

precedencegroup ComposePrecedence {
  associativity: left
}
infix operator >>> : ComposePrecedence
func >>> <Container, View, Subview>(lhs: Lens<Container, View>, rhs: Lens<View, Subview>) -> Lens<Container, Subview> {
  return Lens(
get: { rhs.get(lhs.get($0)) },
set: { (c, a) in return lhs.set(rhs.set(c, lhs.get(a)), a) }
)
}

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) → B
2. 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:

let a = Actor(name: "George", surname: "Clooney")
let m = Movie(title: "A beautiful movie", actor: a)
print(m.actor) // George Clooney
print(m.title) // A beautiful movie
let l3 = Movie.Lenses.actor >>> Actor.Lenses.surname
let m2 = l3.set("Pitt", m)
print(m2.actor) // George Pitt 🤖
print(m2.title) // A Beautiful movie

Through the usage of the Lens l3 we have been able to update the actor name (1 level deep) with a single call, nice no? :)

Conclusion

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.

As I told you at the beginning of the article, this is just a primer about the amazing Lenses, for sure the next steps will be to investigate Fold and Traversal.