Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type-safe generic case class updates in Scala

I'm attempting to write some code that tracks changes to a record and applies them at a later date. In a dynamic language I'd do this by simply keeping a log of List[(String, Any)] pairs, and then simply applying these as an update to the original record when I finally decide to commit the changes.

I need to be able to introspect over the updates, so a list of update functions isn't appropriate.

In Scala this is fairly trivial using reflection, however I'd like to implement a type-safe version.

My first attempt was to try with shapeless. 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 figure out how to make this work generically.

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

Given the way shapeless handles this, I'm not sure if this would even be possible?

If I accept a lot of boilerplate, as a poor mans version I could do something along the lines of the following.

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 isn't particularly elegant and requires a lot of boilerplate. I could probably write my own macro to handle this, however it seems like a fairly common thing - is there a way to handle this with Shapeless or a similar macro library already?

like image 956
James Davies Avatar asked Nov 09 '22 21:11

James Davies


1 Answers

How about using 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, if you want the really type-safe solution, this is the cleanest and most readable, and also, possibly the least pain to implement, as you don't need to deal with Records, lenses and individual field descriptors, just ContactUpdate(name="foo") creates an update, and updates.map(Contact.update(target, _)) applies them all in sequence.

like image 177
Dima Avatar answered Nov 15 '22 13:11

Dima