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:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Greater

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) is available.

Note: implications are a purely compile-time mechanism.

For example, the following implication:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.any.Not

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

allows us (if imported) to compile this code:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.any.Not
import io.github.iltotore.iron.constraint.numeric.Greater

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

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:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.any.Not
import io.github.iltotore.iron.constraint.numeric.Greater

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:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.any.Not

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:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.any.Not
import io.github.iltotore.iron.constraint.numeric.Greater

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

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.*
import io.github.iltotore.iron.constraint.numeric.Greater

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:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.compileTime.*
import io.github.iltotore.iron.constraint.numeric.Greater

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

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