Implication

Implication is a compile-time mechanism to allow casting from a refined type to another. It is analogous to logical implication.

Implication is represented in Iron by Implication[C1, C2] or its alias C1 ==> C2. It should be read as "C1 implies C2".

For example, the following code compiles due to transitivity:

val x: Int :| Greater[5] = ???
val y: Int :| Greater[0] = x

Standard implications are usually stored in the same object as the associated constraint. For instance, the transitive implication mentioned above is stored in numeric.

Creation

You can create your own constraint-to-constraint implications following this single rule: a constraint C1 can be cast to another C2 if an implicit instance of Implication[C1, C2] (or using the alias C1 ==> C2) available.

Note: implications are a purely compile-time mechanism.

For example, the following implication:

given [C1]: (C1 ==> Not[Not[C1]]) = Implication()

allows us (if imported) to compile this code:

val x: Int :| Greater[0] = ???
val y: Int :| Not[Not[Greater[0]]] = x //C1 implies Not[Not[C1]]: `x` can be safely casted.

Dependencies

Almost every implication has a dependency. For example, our previous "double negation" implication doesn't work in the following case:

val x: Int :| Greater[1] = ???

//Assuming that Greater[1] ==> Greater[0]
val y: Int :| Not[Not[Greater[0]]] = x

But it should. This can be fixed by using two different constraints linked by an implication:

given [C1, C2](using C1 ==> C2): (C1 ==> Not[Not[C2]]) = Implication()

Example taken from io.github.iltotore.iron.constraint.any.

Our implication now depends on another implication: C1 ==> C2. With this implementation, our code now compiles:

val x: Int :| Greater[1] = ???

//Assuming that Greater[1] ==> Greater[0]
val y: Int :| Not[Not[Greater[0]]] = x //(Greater[1] ==> Greater[0]) ==> (Greater[1] ==> Not[Not[Greater[0]]])

Type operators

Scala 3 provides multiple compile-time constructs to help you to manipulate and test types. The most notable ones are scala.=:= and io.github.iltotore.iron.ops.

An instance of A =:= B is given by the compiler if A is the same type as B. This can be combined with iron.ops to create logical dependencies.

For example, we can implement the transitive relation:

import io.github.iltotore.iron.compileTime.*

given [V1, V2](using V1 > V2 =:= true): (Greater[V1] ==> Greater[V2]) = Implication()

Example taken from numeric.

Now, the following code compiles:

val x: Int :| Greater[1] = ???
val y: Int :| Greater[0] = x //x > 1 and 1 > 0, so x > 0.