Skip to content

Commit

Permalink
Merge pull request #533 from Andrapyre/adding-dynamic-default-scala2
Browse files Browse the repository at this point in the history
[scala2] feat: adding on-demand default evaluation
  • Loading branch information
adamw authored May 23, 2024
2 parents 756dd7d + a4ebb19 commit a633965
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 9 deletions.
14 changes: 14 additions & 0 deletions core/src/main/scala/magnolia1/interface.scala
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,9 @@ trait Param[Typeclass[_], Type] extends ReadOnlyParam[Typeclass, Type] {
/** provides the default value for this parameter, as defined in the case class constructor */
def default: Option[PType]

/** provides a function to evaluate the default value for this parameter, as defined in the case class constructor */
def evaluateDefault: Option[() => PType] = None

override def toString: String = s"Param($label)"
}

Expand All @@ -322,6 +325,7 @@ object Param {
def typeName: TypeName = typeNameParam
def index: Int = idx
def repeated: Boolean = isRepeated
override def evaluateDefault: Option[() => PType] = getDefaultEvaluatorFromDefaultVal(defaultVal)
def default: Option[PType] = defaultVal.value
def typeclass: Tc[PType] = typeclassParam.value
def dereference(t: T): PType = t.asInstanceOf[Product].productElement(idx).asInstanceOf[PType]
Expand All @@ -346,6 +350,7 @@ object Param {
def typeName: TypeName = typeNameParam
def index: Int = idx
def repeated: Boolean = isRepeated
override def evaluateDefault: Option[() => PType] = getDefaultEvaluatorFromDefaultVal(defaultVal)
def default: Option[PType] = defaultVal.value
def typeclass: Tc[PType] = typeclassParam.value
def dereference(t: T): PType = t.asInstanceOf[Product].productElement(idx).asInstanceOf[PType]
Expand All @@ -370,6 +375,7 @@ object Param {
def typeName: TypeName = typeNameParam
def index: Int = 0
def repeated: Boolean = isRepeated
override def evaluateDefault: Option[() => PType] = getDefaultEvaluatorFromDefaultVal(defaultVal)
def default: Option[PType] = defaultVal.value
def typeclass: Tc[PType] = typeclassParam.value
def dereference(t: T): PType = deref(t)
Expand All @@ -394,6 +400,7 @@ object Param {
def typeName: TypeName = typeNameParam
def index: Int = 0
def repeated: Boolean = isRepeated
override def evaluateDefault: Option[() => PType] = getDefaultEvaluatorFromDefaultVal(defaultVal)
def default: Option[PType] = defaultVal.value
def typeclass: Tc[PType] = typeclassParam.value
def dereference(t: T): PType = deref(t)
Expand All @@ -402,6 +409,13 @@ object Param {
def typeAnnotationsArray: Array[Any] = typeAnnotationsArrayParam
}

private def getDefaultEvaluatorFromDefaultVal[P](defaultVal: CallByNeed[Option[P]]): Option[() => P] =
defaultVal.valueEvaluator.flatMap { evaluator =>
evaluator().fold[Option[() => P]](None) { _ =>
Some(() => evaluator().get)
}
}

}

/** [[ReadOnlyCaseClass]] represents a case class or case object. It provides access to all of the parameters of the case class, the full
Expand Down
48 changes: 42 additions & 6 deletions core/src/main/scala/magnolia1/magnolia.scala
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ object Magnolia {
${if (isValueClass) q"(t: $genericType) => t.$paramName" else q"$idx"},
$repeated,
$CallByNeedObj($ref),
..${default.toList.map(d => q"$CallByNeedObj($d)")},
..${default.toList.map(d => q"$CallByNeedObj.withValueEvaluator($d)")},
$ArrayObj(..$annotations): _root_.scala.Array[_root_.scala.Any],
$ArrayObj(..$inheritedAnnotations): _root_.scala.Array[_root_.scala.Any],
$ArrayObj(..$typeAnnotations): _root_.scala.Array[_root_.scala.Any]
Expand Down Expand Up @@ -944,11 +944,47 @@ private[magnolia1] object CompileTimeState {
}
}

object CallByNeed { def apply[A](a: => A): CallByNeed[A] = new CallByNeed(() => a) }
final class CallByNeed[+A](private[this] var eval: () => A) extends Serializable {
object CallByNeed {

/** Initializes a class that allows for suspending evaluation of a value until it is needed. Evaluation of a value via `.value` can only
* happen once. Evaluation of a value via `.valueEvaluator` will return None. For evaluating a value multiple times, please construct a
* CallByNeed via CallByNeed.withValueEvaluator(value)
*/
def apply[A](a: => A): CallByNeed[A] = new CallByNeed(() => a, () => false)

/** Initializes a class that allows for suspending evaluation of a value until it is needed. Evaluation of a value via `.value` can only
* happen once. Evaluation of a value via `.valueEvaluator.map(evaluator => evaluator())` will happen every time the evaluator is called
*/
def withValueEvaluator[A](a: => A): CallByNeed[A] = new CallByNeed(() => a, () => true)
}

// Both params are later nullified to reduce overhead and increase performance.
// The supportDynamicValueEvaluation is passed as a function so that it can be nullified. Otherwise, there is no need for the function value.
final class CallByNeed[+A] private (private[this] var eval: () => A, private var supportDynamicValueEvaluation: () => Boolean)
extends Serializable {

// This second constructor is necessary to support backwards compatibility for v1.1.9 and earlier
def this(eval: () => A) = this(eval, () => false)

val valueEvaluator: Option[() => A] = {
val finalRes = if (supportDynamicValueEvaluation()) {
val res = Some(eval.fv)
eval = null
res
} else {
None
}
supportDynamicValueEvaluation = null
finalRes
}

lazy val value: A = {
val result = eval()
eval = null
result
if (eval == null) {
valueEvaluator.get.fv()
} else {
val result = eval()
eval = null
result
}
}
}
24 changes: 22 additions & 2 deletions examples/src/main/scala/magnolia1/examples/default.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import magnolia1.{CaseClass, Magnolia, SealedTrait}
import scala.language.experimental.macros

/** typeclass for providing a default value for a particular type */
trait HasDefault[T] { def defaultValue: Either[String, T] }
trait HasDefault[T] {
def defaultValue: Either[String, T]
def getDynamicDefaultValueForParam(paramLabel: String): Option[Any] = None
}

/** companion object and derivation object for [[HasDefault]] */
object HasDefault {
Expand All @@ -21,6 +24,13 @@ object HasDefault {
case None => param.typeclass.defaultValue
}
}

override def getDynamicDefaultValueForParam(paramLabel: String): Option[Any] =
ctx.parameters
.filter(_.label == paramLabel)
.flatMap(_.evaluateDefault)
.headOption
.map(res => res())
}

/** chooses which subtype to delegate to */
Expand All @@ -29,17 +39,27 @@ object HasDefault {
case Some(sub) => sub.typeclass.defaultValue
case None => Left("no subtypes")
}

override def getDynamicDefaultValueForParam(paramLabel: String): Option[Any] =
ctx.subtypes.headOption match {
case Some(sub) => sub.typeclass.getDynamicDefaultValueForParam(paramLabel)
case _ => None
}
}

/** default value for a string; the empty string */
implicit val string: HasDefault[String] = new HasDefault[String] { def defaultValue = Right("") }

/** default value for ints; 0 */
implicit val int: HasDefault[Int] = new HasDefault[Int] { def defaultValue = Right(0) }
implicit val int: HasDefault[Int] = new HasDefault[Int] {
def defaultValue = Right(0)
}

/** oh, no, there is no default Boolean... whatever will we do? */
implicit val boolean: HasDefault[Boolean] = new HasDefault[Boolean] { def defaultValue = Left("truth is a lie") }

implicit val double: HasDefault[Double] = new HasDefault[Double] { def defaultValue = Right(0) }

/** default value for sequences; the empty sequence */
implicit def seq[A]: HasDefault[Seq[A]] = new Typeclass[Seq[A]] { def defaultValue = Right(Seq.empty) }

Expand Down
1 change: 0 additions & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@ addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.1")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.1")
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.3")

15 changes: 15 additions & 0 deletions test/src/test/scala/magnolia1/tests/tests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ object Exactly {

case class ParamsWithDefault(a: Int = 3, b: Int = 4)
case class ParamsWithDefaultGeneric[A, B](a: A = "A", b: B = "B")
case class ParamsWithDynamicDefault(a: Double = scala.math.random())

sealed trait Parent
trait BadChild extends Parent // escape hatch!
Expand Down Expand Up @@ -296,6 +297,20 @@ class Tests extends munit.FunSuite {
assertEquals(res, Right(ParamsWithDefaultGeneric("A", 0)))
}

test("construct a HasDefault instance for a generic product with dynamic default values") {
val res1 = HasDefault.gen[ParamsWithDynamicDefault].getDynamicDefaultValueForParam("a")
val res2 = HasDefault.gen[ParamsWithDynamicDefault].getDynamicDefaultValueForParam("a")

assertEquals(res1.isDefined, true)
assertEquals(res2.isDefined, true)

for {
firstParam <- res1
secondParam <- res2
res = assertNotEquals(firstParam, secondParam)
} yield res
}

test("serialize a Branch") {
val res = implicitly[Show[String, Branch[String]]].show(Branch(Leaf("LHS"), Leaf("RHS")))
assertEquals(res, "Branch[String](left=Leaf[String](value=LHS),right=Leaf[String](value=RHS))")
Expand Down

0 comments on commit a633965

Please sign in to comment.