From de202782ab45b8d29c05e47204f3a639cf14438e Mon Sep 17 00:00:00 2001 From: kyri-petrou <67301607+kyri-petrou@users.noreply.github.com> Date: Thu, 28 Sep 2023 19:28:17 +1000 Subject: [PATCH] Add RuntimeConstraint (#175) --- cats/src/io/github/iltotore/iron/cats.scala | 27 +++++++-------- .../github/iltotore/iron/RefinedTypeOps.scala | 33 ++++++++----------- .../iltotore/iron/RuntimeConstraint.scala | 24 ++++++++++++++ zio/src/io/github/iltotore/iron/zio.scala | 17 ++++------ 4 files changed, 59 insertions(+), 42 deletions(-) create mode 100644 main/src/io/github/iltotore/iron/RuntimeConstraint.scala diff --git a/cats/src/io/github/iltotore/iron/cats.scala b/cats/src/io/github/iltotore/iron/cats.scala index 5f0be0ee..6816f638 100644 --- a/cats/src/io/github/iltotore/iron/cats.scala +++ b/cats/src/io/github/iltotore/iron/cats.scala @@ -127,7 +127,7 @@ object cats extends IronCatsInstances: * @return a [[Right]] containing this value as [[IronType]] or a [[Left]] containing the constraint message. * @see [[either]], [[eitherNel]]. */ - inline def eitherNec(value: A)(using inline c: Constraint[A, C]): EitherNec[String, T] = value.refineNec[C].map(_.asInstanceOf[T]) + def eitherNec(value: A): EitherNec[String, T] = ops.either(value).toEitherNec /** * Refine the given value at runtime, resulting in an [[EitherNel]]. @@ -136,7 +136,7 @@ object cats extends IronCatsInstances: * @return a [[Right]] containing this value as [[IronType]] or a [[Left]] containing the constraint message. * @see [[either]], [[eitherNec]]. */ - inline def eitherNel(value: A)(using inline c: Constraint[A, C]): EitherNel[String, T] = value.refineNel[C].map(_.asInstanceOf[T]) + def eitherNel(value: A): EitherNel[String, T] = ops.either(value).toEitherNel /** * Refine the given value at runtime, resulting in a [[Validated]]. @@ -145,7 +145,8 @@ object cats extends IronCatsInstances: * @return a [[Valid]] containing this value as [[IronType]] or an [[Invalid]] containing the constraint message. * @see [[validatedNec]], [[validatedNel]]. */ - inline def validated(value: A)(using inline c: Constraint[A, C]): Validated[String, T] = value.refineValidated[C].map(_.asInstanceOf[T]) + def validated(value: A): Validated[String, T] = + if ops.rtc.test(value) then Validated.valid(value.asInstanceOf[T]) else Validated.invalid(ops.rtc.message) /** * Refine the given value applicatively at runtime, resulting in a [[ValidatedNec]]. @@ -154,8 +155,8 @@ object cats extends IronCatsInstances: * @return a [[Valid]] containing this value as [[IronType]] or an [[Invalid]] containing a [[NonEmptyChain]] of error messages. * @see [[validated]], [[validatedNel]]. */ - inline def validatedNec(value: A)(using inline c: Constraint[A, C]): ValidatedNec[String, T] = - value.refineValidatedNec[C].map(_.asInstanceOf[T]) + def validatedNec(value: A): ValidatedNec[String, T] = + if ops.rtc.test(value) then Validated.validNec(value.asInstanceOf[T]) else Validated.invalidNec(ops.rtc.message) /** * Refine the given value applicatively at runtime, resulting in a [[ValidatedNel]]. @@ -164,15 +165,15 @@ object cats extends IronCatsInstances: * @return a [[Valid]] containing this value as [[IronType]] or an [[Invalid]] containing a [[NonEmptyList]] of error messages. * @see [[validated]], [[validatedNec]]. */ - inline def validatedNel(value: A)(using inline c: Constraint[A, C]): ValidatedNel[String, T] = - value.refineValidatedNel[C].map(_.asInstanceOf[T]) + def validatedNel(value: A): ValidatedNel[String, T] = + if ops.rtc.test(value) then Validated.validNel(value.asInstanceOf[T]) else Validated.invalidNel(ops.rtc.message) /** * Represent all Cats' typeclass instances for Iron. */ private trait IronCatsInstances extends IronCatsLowPriority, RefinedTypeOpsCats: - //The `NotGiven` implicit parameter is mandatory to avoid ambiguous implicit error when both Eq[A] and Hash[A]/PartialOrder[A] exist + // The `NotGiven` implicit parameter is mandatory to avoid ambiguous implicit error when both Eq[A] and Hash[A]/PartialOrder[A] exist inline given [A, C](using inline ev: Eq[A], notHashOrOrder: NotGiven[Hash[A] | PartialOrder[A]]): Eq[A :| C] = ev.asInstanceOf[Eq[A :| C]] inline given [A, C](using inline ev: PartialOrder[A], notOrder: NotGiven[Order[A]]): PartialOrder[A :| C] = ev.asInstanceOf[PartialOrder[A :| C]] @@ -226,14 +227,14 @@ private trait IronCatsLowPriority: private trait RefinedTypeOpsCats extends RefinedTypeOpsCatsLowPriority: - inline given[T](using mirror: RefinedTypeOps.Mirror[T], ev: Eq[mirror.IronType]): Eq[T] = ev.asInstanceOf[Eq[T]] + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Eq[mirror.IronType]): Eq[T] = ev.asInstanceOf[Eq[T]] - inline given[T](using mirror: RefinedTypeOps.Mirror[T], ev: Order[mirror.IronType]): Order[T] = ev.asInstanceOf[Order[T]] + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Order[mirror.IronType]): Order[T] = ev.asInstanceOf[Order[T]] - inline given[T](using mirror: RefinedTypeOps.Mirror[T], ev: Show[mirror.IronType]): Show[T] = ev.asInstanceOf[Show[T]] + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Show[mirror.IronType]): Show[T] = ev.asInstanceOf[Show[T]] - inline given[T](using mirror: RefinedTypeOps.Mirror[T], ev: PartialOrder[mirror.IronType]): PartialOrder[T] = ev.asInstanceOf[PartialOrder[T]] + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: PartialOrder[mirror.IronType]): PartialOrder[T] = ev.asInstanceOf[PartialOrder[T]] private trait RefinedTypeOpsCatsLowPriority: - inline given[T](using mirror: RefinedTypeOps.Mirror[T], ev: Hash[mirror.IronType]): Hash[T] = ev.asInstanceOf[Hash[T]] \ No newline at end of file + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Hash[mirror.IronType]): Hash[T] = ev.asInstanceOf[Hash[T]] diff --git a/main/src/io/github/iltotore/iron/RefinedTypeOps.scala b/main/src/io/github/iltotore/iron/RefinedTypeOps.scala index 63f5b195..d5d401bf 100644 --- a/main/src/io/github/iltotore/iron/RefinedTypeOps.scala +++ b/main/src/io/github/iltotore/iron/RefinedTypeOps.scala @@ -48,12 +48,13 @@ object RefinedTypeOps: */ type FinalType = T -trait RefinedTypeOpsImpl[A, C, T]: +trait RefinedTypeOpsImpl[A, C, T](using val rtc: RuntimeConstraint[A, C]): + inline protected given RuntimeConstraint[A, C] = rtc + /** * Implicitly refine at compile-time the given value. * * @param value the value to refine. - * @param constraint the implementation of `C` to check. * @tparam A the refined type. * @tparam C the constraint applied to the type. * @return the given value typed as [[IronType]] @@ -67,17 +68,16 @@ trait RefinedTypeOpsImpl[A, C, T]: * @return a constrained value, without performing constraint checks. * @see [[apply]], [[applyUnsafe]]. */ - inline def assume(value: A): T = value.assume[C].asInstanceOf[T] + inline def assume(value: A): T = value.asInstanceOf[T] /** * Refine the given value at runtime, resulting in an [[Either]]. * - * @param constraint the constraint to test with the value to refine. * @return a [[Right]] containing this value as [[T]] or a [[Left]] containing the constraint message. * @see [[fromIronType]], [[option]], [[applyUnsafe]]. */ - inline def either(value: A)(using constraint: Constraint[A, C]): Either[String, T] = - Either.cond(constraint.test(value), value.asInstanceOf[T], constraint.message) + def either(value: A): Either[String, T] = + Either.cond(rtc.test(value), value.asInstanceOf[T], rtc.message) /** * Refine the given value at runtime, resulting in an [[Option]]. @@ -86,21 +86,20 @@ trait RefinedTypeOpsImpl[A, C, T]: * @return an Option containing this value as [[T]] or [[None]]. * @see [[fromIronType]], [[either]], [[applyUnsafe]]. */ - inline def option(value: A)(using constraint: Constraint[A, C]): Option[T] = - Option.when(constraint.test(value))(value.asInstanceOf[T]) + def option(value: A): Option[T] = + Option.when(rtc.test(value))(value.asInstanceOf[T]) /** * Refine the given value at runtime. * - * @param constraint the constraint to test with the value to refine. * @return this value as [[T]]. * @throws an [[IllegalArgumentException]] if the constraint is not satisfied. * @see [[fromIronType]], [[either]], [[option]]. */ - inline def applyUnsafe(value: A)(using Constraint[A, C]): T = - value.refine[C].asInstanceOf[T] + inline def applyUnsafe(value: A): T = + if rtc.test(value) then value.asInstanceOf[T] else throw new IllegalArgumentException(rtc.message) - inline def unapply(value: T): Option[A :| C] = Some(value.asInstanceOf[A :| C]) + def unapply(value: T): Option[A :| C] = Some(value.asInstanceOf[A :| C]) inline given RefinedTypeOps.Mirror[T] with override type BaseType = A @@ -108,12 +107,8 @@ trait RefinedTypeOpsImpl[A, C, T]: inline given [R]: TypeTest[T, R] = summonInline[TypeTest[A :| C, R]].asInstanceOf[TypeTest[T, R]] - inline given [L](using inline constraint: Constraint[A, C]): TypeTest[L, T] = - val test = summonInline[TypeTest[L, A]] - - new TypeTest: - override def unapply(value: L): Option[value.type & T] = - test.unapply(value).filter(constraint.test(_)).asInstanceOf + given [L](using test: TypeTest[L, A]): TypeTest[L, T] with + override def unapply(value: L): Option[value.type & T] = test.unapply(value).filter(rtc.test(_)).asInstanceOf extension (wrapper: T) - inline def value: IronType[A, C] = wrapper.asInstanceOf[IronType[A, C]] \ No newline at end of file + inline def value: IronType[A, C] = wrapper.asInstanceOf[IronType[A, C]] diff --git a/main/src/io/github/iltotore/iron/RuntimeConstraint.scala b/main/src/io/github/iltotore/iron/RuntimeConstraint.scala new file mode 100644 index 00000000..3236dc60 --- /dev/null +++ b/main/src/io/github/iltotore/iron/RuntimeConstraint.scala @@ -0,0 +1,24 @@ +package io.github.iltotore.iron + +import scala.util.NotGiven + +/** + * A [[RuntimeConstraint]] is similar to a [[Constraint]] with the difference that it can be used + * in non-inlined methods. + * + * This allows refinement of values in polymorphic methods / givens without the use of `inline`. + * e.g., the code below would fail to compile if [[Constraint]] was used instead. + * + * {{{ + * def foo[A, C](value: A)(using c: RuntimeConstraint[A, C]): Either[String, A :| C] = + * if c.test(value) then Right(value.assume[C]) else Left(c.message) + * }}} + * + * In cases that one does not exist in scope, one will be automatically derived from a [[Constraint]]. + */ +final class RuntimeConstraint[A, C](_test: A => Boolean, val message: String): + inline def test(value: A): Boolean = _test(value) + +object RuntimeConstraint: + inline given derived[A, C](using inline c: Constraint[A, C]): RuntimeConstraint[A, C] = + new RuntimeConstraint[A, C](c.test(_), c.message) diff --git a/zio/src/io/github/iltotore/iron/zio.scala b/zio/src/io/github/iltotore/iron/zio.scala index 9011650a..4e0e1d1a 100644 --- a/zio/src/io/github/iltotore/iron/zio.scala +++ b/zio/src/io/github/iltotore/iron/zio.scala @@ -1,12 +1,11 @@ package io.github.iltotore.iron import _root_.zio.NonEmptyChunk -import _root_.zio.prelude.{Debug, Equal, Hash, Ord, PartialOrd, Validation} +import _root_.zio.prelude.{Debug, Equal, Hash, Ord, Validation} object zio extends RefinedTypeOpsZio: extension [A](value: A) - /** * Refine the given value applicatively at runtime, resulting in a [[Validation]]. * @@ -16,9 +15,7 @@ object zio extends RefinedTypeOpsZio: inline def refineValidation[C](using inline constraint: Constraint[A, C]): Validation[String, A :| C] = Validation.fromPredicateWith(constraint.message)(value.asInstanceOf[A :| C])(constraint.test(_)) - extension [A, C1](value: A :| C1) - /** * Refine the given value again applicatively at runtime, resulting in a [[Validation]]. * @@ -35,17 +32,17 @@ object zio extends RefinedTypeOpsZio: * @param constraint the constraint to test with the value to refine. * @return a [[Valid]] containing this value as [[T]] or an [[Validation.Failure]] containing a [[NonEmptyChunk]] of error messages. */ - inline def validation(value: A)(using inline constraint: Constraint[A, C]): Validation[String, T] = - value.refineValidation[C].map(_.asInstanceOf[T]) + def validation(value: A): Validation[String, T] = + Validation.fromPredicateWith(ops.rtc.message)(value)(ops.rtc.test(_)).asInstanceOf[Validation[String, T]] private trait RefinedTypeOpsZio extends RefinedTypeOpsZioLowPriority: - inline given[T](using mirror: RefinedTypeOps.Mirror[T], ev: Debug[mirror.IronType]): Debug[T] = ev.asInstanceOf[Debug[T]] + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Debug[mirror.IronType]): Debug[T] = ev.asInstanceOf[Debug[T]] - inline given[T](using mirror: RefinedTypeOps.Mirror[T], ev: Equal[mirror.IronType]): Equal[T] = ev.asInstanceOf[Equal[T]] + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Equal[mirror.IronType]): Equal[T] = ev.asInstanceOf[Equal[T]] - inline given[T](using mirror: RefinedTypeOps.Mirror[T], ev: Ord[mirror.IronType]): Ord[T] = ev.asInstanceOf[Ord[T]] + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Ord[mirror.IronType]): Ord[T] = ev.asInstanceOf[Ord[T]] private trait RefinedTypeOpsZioLowPriority: - inline given[T](using mirror: RefinedTypeOps.Mirror[T], ev: Hash[mirror.IronType]): Hash[T] = ev.asInstanceOf[Hash[T]] \ No newline at end of file + inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Hash[mirror.IronType]): Hash[T] = ev.asInstanceOf[Hash[T]]