Typical general case class updates in Scala

I am trying to write code that tracks changes to records and applies them later. In a dynamic language, I would do this by simply saving the list [List [(String, Any)] pairs log and then simply applying them as an update to the original record when I finally decide to commit the changes.

I need to be able to analyze updates, so the list of update functions is not suitable.

In Scala, this is pretty trivial using reflection, however I would like to implement a safe version.

My first attempt was to try with the formless. This works well if we know specific types.

import shapeless._ import record._ import syntax.singleton._ case class Person(name:String, age:Int) val bob = Person("Bob", 31) val gen = LabelledGeneric[Person] val updated = gen.from( gen.to(bob) + ('age ->> 32) ) // Result: Person("Bob", 32) 

However, I can’t understand how to make this work as a whole.

 trait Record[T] def update( ??? ):T } 

Given how shapeless does this, I'm not sure if this is even possible?

If I accept a lot of patterns, as an option with a poor mother, I could do something in the following order.

 object Contact { sealed trait Field[T] case object Name extends Field[String] case object Age extends Field[Int] } // A typeclass would be cleaner, but too verbose for this simple example. case class Contact(...) extends Record[Contact, Contact.Field] { def update[T]( field:Contact.Field[T], value:T ) = field match { case Contact.Name => contact.copy( name = value ) case Contact.Age => contact.copy( age = value ) } } 

However, this is not particularly elegant and requires a large number of templates. Perhaps I could write my own macro to handle this, however this seems like a pretty common thing - is there a way to handle this using Shapeless or a similar macro library already?

+5
source share
1 answer

How to use the whole instance of the class as an update?

 case class Contact(name: String, age: Int) case class ContactUpdate(name: Option[String] = None, age: Option[Int] = None) object Contact { update(target: Contact, delta: ContactUpdate) = Contact( delta.name.getOrElse(target.name) target.age.getOrElse(delta.age) ) } // also, optionally this: object ContactUpdate { apply(name: String) = ContactUpdate(name = Option(name)) apply(age: Int) = ContactUpdate(age = Option(age)) } 

I think that if you want a truly type-safe solution, this is the cleanest and most readable, and possibly the least pain to implement, since you don’t have to deal with records, lenses and separate field descriptors, just ContactUpdate(name="foo") creates the update, and updates.map(Contact.update(target, _)) applies them all sequentially.

+1
source

Source: https://habr.com/ru/post/1210247/


All Articles