ksceriath
ksceriath

Reputation: 195

Updating case classes from field-name strings and values

Is there a less-verbose way to achieve this?


case class MyClass(
    a: A,
    b: B,
    c: C,
    ...
)

def updatedFromString(m: MyClass, field: String, value: String) = field match {
    case "A" => m.withA(value)
    case "B" => m.withB(value)
    case "C" => m.withC(value)
    ...
  }

implicit class FromStrings(m: MyClass) {
  def withA(v: String) = m.copy(a = A.fromString(v))
  def withB(v: String) = m.copy(b = B.fromString(v))
  def withC(v: String) = m.copy(c = C.fromString(v))
  ...
}

MyClass has a lot of fields - a,b,c, etc - all of which are instances of different case classes. This leads to a lot of case statements above and a lot of updater methods named withXXX, which look fairly repetitive.

Upvotes: 2

Views: 998

Answers (1)

Mateusz Kubuszok
Mateusz Kubuszok

Reputation: 27535

You could extract the logic:

// repetitive and generic enough to make it easier to generate
val setters: Map[String, String => MyClass => MyClass] = Map(
  "A" -> (v => _.copy(a => A.fromString(v)),
  "B" -> (v => _.copy(b => B.fromString(v)),
  "C" -> (v => _.copy(c => C.fromString(v)),
  ...
)

def updatedFromString(m: MyClass, field: String, value: String) = 
  setters(field)(value)(m)

If it is still too much, you could generate setters using macros or runtime reflection, but I am not sure it is worth the effort.

EDIT: An alternative solution which changes how you deal with code:

sealed trait PatchedField
object PatchedField {
  // field names corresponding to names from MyClass 
  case class FieldA(a: A) extends PatchedField
  case class FieldB(b: B) extends PatchedField
  ...
}

// removed stringiness and creates some type-level information
def parseKV(key: String, value: String): PatchedField = key match {
  case "A" => FieldA(A.fromString(v))
  case "B" => FieldB(B.fromString(v))
  ...
}

import io.scalaland.chimney.dls._

def updatedFromString(m: MyClass, field: String, value: String) =
  parse(field, value) match {
    // performs exhaustivity check
    case fieldA: FieldA => m.patchUsing(fieldA)
    case fieldB: FieldB => m.patchUsing(fieldB)
    ...
  }

If you don't like it... well, then you have to write you own macro, very obscure shapeless and/or codegen:

  • there is no way you can generate x.copy(y = z) without a macro, even if some library does it, it does it using a macro underneath. At best you could use some lens library, but AFAIK no lens library out of the box would provide you a Setter for a field by singleton type of the field name (that is without writing something like Lens[Type](_.field) explicitly) - that I believe would be doable with some Shapeless black magic mapping over LabelledGenerics
  • you might still need to convert a singleton type into A singleton type in compile time - that I am not sure if it is possible in Shapeless so you might need to push it down to value level, summoning a Witness, and then .toUpperCaseing it
  • you would have to make each field aware of Type.fromString functionality - is it different for each field by its name or my its type? If the latter - you could use a normal parser typeclass. If the former, this typeclass would have to be dependently typed for a singleton type with a field name. Either way you would most likely have to define these typeclasses yourself
  • then you would have to combine all of that together

It could be easier if you did it in runtime (scanning classes for some method and fields) instead of compile time... but you would have no checks that conversion actually exists for a field string to its value, type erasure would kick in (Option[Int] and Option[String] being the same thing, null no being anything).

With compile time approach you would have to at least define a typeclass per type, and then manually create the code that would put it all together. With some fast prototyping I arrived at:

import shapeless._
import shapeless.labelled._

trait StringParser[A] { def parse(string: String): A }
object StringParser {
  implicit val string: StringParser[String] = s => s
  implicit val int:    StringParser[Int]    = s => java.lang.Integer.parseInt(s).toInt
  implicit val double: StringParser[Double] = s => java.lang.Double.parseDouble(s).toDouble
  // ...
}


trait Mapper[X] { def mapper(): Map[String, StringParser[_]] }
object Mapper {

  implicit val hnilMapper: Mapper[HNil] = () => Map.empty

  implicit def consMapper[K <: Symbol, H, Repr <: HList](
    implicit
    key: Witness.Aux[K],
    parser: StringParser[H],
    mapper: Mapper[Repr]
  ): Mapper[FieldType[K, H] :: Repr] = () => mapper.mapper() + (key.value.name -> (parser : StringParser[_]))

  implicit def hlistMapper[T, Repr <: HList](
    implicit gen: LabelledGeneric.Aux[T, Repr],
    mapper: Mapper[Repr]
  ): Mapper[T] = () => mapper.mapper()

  def apply[T](implicit mapper: Mapper[T]): Map[String, StringParser[_]] = mapper.mapper()
}

val mappers = Mapper[MyClass]

Which you could use like:

  • convert a field String to an actual field name
  • extract a parser from the map using field name
  • pass the value to the parser
  • use runtime reflection to simulate copy or generate copy calls using macros

The last part simply cannot be done "magically" - as far as I am aware, there is no library where you would require an implicit Lens[Type, fieldName] and obtain Lens[Type, fieldName] { type Input; def setter(input: Input): Type => Type }, so there is nothing which would generate that .copy for you. As a result it would require some form of manually written reflection.

If you want to have compile-time safety at this step, you might as well do the rest compile-time safe as well and implement everything as a macro which verifies the presence of the right typeclasses and things.

Upvotes: 2

Related Questions