Model Manipulation

In this workshops we will learn about Functional Programming mechanism called Optics. It allows to compose separated operations on particular fields in complex data model.


Domain in Demo

For demo purposes we are going to use still trivial but relatively complex domain model which _main root _will be Purchase.

It has a list of products (this will be important for Traversable example) also we have a Customer which made the purchase which also consist of some simpler data pieces.


Imagine that we have already created data model. We want to change only one thing - _a city of a customer . _The problem is that to do this we need to copy full structure : first address in a Customer and then update Customer inside Purchase.

And we don't want to mutate state because it leads to many problems like errors during concurrent access.

val newAddress=a.copy(city = new City("Zgierz"))
val newCustomer=c.copy(address = c.address.copy(city = new City("Zgierz")))
val newPurchase=purchase.copy(customer = purchase.customer.copy(address = purchase.customer.address.copy(city = new City("Zgierz"))))

There is a solution to this problem in Functional Programming - concept called Optics . At first let's look at Lenses - a specific mechanism from optics. Lenses allow us to zoom into data - and it is important to understand that it is : "Zoom as a concept", "Zoom abstraction" or "zoom as a reusable mechanism" - in other words you can reuse once defined zoom and what is more important - what we are going to see later - you can compose different lenses to have better zoom on internal structure.

First lets create two lenses for customer in purchase and address in customer

//Second function is a setter internal=>external=>change_external
val purchaseCustomerLens=Lens[Purchase,Customer](_.customer)(c => p => p.copy(customer=c))

//and now similarly customer address
val customerAddressLens=Lens[Customer,Address](_.address)(a => c => c.copy(address = a))

And now because inner type of the first lens is the same as outer of the second one so we can compose them easily


Now we saw that crating both lenses looked were similar. For this mechanical and predictable code there is a macro available which creates this code automatically.

//looks very similar and mundane - use macro!
val addressCityLens=GenLens[Address](

//bigger composition
val cityInPurchase=purchaseCustomerLens.composeLens(customerAddressLens).composeLens(addressCityLens)

//modification of embedded structure
cityInPurchase.set(new City("DirectCity"))(purchase)
cityInPurchase.modify(old=>new City(":modified"))(purchase)


While Lenses allow us to zoom into inner data structures - Iso (from Isomorphism) gives use mechanics to transform given data.

Because both are part of one library _Monocle _so Iso composes nicely with lenses

//Iso example of translation domain class into json structure
val cityJsonIso = Iso[City,String](c => s"{city:${}}"){json =>
      val cityName=json.substring(6,json.length)
      new City(cityName)

//composition Iso + Lenses
val isoComposed=cityInPurchase.composeIso(cityJsonIso)

isoComposed.set("{city:zakopane}")(purchase) //json structure in setter


To present concept of traversable we will use different simple Domain - a School has Classes and Class has Students.

When we have lenses generated for both individual properties

val studentsLens=GenLens[Class](_.students)
val classesLens=GenLens[School](_.classes)

then we can compose into it special construction called traversal to iterate through each element

import monocle.function.Each._
val eachStudent=classesLens composeTraversal each composeLens studentsLens composeTraversal each

what exactly is each? It is predefined traversal in monocle library

trait EachFunctions {
def each[S, A](implicit ev: Each[S, A]): Traversal[S, A] = ev.each

now we can easily get all students





There is small "Meetup" domain

  • Meetup
    • Topic
    • Date
    • Participants : List[Members]
      • Member
        • Name
        • Email
        • History : List[Topics]

Update TopicLens for meetup

lazy val topicLens: Lens[Meetup, MeetupTopic] = ???

Then write Lenses for address and street and add Iso to convert from tuple to Street

lazy val addressLens: Lens[Meetup, Address] = ???
lazy val streetLens: Lens[Address, Street] = ???
lazy val streetTupleIso: Iso[Street, (String, Int)] = ???

so that composition is possible

val streetToTuple= addressLens composeLens streetLens composeIso streetTupleIso

finally create Traversal for each participant so it will be very easy to update each list of topics

lazy val participantsTraversal: Traversal[Meetup, List[MeetupTopic]]
val changed=participantsTraversal.modify(m.topic::_)(m)


Create custom trivial Lens implementation

case class CustomLens[Outer,Inner](getter:Outer=>Inner,setter:Inner => Outer => Outer){
    def composeCustomLens[Value](furtherLens:CustomLens[Inner,Value]) : CustomLens[Outer,Value] = ???

so the following code will work

lazy val customTopicLens= CustomLens[Meetup,MeetupTopic](_.topic,t=>m => m.copy(topic = t))
customTopicLens.getter(meetup) mustBe "Scala DDD"

results matching ""

    No results matching ""