From 3bd4ead2ee82096cea943a04245902f73eb9337d Mon Sep 17 00:00:00 2001 From: Andrapyre <42009361+Andrapyre@users.noreply.github.com> Date: Thu, 16 May 2024 03:37:19 +0200 Subject: [PATCH 1/9] staging --- core/src/main/scala/magnolia1/interface.scala | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/magnolia1/interface.scala b/core/src/main/scala/magnolia1/interface.scala index 4afa3dbc..8c90c8ca 100644 --- a/core/src/main/scala/magnolia1/interface.scala +++ b/core/src/main/scala/magnolia1/interface.scala @@ -40,6 +40,7 @@ object CaseClass: * default argument value, if any */ def default: Option[PType] + def dynamicDefault: Option[() => PType] def inheritedAnnotations: IArray[Any] = IArray.empty[Any] override def toString: String = s"Param($label)" @@ -63,6 +64,9 @@ object CaseClass: ): type PType = P def default: Option[PType] = defaultVal.value + def dynamicDefault: Option[() => PType] = defaultVal.dynamicValue().fold[Option[() => PType]](None) { _ => + Some(() => defaultVal.dynamicValue().get) + } def typeclass = cbn.value override def inheritedAnnotations = inheritedAnns def deref(value: T): P = @@ -87,6 +91,9 @@ object CaseClass: ): type PType = P def default: Option[PType] = defaultVal.value + def dynamicDefault: Option[() => PType] = defaultVal.dynamicValue().fold[Option[() => PType]](None) { _ => + Some(() => defaultVal.dynamicValue().get) + } def typeclass = cbn.value def deref(value: T): P = value.asInstanceOf[Product].productElement(idx).asInstanceOf[P] @@ -162,6 +169,9 @@ abstract class CaseClass[Typeclass[_], Type]( ): type PType = P def default: Option[PType] = defaultVal.value + def dynamicDefault: Option[() => PType] = defaultVal.dynamicValue().fold[Option[() => PType]](None) { _ => + Some(() => defaultVal.dynamicValue().get) + } def typeclass = cbn.value override def inheritedAnnotations = inheritedAnns def deref(value: Type): P = @@ -347,8 +357,15 @@ end SealedTrait object CallByNeed: def apply[A](a: => A): CallByNeed[A] = new CallByNeed(() => a) -final class CallByNeed[+A](private[this] var eval: () => A) extends Serializable: - lazy val value: A = + object CallByNeed { + def apply[A](a: => A): CallByNeed[A] = new CallByNeed(() => a) + } + +final class CallByNeed[+A](private[this] var eval: () => A) extends Serializable { + val dynamicValue: () => A = eval + lazy val value: A = { val result = eval() eval = null result + } +} From 32ae23f97e68f3ef420727ce347cdc539519bfba Mon Sep 17 00:00:00 2001 From: Andrapyre <42009361+Andrapyre@users.noreply.github.com> Date: Thu, 16 May 2024 04:25:33 +0200 Subject: [PATCH 2/9] reverting to native 4 --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 94b2f6d8..da43173d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -9,5 +9,5 @@ addSbtPlugin( ) addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.1") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.3") From f33c4308c8518ac58eb2fda13315fbd573c50569 Mon Sep 17 00:00:00 2001 From: Andrapyre <42009361+Andrapyre@users.noreply.github.com> Date: Mon, 20 May 2024 06:20:05 +0200 Subject: [PATCH 3/9] revising for performance and adding tests --- core/src/main/scala/magnolia1/impl.scala | 2 +- core/src/main/scala/magnolia1/interface.scala | 57 ++++++++++++------- .../scala/magnolia1/examples/default.scala | 22 +++++++ .../magnolia1/tests/DefaultValuesTests.scala | 15 +++++ 4 files changed, 76 insertions(+), 20 deletions(-) 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 8c90c8ca..977c8bec 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,7 +42,7 @@ object CaseClass: * default argument value, if any */ def default: Option[PType] - def dynamicDefault: Option[() => PType] + def evaluateDefault: Option[() => PType] def inheritedAnnotations: IArray[Any] = IArray.empty[Any] override def toString: String = s"Param($label)" @@ -64,9 +66,7 @@ object CaseClass: ): type PType = P def default: Option[PType] = defaultVal.value - def dynamicDefault: Option[() => PType] = defaultVal.dynamicValue().fold[Option[() => PType]](None) { _ => - Some(() => defaultVal.dynamicValue().get) - } + def evaluateDefault: Option[() => PType] = CaseClass.getDefaultEvaluatorFromDefaultVal(defaultVal) def typeclass = cbn.value override def inheritedAnnotations = inheritedAnns def deref(value: T): P = @@ -91,13 +91,18 @@ object CaseClass: ): type PType = P def default: Option[PType] = defaultVal.value - def dynamicDefault: Option[() => PType] = defaultVal.dynamicValue().fold[Option[() => PType]](None) { _ => - Some(() => defaultVal.dynamicValue().get) - } + 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'. @@ -169,9 +174,7 @@ abstract class CaseClass[Typeclass[_], Type]( ): type PType = P def default: Option[PType] = defaultVal.value - def dynamicDefault: Option[() => PType] = defaultVal.dynamicValue().fold[Option[() => PType]](None) { _ => - Some(() => defaultVal.dynamicValue().get) - } + def evaluateDefault: Option[() => PType] = getDefaultEvaluatorFromDefaultVal(defaultVal) def typeclass = cbn.value override def inheritedAnnotations = inheritedAnns def deref(value: Type): P = @@ -355,17 +358,33 @@ object SealedTrait: end SealedTrait object CallByNeed: - def apply[A](a: => A): CallByNeed[A] = new CallByNeed(() => a) - - object CallByNeed { - def apply[A](a: => A): CallByNeed[A] = new CallByNeed(() => a) + def apply[A](a: => A): CallByNeed[A] = new CallByNeed(() => a, () => false) + 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 { + val valueEvaluator: Option[() => A] = { + val finalRes = if (supportDynamicValueEvaluation()) { + val res = Some(eval) + eval = null + res + } else { + None + } + supportDynamicValueEvaluation = null + finalRes } -final class CallByNeed[+A](private[this] var eval: () => A) extends Serializable { - val dynamicValue: () => A = eval lazy val value: A = { - val result = eval() - eval = null - result + if (eval == null) { + valueEvaluator.get.apply() + } else { + val result = eval() + eval = null + result + } } } diff --git a/examples/src/main/scala/magnolia1/examples/default.scala b/examples/src/main/scala/magnolia1/examples/default.scala index 4194ffff..8e561552 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,18 @@ object HasDefault extends AutoDerivation[HasDefault]: case None => param.typeclass.defaultValue } } + + override def getDynamicDefaultValueForParam(paramLabel: String): Option[Any] = + val arr = IArray.genericWrapArray { + ctx.params + .filter(_.label == paramLabel) + }.toArray + + val res = arr.headOption + .flatMap(_.evaluateDefault.map(res => res())) + println("Printing res:") + println(res.mkString("Array(", ", ", ")")) + res } /** chooses which subtype to delegate to */ @@ -28,6 +41,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 +58,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")) From f389422f9445256b4a9fa1c7377a369fcee1e411 Mon Sep 17 00:00:00 2001 From: Andrapyre <42009361+Andrapyre@users.noreply.github.com> Date: Tue, 21 May 2024 03:25:58 +0200 Subject: [PATCH 4/9] revising plugin and formatting --- core/src/main/scala/magnolia1/magnolia.scala | 4 ---- .../main/scala/magnolia1/examples/default.scala | 16 +++++++--------- project/plugins.sbt | 2 +- 3 files changed, 8 insertions(+), 14 deletions(-) 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 8e561552..cd08eeaf 100644 --- a/examples/src/main/scala/magnolia1/examples/default.scala +++ b/examples/src/main/scala/magnolia1/examples/default.scala @@ -22,16 +22,14 @@ object HasDefault extends AutoDerivation[HasDefault]: } override def getDynamicDefaultValueForParam(paramLabel: String): Option[Any] = - val arr = IArray.genericWrapArray { - ctx.params - .filter(_.label == paramLabel) - }.toArray - - val res = arr.headOption + IArray + .genericWrapArray { + ctx.params + .filter(_.label == paramLabel) + } + .toArray + .headOption .flatMap(_.evaluateDefault.map(res => res())) - println("Printing res:") - println(res.mkString("Array(", ", ", ")")) - res } /** chooses which subtype to delegate to */ diff --git a/project/plugins.sbt b/project/plugins.sbt index da43173d..94b2f6d8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -9,5 +9,5 @@ addSbtPlugin( ) addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.3") From 497c813626674884888b5841a02902a9ab2953d0 Mon Sep 17 00:00:00 2001 From: Andrapyre <42009361+Andrapyre@users.noreply.github.com> Date: Tue, 21 May 2024 03:47:42 +0200 Subject: [PATCH 5/9] adding scala docs --- core/src/main/scala/magnolia1/interface.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/src/main/scala/magnolia1/interface.scala b/core/src/main/scala/magnolia1/interface.scala index 977c8bec..ca6c6665 100644 --- a/core/src/main/scala/magnolia1/interface.scala +++ b/core/src/main/scala/magnolia1/interface.scala @@ -358,7 +358,17 @@ object SealedTrait: end SealedTrait 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. + */ 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) end CallByNeed From 14f5bd95b26798a7f04db74e0eb0c8dbdda0a8ad Mon Sep 17 00:00:00 2001 From: Andrapyre <42009361+Andrapyre@users.noreply.github.com> Date: Tue, 21 May 2024 03:48:13 +0200 Subject: [PATCH 6/9] reformatting --- core/src/main/scala/magnolia1/interface.scala | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/core/src/main/scala/magnolia1/interface.scala b/core/src/main/scala/magnolia1/interface.scala index ca6c6665..f0f8c272 100644 --- a/core/src/main/scala/magnolia1/interface.scala +++ b/core/src/main/scala/magnolia1/interface.scala @@ -358,17 +358,14 @@ object SealedTrait: end SealedTrait 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. - */ + /** 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) - /** - * 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 - */ + /** 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 From be4a7bdb609a7375b478abace153a22b9c34c958 Mon Sep 17 00:00:00 2001 From: Andrapyre <42009361+Andrapyre@users.noreply.github.com> Date: Wed, 22 May 2024 03:00:11 +0200 Subject: [PATCH 7/9] fixing bincompat --- core/src/main/scala/magnolia1/interface.scala | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/magnolia1/interface.scala b/core/src/main/scala/magnolia1/interface.scala index f0f8c272..6b2e5513 100644 --- a/core/src/main/scala/magnolia1/interface.scala +++ b/core/src/main/scala/magnolia1/interface.scala @@ -42,7 +42,7 @@ object CaseClass: * default argument value, if any */ def default: Option[PType] - def evaluateDefault: Option[() => PType] + def evaluateDefault: Option[() => PType] = None def inheritedAnnotations: IArray[Any] = IArray.empty[Any] override def toString: String = s"Param($label)" @@ -66,7 +66,7 @@ object CaseClass: ): type PType = P def default: Option[PType] = defaultVal.value - def evaluateDefault: Option[() => PType] = CaseClass.getDefaultEvaluatorFromDefaultVal(defaultVal) + override def evaluateDefault: Option[() => PType] = CaseClass.getDefaultEvaluatorFromDefaultVal(defaultVal) def typeclass = cbn.value override def inheritedAnnotations = inheritedAnns def deref(value: T): P = @@ -91,7 +91,7 @@ object CaseClass: ): type PType = P def default: Option[PType] = defaultVal.value - def evaluateDefault: Option[() => PType] = getDefaultEvaluatorFromDefaultVal(defaultVal) + override def evaluateDefault: Option[() => PType] = getDefaultEvaluatorFromDefaultVal(defaultVal) def typeclass = cbn.value def deref(value: T): P = value.asInstanceOf[Product].productElement(idx).asInstanceOf[P] @@ -174,7 +174,7 @@ abstract class CaseClass[Typeclass[_], Type]( ): type PType = P def default: Option[PType] = defaultVal.value - def evaluateDefault: Option[() => PType] = getDefaultEvaluatorFromDefaultVal(defaultVal) + override def evaluateDefault: Option[() => PType] = getDefaultEvaluatorFromDefaultVal(defaultVal) def typeclass = cbn.value override def inheritedAnnotations = inheritedAnns def deref(value: Type): P = @@ -373,6 +373,9 @@ end CallByNeed // 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 { + + def this(eval: () => A) = this(eval, () => false) + val valueEvaluator: Option[() => A] = { val finalRes = if (supportDynamicValueEvaluation()) { val res = Some(eval) From 9853273a4aa9dc6d95044f25eee941288ea9bcd4 Mon Sep 17 00:00:00 2001 From: Andrapyre <42009361+Andrapyre@users.noreply.github.com> Date: Thu, 23 May 2024 03:41:01 +0200 Subject: [PATCH 8/9] adding clarifying comments --- core/src/main/scala/magnolia1/interface.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main/scala/magnolia1/interface.scala b/core/src/main/scala/magnolia1/interface.scala index 6b2e5513..865435b5 100644 --- a/core/src/main/scala/magnolia1/interface.scala +++ b/core/src/main/scala/magnolia1/interface.scala @@ -42,6 +42,7 @@ 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)" @@ -374,6 +375,7 @@ end CallByNeed 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] = { From fefefb9e61a8ea03be82e7cdd60fdd8c16e29691 Mon Sep 17 00:00:00 2001 From: Andrapyre <42009361+Andrapyre@users.noreply.github.com> Date: Thu, 23 May 2024 03:41:37 +0200 Subject: [PATCH 9/9] adding formatting --- core/src/main/scala/magnolia1/interface.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/scala/magnolia1/interface.scala b/core/src/main/scala/magnolia1/interface.scala index 865435b5..42f14002 100644 --- a/core/src/main/scala/magnolia1/interface.scala +++ b/core/src/main/scala/magnolia1/interface.scala @@ -42,6 +42,7 @@ 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]