Checkers is a library that adds type checks to Scala3 using the built-in intersection and union types. This allows handling checked types as if they were regular types that can be used as parameters or return types, respecting variance.
- Any type can be given a Checker instance to another type (preferably opaque).
- Checking a type to another means using that instance to return Valid or Invalid (from Cats)
- Functions working with checked types can be called with 'checking(...)' to be used more concretely
- The checking is done at RUNTIME but the type restrictions are represented at COMPILE TIME.
- NO compiletime checks for constant literals (more trouble than they are worth)
import checkers.{given, *}
Checking if a type T
is also a type X
returns Checked[T & X]
, an alias for ValidatedNel[FailedCheck, T & X]
val noSymbols = "some string".checkAs[AlphaNumeric]
// ==> Valid("some string": String & AlphaNumeric)
val manySymbols = "some |+| -- >>=".checkAs[AlphaNumeric]
// ==> Invalid('some |+| -- >>=' is not AlphaNumeric)
def safeDiv(numerator: Double, denominator: Double & NonZero): Double =
numerator / denominator
val result1 = safeDiv.checking(12D, 4D)
// ==> Valid(3D)
val result2 = safeDiv.checking(12D, 0D)
// ==> Invalid(denominator: '0D' not NonZero)
Intersection type checking behaves like and-ing the checks of their parts, adding up all errors if necessary
def isDrunk(age: Int & GT[21], drinkAlcohol: Float & Positive & LT[0.6F]): Boolean =
age - (age * drinkAlcohol) // Some arbitrary logic
val youngVsVodka = isDrunk.checking(24, 0.5F)
// ==> runs fine
val babyVsBeer = isDrunk.checking(2, 0.08F)
// ==> Invalid(age: '2' not GT[21]
val babyVsHardVodka = isDrunk.checking(2, 0.64F)
// ==> Invalid(age: '2' is not GT[21], drinkAlcohol: '0.64' is not LT[0.6])
val personVsNegative = isDrunk.checking(30, -0.34F)
// ==> Invalid(age: '-0.34' is not Positive)
object OtherFile {
opaque type Unitary = Any
inline given Checker[Float, Unitary] =
Checker { f => f >= -1 && f <= 1 }
}
object ClientCode {
import OtherFile.{given, *}
2F.checkAs[Unitary]
// ==> Invalid('2' is not Unitary)
0.43F.checkAs[Unitary]
// ==> Valid(0.43: Float & Unitary)
}