Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RuntimeConstraint #175

Merged
merged 9 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 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,7 @@ 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] = ops.either(value).toValidated

/**
* Refine the given value applicatively at runtime, resulting in a [[ValidatedNec]].
Expand All @@ -154,8 +154,7 @@ 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] = ops.either(value).toValidatedNec

/**
* Refine the given value applicatively at runtime, resulting in a [[ValidatedNel]].
Expand All @@ -164,15 +163,14 @@ 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] = ops.either(value).toValidatedNel

/**
* 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 +224,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]]
30 changes: 14 additions & 16 deletions main/src/io/github/iltotore/iron/RefinedTypeOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ object RefinedTypeOps:
*/
type FinalType = T

trait RefinedTypeOpsImpl[A, C, T]:
trait RefinedTypeOpsImpl[A, C, T](using rtc: RuntimeConstraint[A :| C]):
inline given RuntimeConstraint[T] = rtc.asInstanceOf[RuntimeConstraint[T]]

/**
* Implicitly refine at compile-time the given value.
*
Expand All @@ -67,7 +69,7 @@ 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]].
Expand All @@ -76,8 +78,8 @@ trait RefinedTypeOpsImpl[A, C, T]:
* @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,8 +88,8 @@ 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.
Expand All @@ -97,23 +99,19 @@ trait RefinedTypeOpsImpl[A, C, 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)
Iltotore marked this conversation as resolved.
Show resolved Hide resolved

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]]
11 changes: 11 additions & 0 deletions main/src/io/github/iltotore/iron/RuntimeConstraint.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.github.iltotore.iron

type RuntimeConstraint[T] = T match
case IronType[a, c] => RuntimeConstraint.Impl[a, c, T]
Iltotore marked this conversation as resolved.
Show resolved Hide resolved

object RuntimeConstraint:
final class Impl[A, C, T](_test: A => Boolean, val message: String):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it need to have a third T type parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type parameter is required for opaque types so one can do something alongside what's below. It seems that the unnecessary type param was C in this case

object One {
  opaque type Foo = Int :| GreaterEqual[10]
  object Foo extends RefinedTypeOpsImpl[Int, GreaterEqual[10], Foo]
}

object Two {
  import One.Foo
  def doSomething(a: Int)(using RuntimeConstraint[Int, Foo]): Foo = ???
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why it should have a different shape than Constraint[A, C]:

final class RuntimeConstraint[A, C](_test: A => Boolean, val message: String):
  inline def test(value: A): Boolean = _test(value)
trait RefinedTypeOpsImpl[A, C, T](using rtcAuto: RuntimeConstraint.AutoDerived[A, C]):
  ???

For notes about AutoDerived, see my other reply below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial thinking was so that we could use it for opaque types outside of the opaque type companion object. However, thinking about it a bit more now I don't think that makes too much sense. I changed it as per your suggestion

inline def test(value: A): Boolean = _test(value)

inline given derived[A, C](using inline c: Constraint[A, C]): RuntimeConstraint[A :| C] =
new Impl[A, C, 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.fromEither(ops.either(value))

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]]
Loading