Reputation: 195
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
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:
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 LabelledGenericsa
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 .toUpperCase
ing itType.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 yourselfIt 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:
String
to an actual field nameThe 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