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.
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 :
Exercise1
There is small "Meetup" domain
- Meetup
- Topic
- Date
- Participants : List[Members]
- Member
- Name
- History : List[Topics]
- Member
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"