Skip to content

v2.0.0

Compare
Choose a tag to compare
@Iltotore Iltotore released this 29 Jan 15:31
· 117 commits to main since this release

Introduction

This is the new major release of Iron, featuring a complete rewrite of the library on top of better foundations:

  • Developer experience - Iron must be joyful to work with, making code cleaner and simpler to understand. This means simple API and no black magic.
  • Safety - Iron must ensure that all refined values are actually valids, allowing developers to focus on what's important.
  • Extension - Easily create your own constraints and integrations with other libraries.
  • Performance - Libraries like Iron must impact runtime performance as little as possible to be seamlessly integrated in any project of any size.

Features

Developer experience

Refined types are now subtypes of their inner type: you can use them like any "normal" types.

val x: Int :| Positive = 1
x + 1 //2

Scastie

Variance also works with refined types:

val x: List[Int :| Positive] = List.empty
val y: List[Int] = x

Scastie

Imports have been improved. You don't need to import given anymore:

import io.github.iltotore.iron.* //Mandatory: contains implicit conversions and refinement methods
import io.github.iltotore.iron.constraint.numeric.Positive //Import a specific constraint
import io.github.iltotore.iron.constraint.numeric.* //A whole category
import io.github.iltotore.iron.constraint.all.* //Or simply everything

Iron now has several constraints out of the box for:

  • Collections
  • Numbers
  • Characters
  • Strings

There are operators like Not. Iron also makes use of Scala 3's unions A | B (or) and intersections A & B (and).

Safety

In 2.0.0, refined values must be checked at compile-time. If a value does not fulfill its constraint or is not evaluable at runtime, the program will not compile:

val x: Int :| Positive = 1 //Compiles
val y: Int :| Positive = -1 //Does not compile because -1 is not positive
val z: Int :| Positive = runtimeValue //Does not compile because `runtimeValue` is (in this example) not evaluable at compile-time

Scastie

To refine values at runtime, the user must use refine, refineEither or refineOption extension types.

val runtimeValue: Int = 1
val anotherRuntimeValue: Int = -1

val x: Int :| Positive = runtimeValue.refine //OK
val y: Int :| Positive = anotherRuntimeValue.refine //thrown IllegalArgumentException: Should be positive

Scastie

See this page for further details.

Iron also provides constraint-to-constraint implications. For example, this code compiles:

val x: Int :| Greater[10] = runtimeValue.refine
val y: Int :| Positive = x //No need to check `x` because `Greater[10]` implies `Positive`

Scastie

Extension

Easily create your own constraints and implications. You can use already existing constraints combined with a type alias:

type Password =
  DescribedAs[
    Exists[Letter] & Exists[Digit] & Exists[Special],
    "Should contain at least a letter, a digit and a special character"
  ]

val x: String :| Password = "abc123*"

Scastie

Or building them from scratch:

final class Positive

inline given Constraint[Int, Positive] with

  override inline def test(value: Int): Boolean = value > 0

  override inline def message: String = "Should be positive"

val x: Int :| Positive = 5
val y: Int :| Positive = 0

Scastie

You can also create your own implications:

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

val x: Int :| Greater[5] = 6
val y: Int :| Greater[0] = x

Scastie

Performances

Refined types are no longer either-based and instead are an opaque type on top of their "unrefined" version. This mean they now completely disappear at compilation.

val x: Int :| Positive = 1

compiles to

val x: Int = 1

No overhead at runtime!

For runtime checking, only the condition remains:

val x: Int :| Positive = runtimeValue.refine

compiles to

val x: Int = if runtimeValue > 0 then x else throw IllegalArgumentException("Should be positive")

Note: there are functional variants like refineEither and refineOption.

Future releases

Iron took an entire year to rewrite and enhance! Moreover, this release breaks compatibility with Iron 1.x.

Now that Iron's codebase and foundations are much better and clearer, releases will come faster (when issues/suggestions are raised) and without breaking retrocompatibility. The semver conventions will be used:

  • Major (a..) changes for API-incompatible changes
  • Minor (.a.) new features, potentially binary incompatible changes (but API compatible)
  • Patch (..a) bug fixes and internal improvements

Full Changelog: https://github.com/Iltotore/iron/commits/v2.0.0

Thank you to all contributors

  • @gvolpe: ergonomics enhancement, suggested SJS support
  • @kyri-petrou: fixed and added some constraints (like Blank)
  • @andrzejressel: first use of macros for less-trivial constraints (like UpperCase)
  • @SethTisue: corrected README english
  • @nox213: fixed typo