Unveiling the Magic

Chimney’s Internals, Macros & Scala 3 Transition

Mateusz Kubuszok

About me

Agenda

  • what are Chimney’s features

  • what kind of code are we generating

  • goals for the migration to Scala 2+3

  • research and the current state

Chimney’s features

case class conversions

import io.scalaland.chimney.dsl.* // assume this from now on
case class Foo(a: Int, b: Double)
case class Bar(a: Int, b: Double) // same fields

Foo(1, 2.0).transformInto[Bar] // Bar(1, 2.0)
case class Foo(a: Int, b: Double)
case class Bar(a: Int, c: Double) // c instead of b

Foo(1, 2.0).into[Bar]
  .withFieldRenamed(_.b, _.c)
  .transform // Bar(1, 2.0)
case class Foo(a: Int, b: Double)
case class Bar(a: Int, b: Double, c: String) // extra field

Foo(1, 2.0).into[Bar]
  .withFieldConst(_.c, "test")
  .transform // Bar(1, 2.0, "test")
case class Foo(a: Int, b: Double)
case class Bar(a: Int, b: Double, c: String) // extra field

Foo(1, 2.0).into[Bar]
  .withFieldComputed(_.c, foo => foo.toString)
  .transform // Bar(1, 2.0, "Foo(1, 2.0)")
case class Foo(a: Int, b: Double)
case class Bar(a: Int, b: Double, c: String = "c") // ditto

Foo(1, 2.0).into[Bar]
  .enableDefaultValues
  .transform // Bar(1, 2.0, "c")
case class Foo(a: Int, b: Double)
case class Bar(a: Int, b: Double, c: Option[String])//ditto

Foo(1, 2.0).into[Bar]
  .enableOptionDefaultsToNone
  .transform // Bar(1, 2.0, None)

Tuples conversions

case class Foo(a: Int, b: Double)

Foo(1, 2.0).transformInto[(Int, Double)] // (1, 2.0, None)
(1, 2.0).transformInto[Foo] // Foo(1, 2.0, None)

Java Beans conversions

case class Foo(a: Int, b: Double)
class Bar private (
  @BeanProperty var a: Int,
  @BeanProperty var b: Double
) {
  def this() = this(0, 0.0)
}

Foo(1, 2.0).into[Bar].enableBeanSetters.transform

import scala.util.chaining.*
new Bar().pipe(_.setA(1)).pipe(_.setB(2.0))
  .into[Foo].enableBeanGetters.transform
class Foo {
  def a: Int = 1
}

case class Bar(a: Int)

Foo(1).into[Bar].enableMethodAccessors.transform

sealed hierarches conversions

sealed trait Foo
object Foo {
  case object A extends Foo
  case class B(b: Int) extends Foo
}

sealed trait Bar // same subtypes
object Bar {
  case object A extends Foo
  case class B(b: Int) extends Bar
}

(Foo.A : Foo).transformInto[Bar] // Bar.A
(Foo.B(1) : Foo).transformInto[Bar] // Bar.B(1)
sealed trait Foo
object Foo {
  case object A extends Foo
  case class B(b: Int) extends Foo
  case class C(c: Double) extends Foo
}
sealed trait Bar // missing Bar.C
object Bar {
  case object A extends Foo
  case class B(b: Int) extends Bar
}
(Foo.C(2.0) : Foo).into[Bar].withCoproductInstance[Foo.C] {
    case Foo.C(c) => Bar.B(c.toInt)
  }.transform // Bar.B(2)

AnyVal conversions

case class Foo(val value: Int) extends AnyVal
case class Bar(val value: Int) extends AnyVal

Foo(1).transformInto[Bar] // Bar(1)
Foo(1).transformInto[Int] // 1
1.transformInto[Bar] // Bar(1)

Implicit Transformer

class SomeType(val x: Int)
class AnotherType(val y: Int)

case class Foo(a: SomeType, b: Int)
case class Bar(a: AnotherType, b: Int)

foo.transformInto[Bar] // error: how to convert foo.a?
import io.scalaland.chimney.Transformer

implicit val someTypeToAnotherType:
   Transformer[SomeType, AnotherType] =
  st => new AnotherType(y = st.x) // single abstract method

foo.transformInto[Bar]//uses someTypeToAnotherType for foo.a

Implicit TransformerConfiguration

implicit val cfg = TransformerConfiguration.default
  .enableDefaultValues
  .enableOptionDefaultsToNone
  .enableBeanGetters
  .enableBeanSetters

// now .into.transform and .transformInto use above flags

Partial Transformers

case class Foo(a: Option[Int])
case class Bar(a: Int)

Foo(None).transformInto[Bar] // cannot handle None!
val result = Foo(None).transformIntoPartial[Bar]
result.asEither // Left(...)
result.asErrorPathMessages
  // Iterable("a" -> ErrorMessage.EmptyValue)
implicit val intToString:
    Transformer[Int, String] =
  int => int.toString

implicit val stringToInt:
    PartialTransformer[String, Int] =
  PartialTransformer { str =>
    partial.Result.fromCatching(str.toInt)
  }

case class Foo(a: Option[Int], b: String)
case class Bar(a: String, b: Int)

Foo(None, "test").transformIntoPartial[Bar]
// from is case class
from.intoPartial[To]
  // total operations
  .withFieldRenamed(_.a, _.b)
  .withFieldConst(_.c, value)
  .withFieldComputed(_.d, from => ...)
  // partial operations
  .withFieldComputedPartial(_.e, from => ...)
  .transform
// from is sealed hierarchy
from.intoPartial[To]
  // total operations
  .withCoproductInstance[To.Subtype](...)
  // partial operations
  .withCoproductInstancePartial[To.Subtype](...)
  .transform
implicit val totalInt2String:
    Transformer[String, Int] =
  str => scala.util.Try(str.toInt).getOrElse(0)

implicit val stringToInt:
    PartialTransformer[String, Int] =
  PartialTransformer { str =>
    partial.Result.fromCatching(str.toInt)
  }

case class Foo(a: String)
case class Bar(a: Int)

Foo(1).transformIntoPartial[Bar] // error: ambiguity!
implicit val cfg = TransformerConfiguration.default
 .enableImplicitConflictResolution(PreferTotalTransformer)
 /* or
 .enableImplicitConflictResolution(PreferPartialTransformer)
 */
implicit val stringToInt:
    PartialTransformer[String, Int] =
  PartialTransformer.fromFunction(_.toInt)

case class Foo(a: String, foo: Foo2)
case class Foo2(b: String)

case class Bar(a: Int, foo: Bar2)
case class Bar2(b: Int)
Foo("a", Foo2("b"))
  .transformIntoPartial[Bar]
  .asErrorPathMessageStrings
// List("a" -> "For input string: \"a\"",
//      "foo.b" -> "For input string: \"b"\")
Foo("a", Foo2("b"))
  .transformIntoPartial[Bar](failFast = true)
  .asErrorPathMessageStrings
// List("a" -> "For input string: \"a\"")

Patchers

case class Foo(a: Int, b: String, c: Double)
case class Patch(c: Double)

Foo(1, "2", 3.0).patchUsing(Patch(4.0)) // Foo(1, "2", 4.0)

Generated code

Are we generating new Transformers?

case class Foo(a: Int, b: String)
case class Bar(a: Int, b: String)

Foo(1, "b").transformInto[Bar]

Do we generate:

new Transformer {
  def transform(foo: Foo): Bar = Bar(
    Transformer.identity[Int].transform(foo.a),
    Transformer.identity[String].transform(foo.b)
  )
}.transform(Foo(1, "b"))

?

case class Foo(a: Int, b: String)
case class Bar(a: Int, b: String)

Foo(1, "b").transformInto[Bar]
val foo = Foo(1, "b")
new Bar(foo.a, foo.b)
implicit val totalInt2String:
    Transformer[String, Int] =
  str => scala.util.Try(str.toInt).getOrElse(0)

case class Foo1(a: Int, b: String, foo: Foo2)
case class Foo2(f: String)

case class Bar1(a: Int, b: String, foo: Bar2)
case class Bar2(f: Int)

Foo1(1, "b", Foo2("9")).transformInto[Bar1]
val foo1 = Foo1(1, "b", Foo2("9"))
new Bar1(
  foo1.a,
  foo1.b,
  new Bar2(totalInt2String.transform(foo1.foo.f))
)

Calling unapply?

implicit val totalInt2String: Transformer[String, Int] = ...

sealed trait Foo3
object Foo3 {
  case object A extends Foo3
  case class B(b: String) extends Foo3 }

sealed trait Bar3
object Bar3 {
  case object A extends Bar3
  case class B(b: Int) extends Bar3 }

(Foo3.B("9"): Foo3).transformInto[Bar3]
val foo1 = (Foo3.B("9"): Foo3)
foo3 match {
  case (_: Foo3.A.type) => Bar3.A
  case (b @ (_: Foo3.B)) =>
    new Bar3.B(totalInt2String.transform(b.b))
}

What happens when we customize Transformer?

case class Foo4(a: Int)
case class Bar4(a: Int, b: String, c: Double)
Foo4(1).into[Bar4]
  .withFieldConst(_.b, "test")
  .withFieldComputed(_.c, _.a.toDouble)
  .transform
val ti$1 = Foo4(1).into[Bar4] // <- stores runtime data
.__refineTransformerDefinition(td =>
td.__addOverride("test".asInstanceOf[scala.Any])
  .__refineConfig[FieldConst["b",Empty]]
)
.__refineTransformerDefinition(td =>
td.__addOverride((_.a.toDouble).asInstanceOf[scala.Any]
  .__refineConfig[FieldComputed["c",FieldConst["b",Empty]]])
)
new Transformer[Foo4, Bar4] {
def transform(foo4$1: Foo4): Bar4 = new Bar4(
 foo4$1.a,
 ti$1.td.runtimeData(1).asInstanceOf[String],
 ti$1.td.runtimeData(0).asInstanceOf[Foo4 => Double](foo4$1)
)
}.transform(ti$1.source)

What when we need recursion?

case class FooR(a: Int, b: Option[FooR])
case class BarR(a: Int, b: Option[BarR])

FooR(1, Some(FooR(2, None))).transformInto[BarR] // error!
implicit def fooBarR: Transformer[FooR, BarR] =
  fooR => fooR.transformInto[Bar] // also error!

FooR(1, Some(FooR(2, None))).transformInto[BarR]
implicit def fooBarR: Transformer[FooR, BarR] =
  Transformer.derive[FooR, BarR]

FooR(1, Some(FooR(2, None))).transformInto[BarR]
new BarR(foor.a, foor.b.map(b => fooBarR.transform(b)))

Partial Transformers combinators?

case class Foo5_0(a: Int, b: Int, c: Int)
case class Bar5(a: Int, b: Int, c: Int)

implicit val stringToInt: PartialTransformer[String, Int] =
  PartialTransformer.fromFunction[String, Int](_.toInt)

Foo5_0(1, 2, 3).transformIntoPartial[Bar5]
val foo5_0 = Foo5_0(1, 2, 3)
partial.Result.Value(new Bar5(foo5_0.a, foo5_0.b, foo5_0.c))
case class Foo5_1(a: String, b: Int, c: Int)
case class Bar5(a: Int, b: Int, c: Int)

implicit val stringToInt: PartialTransformer[String, Int] =
  PartialTransformer.fromFunction[String, Int](_.toInt)

Foo5_1("1", 2, 3).transformIntoPartial[Bar5]
val foo5_1 = Foo5_1("1", 2, 3)
stringToInt.transform(foo5_1.a)
  .prependErrorPath(partial.PathElement.Accessor("a"))
  .map((a: Int) => new Bar5(a, foo5_1.b, foo5_1.c)
case class Foo5_2(a: String, b: String, c: Int)
case class Bar5(a: Int, b: Int, c: Int)

implicit val stringToInt: PartialTransformer[String, Int] =
  PartialTransformer.fromFunction[String, Int](_.toInt)

Foo5_2("1", "2", 3).transformIntoPartial[Bar5]
val foo5_2 = Foo5_2("1", "2", 3)
partial.Result.map2[Int, Int, Bar5](
  stringToInt.transform(foo5_2.a)
    .prependErrorPath(partial.PathElement.Accessor("a")),
  stringToInt.transform(foo5_2.b)
    .prependErrorPath(partial.PathElement.Accessor("b")),
  {
    case (a: Int), (b : Int) => new Bar5(a, b, foo5_2.c)
  },
  failfast
)
case class Foo5_3(a: String, b: String, c: String)
case class Bar5(a: Int, b: Int, c: Int)

implicit val stringToInt: PartialTransformer[String, Int] =
  PartialTransformer.fromFunction[String, Int](_.toInt)

Foo5_3("1", "2", "3").transformIntoPartial[Bar5]
val foo5_3 = Foo5_3("1", "2", "3")
final def rd_a: partial.Result[Int] =
  stringToInt.transform(foo5_3.a)
    .prependErrorPath(partial.PathElement.Accessor("a"))
final def rd_b: partial.Result[Int] =
  stringToInt.transform(foo5_3.b)
     .prependErrorPath(partial.PathElement.Accessor("b"))
final def rd_c: partial.Result[Int] =
  stringToInt.transform(foo5_3.c)
    .prependErrorPath(partial.PathElement.Accessor("c"))
if (failfast)
  for {
    rvff_a <- rd_a
    rvff_b <- rd_b
    rvff_b <- rd_c
  } yield new Bar5(rvff_a, rvff_b, rvff_c)
else {
  var allerrors: partial.Result.Errors = null;
  final val rv_a = rd_a
  allerrors = partial.Result.Errors
    .__mergeResultNullable(allerrors, rv_a)
  final val rv_b = rd_b
  allerrors = partial.Result.Errors
    .__mergeResultNullable(allerrors, rv_b)
  final val rv_c = rd_c
  allerrors = partial.Result.Errors
    .__mergeResultNullable(allerrors, rv_c)
  if (allerrors == null)
    partial.Result.Value(
      new Bar5(
        rv_a.asInstanceOf[partial.Result.Value[Int]].value,
        rv_b.asInstanceOf[partial.Result.Value[Int]].value,
        rv_c.asInstanceOf[partial.Result.Value[Int]].value
      )
    )
  else
    allerrors
}
benchmarks

Migration’s goals (Chimney 0.8.0)

  • source compatibility

  • (excluding deprecated features which are removed)

  • sharing as much code generation logic between Scala 2 and Scala 3 as possible

enableUnsafeOption deprecation

case class Foo(a: Option[Int])
case class Bar(b: Int)

// this won't compile
Foo(None).transformInto[Bar]

// this is deprecated
Foo(None).into[Bar].enableUnsafeOption.transform
Foo(None).intoPartial[Bar].asOption // Option[Bar]

// alternatively, on your own risk!
// implicit def unsafeOption[A, B](
//   implicit t: Transform[A, B]
// ): Transformer[Option[A], B] = ...
// Foo(None).transformInto[Bar]

TransformerF deprecation

case class Foo(a: Option[Int])
case class Bar(b: Int)

// deprecated
Foo(None).transformIntoF[Option, Bar]
Foo(None).transformIntoF[Either[String, +*], Bar]
Foo(None).transformIntoPartial[Bar].asOption
Foo(None).transformIntoPartial[Bar].asEither

Research for Scala 3 migration

Are differences in macros the issue?

trait Common {
  type Type[A]
  type Expr[A]
  // methods working with Type[A] and Expr[A]
}
class Scala2Macro(val c: blackbox.Context) extends Common {
  final Type[A] = c.Type @@ A // tagged type
  final Expr[A] = c.Expr[A]
  // platform-specific methods implementations
}
class Scala3Macro(using q: quoted.Quotes) extends Common {
  final Type[A] = quoted.Type[A]
  final Expr[A] = quoted.Expr[A]
  // platform-specific methods implementations
}

So what is the actual issue?

  def mkTransformerBodyTree(
      To: Type,
      targets: Seq[Target],
      bodyTreeArgs: Seq[DerivedTree],
      derivationTarget: DerivationTarget
  )(
      mkTargetValueTree: Seq[Tree] => Tree
  ): DerivedTree = {
    assert(targets.size == bodyTreeArgs.size, "targets arity must correspond to the argument trees arity")

    derivationTarget match {
      case DerivationTarget.TotalTransformer =>
        assertOrAbort(
          bodyTreeArgs.forall(_.isTotalTarget),
          "All derived body trees arguments must be total in Total target derivation!"
        )
        DerivedTree.fromTotalTree(mkTargetValueTree(bodyTreeArgs.map(_.tree)))

      case pt: DerivationTarget.PartialTransformer =>
        assertOrAbort(
          bodyTreeArgs.forall(a => a.isTotalTarget || a.isPartialTarget),
          "Only Total and Partial body tree arguments are supported in Partial target derivation!"
        )

        val (totalArgs, partialArgs) = (targets zip bodyTreeArgs).partition(_._2.isTotalTarget)

        if (partialArgs.isEmpty) {
          DerivedTree.fromTotalTree(mkTargetValueTree(bodyTreeArgs.map(_.tree)))
        } else if (partialArgs.sizeIs == 1) {
          val (target, bodyTree) = partialArgs.head
          val fn = freshTermName(target.name)
          val totalArgsMap = totalArgs.map { case (target, bt) => target -> bt.tree }.toMap
          val argsMap = totalArgsMap + (target -> q"$fn")
          val updatedArgs = targets.map(argsMap)

          DerivedTree(q"${bodyTree.tree}.map { ($fn: ${target.tpe}) => ${mkTargetValueTree(updatedArgs)} }", pt)
        } else if (partialArgs.sizeIs == 2) {
          val (target0, bodyTree0) = partialArgs.head
          val (target1, bodyTree1) = partialArgs.last
          val fn0 = freshTermName(target0.name)
          val fn1 = freshTermName(target1.name)

          val totalArgsMap = totalArgs.map { case (target, bt) => target -> bt.tree }.toMap
          val argsMap = totalArgsMap + (target0 -> q"$fn0") + (target1 -> q"$fn1")
          val updatedArgs = targets.map(argsMap)

          val tree = Trees.PartialResult
            .map2(
              target0.tpe,
              target1.tpe,
              To,
              bodyTree0.tree,
              bodyTree1.tree,
              q"{ case ($fn0: ${target0.tpe}, $fn1: ${target1.tpe}) => ${mkTargetValueTree(updatedArgs)} }",
              pt.failFastTree
            )
          DerivedTree(tree, pt)
        } else {
          val totalArgsMap = totalArgs.map { case (target, bt) => target -> bt.tree }.toMap

          val partialTargets = partialArgs.map(_._1)

          val localDefNames = partialTargets.map(t => freshTermName(s"rd_${t.name}"))
          val localTreeDefs = (localDefNames zip partialArgs).map { case (dn, (target, tbt)) =>
            q"final def $dn: ${Trees.PartialResult.tpe(target.tpe)} = { ${tbt.tree} }"
          }
          val localValNames = partialTargets.map(t => freshTermName(s"rv_${t.name}"))

          // short circuit branch (fail fast)
          val succFFValIdents = partialTargets.map(t => freshTermName(s"rvff_${t.name}"))
          val succFFFqs = (succFFValIdents zip localDefNames).map { case (rvff, rd) => fq"$rvff <- $rd" }
          val succValFFTrees = succFFValIdents.map(rvff => q"$rvff")
          val patRefArgsMapFF = (partialTargets zip succValFFTrees).toMap
          val argsMapFF = totalArgsMap ++ patRefArgsMapFF
          val updatedArgsFF = targets.map(argsMapFF)

          // long circuit branch (errors accumulation)
          val succValTrees = (localValNames zip partialTargets).map { case (rv, target) =>
            q"$rv.asInstanceOf[${Trees.PartialResult.valueTpe(target.tpe)}].value"
          }
          val patRefArgsMap = (partialTargets zip succValTrees).toMap
          val argsMap = totalArgsMap ++ patRefArgsMap
          val updatedArgs = targets.map(argsMap)
          val allErrorsIdent = freshTermName("allErrors")
          val errorsCaptureTrees = (localValNames zip localDefNames).flatMap { case (rv, rd) =>
            Seq(
              q"final val $rv = $rd",
              q"""$allErrorsIdent = ${Trees.PartialErrors.mergeResultNullable(q"$allErrorsIdent", q"$rv")}"""
            )
          }

          val tree = q"""{
                ..$localTreeDefs
                if(${pt.failFastTree}) {
                  for (..$succFFFqs) yield ${mkTargetValueTree(updatedArgsFF)}
                } else {
                  var $allErrorsIdent: ${Trees.PartialErrors.tpe} = null
                  ..$errorsCaptureTrees
                  if ($allErrorsIdent == null) {
                    ${Trees.PartialResult.value(mkTargetValueTree(updatedArgs))}
                  } else {
                    $allErrorsIdent
                  }
                }
              }"""

          DerivedTree(tree, pt)
        }
    }
  }

Questions?

Thank You!