diff --git a/core/src/main/scala/magnolia1/impl.scala b/core/src/main/scala/magnolia1/impl.scala index 79349938..7d67c544 100644 --- a/core/src/main/scala/magnolia1/impl.scala +++ b/core/src/main/scala/magnolia1/impl.scala @@ -102,7 +102,7 @@ object CaseClassDerivation: paramFromMaps[Typeclass, A, p]( label, CallByNeed(summonInline[Typeclass[p]]), - CallByNeed(defaults.get(label).flatten.flatMap(d => unsafeCast(d.apply))), + CallByNeed.withValueEvaluator(defaults.get(label).flatten.flatMap(d => unsafeCast(d.apply))), repeated, annotations, inheritedAnnotations, diff --git a/core/src/main/scala/magnolia1/interface.scala b/core/src/main/scala/magnolia1/interface.scala index 4afa3dbc..42f14002 100644 --- a/core/src/main/scala/magnolia1/interface.scala +++ b/core/src/main/scala/magnolia1/interface.scala @@ -1,5 +1,7 @@ package magnolia1 +import magnolia1.CaseClass.getDefaultEvaluatorFromDefaultVal + import scala.annotation.tailrec import scala.reflect.* @@ -40,6 +42,9 @@ object CaseClass: * default argument value, if any */ 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 def inheritedAnnotations: IArray[Any] = IArray.empty[Any] override def toString: String = s"Param($label)" @@ -63,6 +68,7 @@ object CaseClass: ): type PType = P def default: Option[PType] = defaultVal.value + override def evaluateDefault: Option[() => PType] = CaseClass.getDefaultEvaluatorFromDefaultVal(defaultVal) def typeclass = cbn.value override def inheritedAnnotations = inheritedAnns def deref(value: T): P = @@ -87,10 +93,18 @@ object CaseClass: ): type PType = P def default: Option[PType] = defaultVal.value + override def evaluateDefault: Option[() => PType] = getDefaultEvaluatorFromDefaultVal(defaultVal) def typeclass = cbn.value def deref(value: T): P = value.asInstanceOf[Product].productElement(idx).asInstanceOf[P] end Param + + private def getDefaultEvaluatorFromDefaultVal[P](defaultVal: CallByNeed[Option[P]]): Option[() => P] = + defaultVal.valueEvaluator.flatMap { evaluator => + evaluator().fold[Option[() => P]](None) { _ => + Some(() => evaluator().get) + } + } end CaseClass /** In the terminology of Algebraic Data Types (ADTs), case classes are known as 'product types'. @@ -162,6 +176,7 @@ abstract class CaseClass[Typeclass[_], Type]( ): type PType = P def default: Option[PType] = defaultVal.value + override def evaluateDefault: Option[() => PType] = getDefaultEvaluatorFromDefaultVal(defaultVal) def typeclass = cbn.value override def inheritedAnnotations = inheritedAnns def deref(value: Type): P = @@ -345,10 +360,44 @@ object SealedTrait: end SealedTrait object CallByNeed: - def apply[A](a: => A): CallByNeed[A] = new CallByNeed(() => a) + /** 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. + */ + def apply[A](a: => A): CallByNeed[A] = new CallByNeed(() => a, () => false) -final class CallByNeed[+A](private[this] var eval: () => A) extends Serializable: - lazy val value: A = - val result = eval() - eval = null - result + /** 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) +end CallByNeed + +// 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.3.6 and earlier + def this(eval: () => A) = this(eval, () => false) + + val valueEvaluator: Option[() => A] = { + val finalRes = if (supportDynamicValueEvaluation()) { + val res = Some(eval) + eval = null + res + } else { + None + } + supportDynamicValueEvaluation = null + finalRes + } + + lazy val value: A = { + if (eval == null) { + valueEvaluator.get.apply() + } else { + val result = eval() + eval = null + result + } + } +} diff --git a/core/src/main/scala/magnolia1/magnolia.scala b/core/src/main/scala/magnolia1/magnolia.scala index 1c42c5cf..0cc048af 100644 --- a/core/src/main/scala/magnolia1/magnolia.scala +++ b/core/src/main/scala/magnolia1/magnolia.scala @@ -1,10 +1,6 @@ package magnolia1 -import scala.compiletime.* import scala.deriving.Mirror -import scala.reflect.* - -import Macro.* trait CommonDerivation[TypeClass[_]]: type Typeclass[T] = TypeClass[T] diff --git a/examples/src/main/scala/magnolia1/examples/default.scala b/examples/src/main/scala/magnolia1/examples/default.scala index 4194ffff..cd08eeaf 100644 --- a/examples/src/main/scala/magnolia1/examples/default.scala +++ b/examples/src/main/scala/magnolia1/examples/default.scala @@ -5,6 +5,7 @@ import magnolia1._ /** typeclass for providing a default value for a particular type */ trait HasDefault[T]: def defaultValue: Either[String, T] + def getDynamicDefaultValueForParam(paramLabel: String): Option[Any] = None /** companion object and derivation object for [[HasDefault]] */ object HasDefault extends AutoDerivation[HasDefault]: @@ -19,6 +20,16 @@ object HasDefault extends AutoDerivation[HasDefault]: case None => param.typeclass.defaultValue } } + + override def getDynamicDefaultValueForParam(paramLabel: String): Option[Any] = + IArray + .genericWrapArray { + ctx.params + .filter(_.label == paramLabel) + } + .toArray + .headOption + .flatMap(_.evaluateDefault.map(res => res())) } /** chooses which subtype to delegate to */ @@ -28,6 +39,12 @@ object HasDefault extends AutoDerivation[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 */ given string: HasDefault[String] with def defaultValue = Right("") @@ -39,6 +56,9 @@ object HasDefault extends AutoDerivation[HasDefault]: given boolean: HasDefault[Boolean] with def defaultValue = Left("truth is a lie") + given double: HasDefault[Double] with + def defaultValue = Right(0) + /** default value for sequences; the empty sequence */ given seq[A]: HasDefault[Seq[A]] with def defaultValue = Right(Seq.empty) diff --git a/test/src/test/scala/magnolia1/tests/DefaultValuesTests.scala b/test/src/test/scala/magnolia1/tests/DefaultValuesTests.scala index d827ae8b..98bfbcb6 100644 --- a/test/src/test/scala/magnolia1/tests/DefaultValuesTests.scala +++ b/test/src/test/scala/magnolia1/tests/DefaultValuesTests.scala @@ -37,6 +37,19 @@ class DefaultValuesTests extends munit.FunSuite: assertEquals(res, Right(Item("", 1, 0))) } + test("access dynamic default constructor values") { + val res1 = summon[HasDefault[ParamsWithDynamicDefault]].getDynamicDefaultValueForParam("a") + val res2 = summon[HasDefault[ParamsWithDynamicDefault]].getDynamicDefaultValueForParam("a") + + assertEquals(res1.isDefined, true) + assertEquals(res2.isDefined, true) + + for { + default1 <- res1 + default2 <- res2 + } yield assertNotEquals(default1, default2) + } + test("construct a HasDefault instance for a generic product with default values") { val res = HasDefault.derived[ParamsWithDefaultGeneric[String, Int]].defaultValue assertEquals(res, Right(ParamsWithDefaultGeneric("A", 0))) @@ -52,6 +65,8 @@ object DefaultValuesTests: case class ParamsWithDefault(a: Int = 3, b: Int = 4) + case class ParamsWithDynamicDefault(a: Double = scala.math.random()) + case class ParamsWithDefaultGeneric[A, B](a: A = "A", b: B = "B") case class ParamsWithDefaultDeepGeneric[A, B](a: Option[A] = Some("A"), b: Option[B] = Some("B"))