-
Notifications
You must be signed in to change notification settings - Fork 46
Constraint Creation
This page details all steps to create a type constraint.
A type constraint consists of two parts:
- Dummy: the shallow/API part of your constraint
- Behaviour: the internal behaviour of your constraint
A dummy can be anything passable as a generic type. Iron's defaults use traits for dummies.
The dummy can contain information like values through singleton types.
Example:
import io.github.iltotore.numeric.Number
trait Greater[V <: Number] //A V singleton type is required
Note: Number is an alias for the union of all numerical primitives.
We need to attach a behaviour to our dummy. A constraint behaviour is a given instance of Constraint[A, B]
where A is
the input type and B the dummy.
For constraints without parameters, we can just use create an anonymous given:
import io.github.iltotore.numeric.Number
import scala.compiletime.constValue
trait Dummy
inline given Constraint[Boolean, Dummy] with {
override inline def assert(value: Boolean): Boolean = value
}
If our constraint is runtime-only, you can use the same method as above.
Due to Dotty's nested inlines limitation, we need to create a class for compile-time constraints with parameters:
import io.github.iltotore.numeric.Number
import scala.compiletime.constValue
trait Greater[V <: Number]
class GreaterConstraint[A <: Number, V <: A] extends Constraint[A, Greater[V]] {
override inline
def assert(value: A): Boolean = NumberOrdering.gt(value, constValue[V])
}
Note: NumberOrdering is an instance of InlineOrdering for Number. It offers the same features as a normal Ordering but inline.
Now, we need to create a given method returning an instance of our constraint:
inline given[A <: Number, V <: A]: GreaterConstraint[A, V] = new GreaterConstraint
You can now use your constraint:
inline def log(x: Double / Greater[0d]): Refined[Double] = x.map(Math.log)
Note: here, the log
method is inline to reduce overhead (log(x)
will desugar to Right(x).map(Math.log)
).
You can use Scala's type aliases to alias constraints. This can be useful for special cases or readability.
Example using the Greater
constraint:
type >[A, V] = A / Greater[V]
Usage:
inline def log(x: Double > 0d): Refined[Double] = x.map(Math.log)
//Desugars to
inline def log(x: Double / Greater[0d]): Refined[Double] = x.map(Math.log)
You can attach a description to a custom constraint alias using the DescribedAs[B, V]
type:
type Email = String / (Match["^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"] DescribedAs "Value should be an email")
Note: you can only use string literals in DescribedAs message.
You can create constraints that belong to a special algebra to allow creation of complex constraint equations.
To avoid conflict, algebraic constraints have a special algebra type, for example the trait MathAlgebra
.
An algebraic entry point is a starting point of an algebraic equation.
Its right value is always the input value of the equation (= the input of this Constrained
).
def foo(x: Double / (-1d < ?? < 1d)): Unit = ???
In this example, -1d < ??
is an entry point.
You can create an algebraic entry point by making its dummy extending AlgebraEntryPoint[T]
:
trait Less[V] extends AlgebraEntryPoint[MathAlgebra]
Example taken from the numeric
module.
An algebra part is part of the algebraic equation containing an algebra type (to avoid conflict with other algebras) and a right value.
Here is an example taken from the core of Iron:
trait Placehold[B, Alg, V] extends AlgebraPart[Alg, V]
An algebraic binary operator allows chaining with other binary operators of the same algebra. Iron provides a type alias to ease their creation:
type <[A, B] = BiOperator[A, B, MathAlgebra, Number, Less, Greater]
The parameters details are available on the Scaladoc.
Further information about algebraic constraints usage here