Skip to content

Commit

Permalink
Add RuntimeConstraint (#175)
Browse files Browse the repository at this point in the history
  • Loading branch information
kyri-petrou authored Sep 28, 2023
1 parent 193b93d commit de20278
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 42 deletions.
27 changes: 14 additions & 13 deletions cats/src/io/github/iltotore/iron/cats.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]].
Expand All @@ -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]].
Expand All @@ -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]].
Expand All @@ -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]].
Expand All @@ -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]]
Expand Down Expand Up @@ -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]]
inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Hash[mirror.IronType]): Hash[T] = ev.asInstanceOf[Hash[T]]
33 changes: 14 additions & 19 deletions main/src/io/github/iltotore/iron/RefinedTypeOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand All @@ -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]].
Expand All @@ -86,34 +86,29 @@ 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
override type ConstraintType = C

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]]
inline def value: IronType[A, C] = wrapper.asInstanceOf[IronType[A, C]]
24 changes: 24 additions & 0 deletions main/src/io/github/iltotore/iron/RuntimeConstraint.scala
Original file line number Diff line number Diff line change
@@ -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)
17 changes: 7 additions & 10 deletions zio/src/io/github/iltotore/iron/zio.scala
Original file line number Diff line number Diff line change
@@ -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]].
*
Expand All @@ -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]].
*
Expand All @@ -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]]
inline given [T](using mirror: RefinedTypeOps.Mirror[T], ev: Hash[mirror.IronType]): Hash[T] = ev.asInstanceOf[Hash[T]]

0 comments on commit de20278

Please sign in to comment.