Functional references: Lens and other Optics in Scala

If we were to implement this functionality in an imperative object oriented way we could, for example, make number a mutable parameter:case class Street(name: String, var number: Int)case class Address(country: String, city: String, street: Street) { def changeStreetNumber(int: Int): Unit = { this.street.number = int }}Scala though offers a function called copy to modify the parameters value inside a case class..This function doesn’t mutate the referred value, instead it creates a new object :case class Street(name: String, number: Int)case class Address(country: String, city: String, street: Street) { def changeStreetNumber(int: Int): Address = this.copy( street = street.copy( number = int ) )}With a functional programming mindset we could then decide to separate the creation of the object from its functionality and move the changeStreetNumber function outside of the class scope:case class Street(name: String, number: Int)case class Address(country: String, city: String, street: Street) def changeStreetNumber(address: Address, int: Int): Address = address.copy( street = address.street.copy( number = int ))But let’s suppose that we have to change a more deeply nested object:case class Street(name: String, number: Int)case class Address(country: String, city: String, street: Street)case class User(id: Long, address: Address)case class Account(id: Long, user: User, isActive: Boolean)def changeStreetNumber(account: Account, int: Int): Account = account.copy( user = account.user.copy( address = account.user.address.copy( street = account.user.address.street.copy( number = int ) ) ) )The greater the level of nesting of the objects, the less readable the syntax becomes.Introducing LensesLet’s take a step backwards and have a look to what we’re trying to achieve here.A computer program that accesses data is said a reference..Using mutable variables we make implicit use of references..Indeed the reference cells can hold any value and are of reference type a ref, where a is to be replaced with the type of value pointed to..If the reference is mutable, it can be pointed to different objects..An example of mutable reference in imperative programming languages are pointers.In functional programming languages, in order to enforce immutability, other data structures are used in place of pointers — even if the compiler under the hood still uses them.As the School of Haskell points out:A lens is a first-class reference to a subpart of some data type.We can define a Lens as follows:case class Lens[A, B]( get: A => B, set: (A, B) => A)In a less formal way, we can then describe the Lens as a group of functions, set and get, that allows us to manipulate data inside a class.We have now a data structure that allows us to easily update the street number:val streetNumberLens = Lens[Street, Int]( get = _.number, set = (a, b) => a.copy(number = b))val bigStreet = Street("Unter den Linden", 3)streetNumberLens.get(bigStreet)//res0: Int = 3streetNumberLens.set(bigStreet, 9)//res1: Street = Street(Unter den Linden,9)So far so good, but besides giving us a better syntax and a more functional way to get and set a value in a case class, Lenses don’t seem to provide much.Where is then the advantage of using a Lens in place of a nested copy function, if every time we have to create a new Lens?.Here is the thing: we don’t have to.Debasish Ghosh, in in the book Functional And Reactive Domain Modeling, defined a compose function that allows us to put together Lenses and reuse code:def compose[Outer, Inner, Value]( outer: Lens[Outer, Inner], inner: Lens[Inner, Value]) = Lens[Outer, Value]( get = outer.get andThen inner.get, set = (obj, value) => outer.set(obj, inner.set(outer.get(obj), value)))This is a powerful feature that allows us to create new Lenses from existing ones in a modular way.val addressStreetLens = Lens[Address, Street]( get = _.street, set = (a, b) => a.copy(street = b))val addressStreetNumberLens: Lens[Address, Int] = compose(addressStreetLens, streetNumberLens)So basically we can imagine that a Lens is like an instance of a function — or to be more accurate it’s an instance of a profunctor, a generalization of function.In the Profunctor Optics Modular Data Accessors paper is indeed introduced in this way:Any data accessor for a component of a data structure is ‘function-like’, in the sense that reading ‘consumes’ the component from the data structure and writing ‘produces’ an updated component to put back into the data structure..The type structure of such function-like things — henceforth transformers — is technically known as a profunctor.In a notation that is not 100% accurate we could say that Lens[A,B] ~ A => B composed with this other Lens[B,C] ~ B => C gives Lens[A,C] ~ A => CLens LawsA Lens is expected to satisfy general laws:Identity — If you get and then set back with the same value, the object remains identical:def getSet[S, A](lens: Lens[S, A], s: S): Boolean = lens.set(s, lens.get(s)) == sRetention — If you set with a value and then perform a get, you get the same value back:def setGet[S, A](lens: Lens[S, A], s: S, a: A): Boolean = lens.get(lens.set(s, a)) == aDouble set — If you set twice in succession and then perform a get, you get back the last set value:def putPut[S, A](lens: Lens[S, A], s: S, a: A, b: A): Boolean = lens.get(lens.set(lens.set(s, a), b)) == bBeyond Lenses: OpticsLenses are not the only functional references we can think of..Generalizations of Lenses are called Optics.As described in the Monocle documentation — where Monocle is a Scala library for Optics:Optics are a group of purely functional abstractions to manipulate (get, set, modify, …) immutable objects.What if we want to manipulate data inside a trait , in general referred as a sum type or coproduct?.Prisms come in handy..They’re like Lenses but for sum types.//this is a simplification of Prismcase class Prism[S, A](_getOption: S => Option[A])(_reverseGet: A => S) { def getOption(s: S): Option[A] = _getOption(s) def reverseGet(a: A): S = _reverseGet(a)}val petPrism = Prism[Pet, String]{ case Dog(n) => Some(n) case _ => None}(name => Dog(name))petPrism.getOption(Dog("Santa's Little Helper"))res0: Option[String] = Some(Santa's Little Helper)petPrism.reverseGet("Santa's Little Helper")res1: Pet = Dog(Santa's Little Helper)There is a generalization of Prism in case the object of type A may not exist, it’s called Optional.//this is a simplification of Optionalcase class Optional[S, A](_getOption: S => Option[A])(_set: A => S => S){ def getOption(s: S): Option[A] = _getOption(s) def set(a: A): S => S = _set(a)}sealed trait Boxcase class Present(quantity: Int) extends Boxcase object NoPresent extends Boxval maybePresents = Optional[Box, Int] { case present: Present => Some(present.quantity) case _ => None} { numberOfPresents => box => box match { case present: Present => present.copy(quantity = numberOfPresents) case _ => box }}maybePresents.getOption(Present(3))res0: Option[Int] = Some(3)maybePresents.set(9)res1: Box => Box = <function>Unlike Prism when using set on Optional we lose information: we don’t have enough information to go back to S without additional argument.We could go a step further and extend the logic behind Optional to traversable datatypes, such as List or Tree : in this case we would need an optic called Traversal..More on how a Traversal works can be found here.When to use Optics?We’ve already seen that one possible use case would be in case of deeply nested objects.. More details

Leave a Reply