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.

DEMO FILE : https://github.com/PawelWlodarski/workshops/blob/master/src/main/scala/jug/lodz/workshops/modeling/modification/LensesDemo.scala

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.

Problem

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.

//WE WANT TO CHANGE CUSTOMERS CITY
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"))))
solution

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

purchaseCustomerLens.composeLens(customerAddressLens)

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](_.city)

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

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

ISO

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:${c.name}}"){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

Traversable

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

eachStudent.getAll(school)

Exercises

EXERCISES :

https://github.com/PawelWlodarski/workshops/blob/master/src/test/scala/jug/lodz/workshops/modeling/modification/exercises/LensesExercise.scala

Exercise1

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)

Exercise2

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 ""