Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[scala2] feat: adding on-demand default evaluation #533

Merged
merged 8 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Andrapyre marked this conversation as resolved.
Show resolved Hide resolved
* 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)
Andrapyre marked this conversation as resolved.
Show resolved Hide resolved

val valueEvaluator: Option[() => A] = {
Andrapyre marked this conversation as resolved.
Show resolved Hide resolved
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
Loading