Skip to content

Constraint Usage

Iltotore edited this page Jul 8, 2021 · 17 revisions

This page informs on how to use Iron's constraints.

Basic usage

You can attach a constraint to:

  • Variables
  • Methods (Return type)
  • Parameters

using the type Constrained[A, B] or its alias ==>[A, B]:

inline def log(x: Double ==> Greater[0d]): Refined[Double] = x.map(Math.log)

You can also use type aliases:

type >[A, V] = A ==> Greater[V]

inline def log(x: Double > 0d): Refined[Double] = x.map(Math.log)

Note: You need to have a suitable constraint behaviour in the implicit scope.

Bad value handling

A Constrained[A, B] is a Refined[A] ensuring that A passed (successfully or not) the B constraint. Refined[A] is simply an alias for Either[IllegalValueError[A], A]. This Either-based structure allows functional error handling.

This functional approach allows constraints to synergise efficiently with other functional libraries like Cats or ZIO.

Here is an example using Iron and Cats for a user account:

case class Account(username: String, email: String, password: String)

object Account {

  //Here, the Match[String] constraint isn't natively implemented in Iron and is only used as example.
  
  //Type aliases are not mandatory. They can be used for readability purpose.
  type Username = String ==> Match["^[a-zA-Z0-9]+"]
  type Email = String ==> Match["^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"]
  //At least one upper, one lower and one number
  type Password = String ==> Match["^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])[a-zA-Z0-9]+"]
  
  def createAccount(username: Username, email: Email, password: Password): Refined[Account] = (
    username,
    email,
    password
  ).parMap3(Account.apply) //parMapN comes from Cats. This function is useful to build an instance using Eithers.
}

Usage:

//Right(Account(...))
val validAccount = Account.createAccount("Iltotore", "[email protected]", "SafePassword1")

//Left(IllegalValueError("Il_totore", constraintOfUsername))
val invalidAccount = Account.createAccount("Il_totore", "[email protected]", "SafePassword1")

Compile time evaluation

A constraint is evaluated at compile time if the value and the behaviour is fully inline. Otherwise, the compiler will inline as much as possible the constraint then evaluate at runtime.

Runtime fallback

The fallback behaviour can be configured using the -Diron.fallback argument:

  • error: Throw a compile time error
  • warn: Warn, then fallback to runtime
  • allow (default): Silently fallback to runtime

You can also configure this behaviour individually by extending the right trait:

  • Constraint.CompileTimeOnly: Throw a compile-time error if unable to evaluate.
  • Constraint.RuntimeOnly: Iron will not try to check this constraint at compile-time and directly fallback to runtime evaluation
Clone this wiki locally