Disclaimer. It is not a full-featured introduction to Iron library.
The note highlights a couple of updates that seem very important to me. The updates are not released yet, and there is even one unassigned “first good issue” ticket. It could be a good thing to tackle for somebody.
I need to write down some words for those of us who, like me, occasionally write in Scala 3 and look for the upgrade with curiosity.
The context for the note is co-allocations of the facts:
- Scala 3 doesn’t support Scala 2 macro, macros-based features of scala-newtype and refined libraries are not working. It definitely hurt people who used to have everything neat with definitions like this:
@newtype case class FirstName(value: NonEmptyString)
opaquetypes were introduced. Initially, they were perceived (by some people, including me) as built-in tooling to have a native mechanism for Value Objects (in DDD terms). In practice, additional manipulations are required; opaque types are not equivalent to newtypes. Specifically, extra tricks are needed to enable standard
valuefunctions. trading/Newtype.scala is an example of how to turn on value objects back.
In case you aspire to acquire a richer understanding of the prior status quo and problems, I recommend reading the discussion: Improve opaque types (scala-lang.org).
Gabriel Volpe triggered
And more detailed motivation can be found in the original SIP-35 - OPAQUE TYPES.
I recommend quickly going through the README/docs of Iltotore/iron library.
Gabriel also has a short demo under the Trading project: IronDemo.scala:
type AgeR = DescribedAs[ Greater & Less, "Alien's age must be an integer between 1 and 150" ]
Long story short, you can define limitations as predicates and turn them into types. And convenient tooling help to perform validation against those types.
Let us have the definition:
opaque type Temperature = Double :| Positive object Temperature extends RefinedTypeOps[Temperature]
First of all, now we have
apply for cases when validation could be performed at compile time.
val temperature = Temperature(100)
Most likely, real usage will be the following:
val temperature = Temperature.either(runtimeVariable)
The point here is that a convenient companion object will pack the value into
The neat detail I want to mention is the implication of constraints.
val x: Double :| Positive = 5.0 val y: Double :| Greater = 15.0 val t1 = Temperature.fromIronType(x) val t2 = Temperature.fromIronType(y)
Double :| Greater is not of the same type but it is positive and could be used to create a
Temperature without explicit validation.
RefinedTypeOps[T] has a single parameter and looks for existing
IronType without explicitly mentioning parameters for
IronType itself! Very neat, and no macros are required.
type RefinedTypeOps[T] = T match case IronType[a, c] => RefinedTypeOpsImpl[a, c, T]
RefinedTypeOpsImpl handles some functions:
class RefinedTypeOpsImpl[A, C, T]: inline def apply(value: A)(using Constraint[A, C]): T = autoRefine[A, C](value).asInstanceOf[T] ...
Temperature is opaque type. By default, you can’t access the underlying value. So, you know that there is a
Double inside, but you have no access.
value function is here to help!
val t1 = Temperature(100) val t2 = Temperature(100) val result = t1.value + t2.value
value is enabled by extension for iron types with companion objects.
The cool thing here is that
value returns the underlying
IronType, and that type is the subtype of the underlying primitive.
t1.value + t2.valueis a legitimate operation considered the sum of two doubles. No
.value.valuecalls to reach primitives!
Double :| Positiveand can be used appropriately. E.x. use
fromIronTypewith inherent implication to re-pack into different iron types.
With all those features enabled, we can use iron as convenient tooling to build lightweight value objects on top of the opaque types.
The library focuses on precise validation, so better language should be derived.
True predicate (which is always valid) is in place already.
opaque type FirstName = String :| True object FirstName extends RefinedTypeOps[FirstName]
apply works for any value in runtime!
val firstName = FirstName(anyRuntimeString)
The next potential improvements are:
- Provide an alias for
Trueto depict that it is not a constraint but a tool to build value objects without validation:
opaque type FirstName = String :| Pure
- Make an alias for
A :| Pure. E.x.
opaque type FirstName = NewType[String].
The first option is convenient for initial domain modeling with the connotation of a placeholder that will be replaced later.
NewType simply explicitly says “no validation here, just value object”
I suggest jumping into iron/Issue#125: Add alias for True constraint and IronType[A, True] to share thoughts or implement it!
Thank you for reading!