Scala Matters, Paris
November 28th, 2023
Suppose you find this code in your codebase
case class IBAN(
countryCode: String,
checkDigits: String,
bankCode: String,
branchCode: String,
accountNumber: String,
nationalCheckDigit: String
)
This looks good
val iban = IBAN(
"FR",
"14",
"20041",
"01005",
"0500013M026",
"06"
)
Until you find something like this
val shuffled = IBAN(
"0500013M026",
"FR",
"06",
"14",
"20041",
"01005"
)
So, you try this
val wtf = IBAN(
"π«π·",
"β
",
"π¦",
"π³",
"π§Ύ",
"π€‘"
)
type CountryCode = String
type CheckDigits = String
type BankCode = String
type BranchCode = String
type AccountNumber = String
type NationalCheckDigit = String
case class IBAN(
countryCode: CountryCode,
checkDigits: CheckDigits,
bankCode: BankCode,
branchCode: BranchCode,
accountNumber: AccountNumber,
nationalCheckDigit: NationalCheckDigit
)
case class CountryCode(value: String) extends AnyVal
case class CheckDigits(value: String) extends AnyVal
case class BankCode(value: String) extends AnyVal
case class BranchCode(value: String) extends AnyVal
case class AccountNumber(value: String) extends AnyVal
case class NationalCheckDigit(value: String) extends AnyVal
This looks good
val iban = IBAN(
CountryCode("FR"),
CheckDigits("14"),
BankCode("20041"),
BranchCode("01005"),
AccountNumber("0500013M026"),
NationalCheckDigit("06")
)
And this cannot compile anymore
val shuffled = IBAN(
AccountNumber("0500013M026"),
CountryCode("FR"),
NationalCheckDigit("06"),
CheckDigits("14"),
BankCode("20041"),
BranchCode("01005")
)
But this one still compiles
val wtf = IBAN(
CountryCode("π«π·"),
CheckDigits("β
"),
BankCode("π¦"),
BranchCode("π³"),
AccountNumber("π§Ύ"),
NationalCheckDigit("π€‘")
)
case class CountryCode(value: String) extends AnyVal:
require(value.length == 2, "Country code must be 2 characters")
case class CheckDigits(value: String) extends AnyVal:
require(value.length == 2, "Check digits must be 2 characters")
case class BankCode(value: String) extends AnyVal:
require(value.length == 5, "Bank code must be 5 characters")
case class BranchCode(value: String) extends AnyVal:
require(value.length == 5, "Branch code must be 5 characters")
case class AccountNumber(value: String) extends AnyVal:
require(value.length == 11, "Account number must be 11 characters")
case class NationalCheckDigit(value: String) extends AnyVal:
require(value.length == 2, "National check digit must be 2 characters")
case class FormatError(reason: String)
extends Exception(reason), NoStackTrace
case class CountryCode(value: String) extends AnyVal
object CountryCode:
def parse(input: String): Either[FormatError, CountryCode] =
Either.cond(input.length == 2, CountryCode(input),
FormatError("Country code must be 2 characters"))
case class CheckDigits(value: String) extends AnyVal
object CheckDigits:
def parse(input: String): Either[FormatError, CheckDigits] =
Either.cond(input.length == 2, CheckDigits(input),
FormatError("Check digits must be 2 characters"))
case class BankCode(value: String) extends AnyVal
object BankCode:
def parse(input: String): Either[FormatError, BankCode] =
Either.cond(input.length == 5, BankCode(input),
FormatError("Bank code must be 5 characters"))
case class CountryCode(value: String) extends AnyVal
object CountryCode:
def parse(input: String): Either[FormatError, CountryCode] =
Either.cond(input.length == 2, CountryCode(input),
FormatError("Country code must be 2 characters"))
case class CheckDigits(value: String) extends AnyVal
object CheckDigits:
def parse(input: String): Either[FormatError, CheckDigits] =
Either.cond(input.length == 2, CheckDigits(input),
FormatError("Check digits must be 2 characters"))
case class BankCode(value: String) extends AnyVal
object BankCode:
def parse(input: String): Either[FormatError, BankCode] =
Either.cond(input.length == 5, BankCode(input),
FormatError("Bank code must be 5 characters"))
case class BranchCode(value: String) extends AnyVal
object BranchCode:
def parse(input: String): Either[FormatError, BranchCode] =
Either.cond(input.length == 5, BranchCode(input),
FormatError("Branch code must be 5 characters"))
case class AccountNumber(value: String) extends AnyVal
object AccountNumber:
def parse(input: String): Either[FormatError, AccountNumber] =
Either.cond(input.length == 11, AccountNumber(input),
FormatError("Account number must be 11 characters"))
case class NationalCheckDigit(value: String) extends AnyVal
object NationalCheckDigits:
def parse(input: String): Either[FormatError, NationalCheckDigits] =
Either.cond(input.length == 2, NationalCheckDigits(input),
FormatError("Notional check digits must be 2 characters"))
case class BranchCode(value: String) extends AnyVal
object BranchCode:
def parse(input: String): Either[FormatError, BranchCode] =
Either.cond(input.length == 5, BranchCode(input),
FormatError("Branch code must be 5 characters"))
case class AccountNumber(value: String) extends AnyVal
object AccountNumber:
def parse(input: String): Either[FormatError, AccountNumber] =
Either.cond(input.length == 11, AccountNumber(input),
FormatError("Account number must be 11 characters"))
case class NationalCheckDigit(value: String) extends AnyVal
object NationalCheckDigits:
def parse(input: String): Either[FormatError, NationalCheckDigits] =
Either.cond(input.length == 2, NationalCheckDigits(input),
FormatError("Notional check digits must be 2 characters"))
opaque type BranchCode <: String = String
object BranchCode:
inline def wrap(input: String): BranchCode = input
extension (value: BranchCode) inline def unwrap: String = value
def parse(input: String): Either[FormatError, BranchCode] =
Either.cond(input.length == 5, wrap(input),
FormatError("Branch code must be 5 characters"))
| Legible | Ordered | Valid | Pure | Performance | Concise | |
|---|---|---|---|---|---|---|
| Raw Classes | β | β | β | β | β | β |
| Type Aliases | β | β | β | β | β | β |
| Value Classes | β | β | β | β | β | β |
| VC + Require | β | β | β | β | β | β |
| VC + Either | β | β | β | β | β | β |
| Opaque types | β | β | β | β | β | β |

Composable type constraint library
Created in Scala 3 by RaphaΓ«l Fromentin
It enables binding constraints to a specific type
Composable type constraint library
final class Positive
import io.github.iltotore.iron.*
given Constraint[Int, Positive] with
override inline def test(value: Int): Boolean = value > 0
override inline def message: String = "Should be strictly positive"
//
Created in Scala 3 by RaphaΓ«l Fromentin
It enables binding constraints to a specific type
Composable type constraint library
final class Positive
import io.github.iltotore.iron.*
given Constraint[Int, Positive] with
override inline def test(value: Int): Boolean = value > 0
override inline def message: String = "Should be strictly positive"
val x: Int :| Positive = 1
//
Created in Scala 3 by RaphaΓ«l Fromentin
It enables binding constraints to a specific type
Composable type constraint library
final class Positive
import io.github.iltotore.iron.*
given Constraint[Int, Positive] with
override inline def test(value: Int): Boolean = value > 0
override inline def message: String = "Should be strictly positive"
val x: Int :| Positive = 1
//Compile-time error: Should be strictly positive
val y: Int :| Positive = -1
//
Created in Scala 3 by RaphaΓ«l Fromentin
It enables binding constraints to a specific type
Composable type constraint library
final class Positive
// ...
val x: Int :| Positive = 1
//Compile-time error: Should be strictly positive
val y: Int :| Positive = -1
val foo: Int :| (Positive & Less[42]) = 1
//
Created in Scala 3 by RaphaΓ«l Fromentin
It enables binding constraints to a specific type
Composable type constraint library
final class Positive
// ...
val x: Int :| Positive = 1
//Compile-time error: Should be strictly positive
val y: Int :| Positive = -1
val foo: Int :| (Positive & Less[42]) = 1
//Compile-time error: Should be strictly positive
val bar: Int :| (Positive & Less[42]) = -1
//
Created in Scala 3 by RaphaΓ«l Fromentin
It enables binding constraints to a specific type
Composable type constraint library
final class Positive
// ...
val x: Int :| Positive = 1
//Compile-time error: Should be strictly positive
val y: Int :| Positive = -1
val foo: Int :| (Positive & Less[42]) = 1
//Compile-time error: Should be strictly positive
val bar: Int :| (Positive & Less[42]) = -1
//Compile-time error: Should be less than 42
val baz: Int :| (Positive & Less[42]) = 123
//
Created in Scala 3 by RaphaΓ«l Fromentin
It enables binding constraints to a specific type
val value: Int = ???
val x: Int :| Greater[0] = value
inline val value = 2
val x: Int :| Greater[0] = value
val value = ???
val x: Int :| Greater[0] = value.refine
def createIBAN( countryCode: String,
checkDigits: String,
bankCode: String,
branchCode: String,
accountNumber: String,
nationalCheckDigit: String
): Either[String, User] =
for
ctr <- countryCode.refineEither[Alphanumeric & Length[Equals[2]]]
chk <- checkDigits.refineEither[Alphanumeric & Length[Equals[2]]]
ban <- bankCode.refineEither[Alphanumeric & Length[Equals[5]]]
bra <- branchCode.refineEither[Alphanumeric & Length[Equals[5]]]
acc <- accountNumber.refineEither[Alphanumeric & Length[Equals[11]]]
nck <- nationalCheckDigit.refineEither[Alphanumeric & Length[Equals[2]]]
yield IBAN(ctr, chk, ban, bra, acc, nck)
No implementation leak
opaque type Positive <: Int = Int :| Greater[0]
object Positive extends RefinedTypeOps[Int, Greater[0], Positive]
Constraint factorization
private type SatsConstraint =
GreaterEqual[0] & LessEqual[100000000 * 21000000]
opaque type Sats <: Long = Long :| SatsConstraint
object Sats extends RefinedTypeOps[Long, SatsConstraint, Sats]
How much time do we need to find a bug?
In production
In staging
Integration tests
Unit tests
Compilation time
| Legible | Ordered | Valid | Pure | Performance | Concise | Compiles | |
|---|---|---|---|---|---|---|---|
| Raw Classes | β | β | β | β | β | β | β |
| Type Aliases | β | β | β | β | β | β | β |
| Value Classes | β | β | β | β | β | β | β |
| VC + Require | β | β | β | β | β | β | β |
| VC + Either | β | β | β | β | β | β | β |
| Opaque types | β | β | β | β | β | β | β |
| Iron | β | β | β | β | β | β | β |
Validated, Either +
Parallel[F])Validation)final case class Tag(name: Tag.Name, value: Tag.Value)
object Tag:
private type NameConstraint = Not[Empty] & MaxLength[128]
opaque type Name <: String = String :| NameConstraint
object Name extends RefinedTypeOps[String, NameConstraint, Name]
private type ValueConstraint = Not[Empty] & MaxLength[512]
opaque type Value <: String = String :| ValueConstraint
object Value extends RefinedTypeOps[String, ValueConstraint, Value]
val getLatest = base
.name("Get latest account addresses")
.in(query[Option[Tag]]("tag"))
.get
.in("latest")
.out(jsonBody[Option[AddressView]])
def getLatestByTag(account: AccountId, name: Tag.Name, value: Tag.Value): ConnectionIO[Option[Position]] =
sql"""
select
account_id,
address,
coalesce(
(select jsonb_object_agg(tag_name, tag_value order by tag_name)
from position_tags pt where p.position_id = pt.position_id),
'{}'::jsonb),
sync_status
from positions p left join position_tags pt using (position_id)
where account_id = $account
and tag_name = $name
and tag_value = $value
order by position_id desc limit 1
""".query[Position].option
inline given [A, C]
(using inline meta: Meta[A])
(using Constraint[A, C], Show[A]): Meta[A :| C] =
meta.tiemap[A :| C](_.refineEither)(identity)
inline given [T]
(using m: RefinedTypeOps.Mirror[T], ev: Meta[m.IronType]): Meta[T] =
ev.asInstanceOf[Meta[T]]
Making illegal states unrepresentable
Scala 3 type system is incredibly powerful
Noticeably increased the reliability of our code