Skip to content

Commit

Permalink
Merge pull request #534 from Andrapyre/adding-dynamic-default-scala3
Browse files Browse the repository at this point in the history
feat: adding on-demand default evaluation
  • Loading branch information
adamw authored May 23, 2024
2 parents a7fa6de + fefefb9 commit c9656f3
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 11 deletions.
2 changes: 1 addition & 1 deletion core/src/main/scala/magnolia1/impl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
61 changes: 55 additions & 6 deletions core/src/main/scala/magnolia1/interface.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package magnolia1

import magnolia1.CaseClass.getDefaultEvaluatorFromDefaultVal

import scala.annotation.tailrec
import scala.reflect.*

Expand Down Expand Up @@ -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)"

Expand All @@ -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 =
Expand All @@ -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'.
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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
}
}
}
4 changes: 0 additions & 4 deletions core/src/main/scala/magnolia1/magnolia.scala
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
20 changes: 20 additions & 0 deletions examples/src/main/scala/magnolia1/examples/default.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand All @@ -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 */
Expand All @@ -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("")
Expand All @@ -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)
15 changes: 15 additions & 0 deletions test/src/test/scala/magnolia1/tests/DefaultValuesTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand All @@ -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"))
Expand Down

0 comments on commit c9656f3

Please sign in to comment.