v2.0.0
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
Variance also works with refined types:
val x: List[Int :| Positive] = List.empty
val y: List[Int] = x
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
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
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`
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*"
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
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
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