Creating New Types
You can create no-overhead new types like scala-newtype in Scala 2 with Iron.
RefinedTypeOps
Iron provides a convenient trait called RefinedTypeOps
to easily add smart constructors to your type:
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]
val temperature = Temperature(15) //Compiles
println(temperature) //15
val positive: Double :| Positive = 15
val tempFromIron = Temperature(positive) //Compiles too
For transparent type aliases, it is possible to use the RefinedTypeOps.Transparent
alias to avoid boilerplate.
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps.Transparent[Temperature]
Runtime refinement
RefinedTypeOps
supports all refinement methods provided by Iron:
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps.Transparent[Temperature]
val unsafeRuntime: Temperature = Temperature.applyUnsafe(15)
val option: Option[Temperature] = Temperature.option(15)
val either: Either[String, Temperature] = Temperature.either(15)
Constructors for other modules exist:
import zio.prelude.Validation
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
import io.github.iltotore.iron.zio.*
type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps.Transparent[Temperature]
val zioValidation: Validation[String, Temperature] = Temperature.validation(15)
Note: all these constructors are inline. They don't bring any overhead:
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps.Transparent[Temperature]
val temperature: Temperature = Temperature(15)
val runtimeValue: Double = ???
val unsafeRuntime: Temperature = Temperature.applyUnsafe(runtimeValue)
compiles to
val temperature: Double = 15
val runtimeValue: Double = ???
val unsafeRuntime: Double =
if runtimeValue > 0 then runtimeValue
else throw new IllegalArgumentException("...")
New type with no constraint
You can create a new type without restriction by using the Pure constraint. Pure is an alias for True, a constraint that is always satisfied.
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.any.Pure
type FirstName = String :| Pure
object FirstName extends RefinedTypeOps.Transparent[FirstName]
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.any.Pure
type FirstName = String :| Pure
object FirstName extends RefinedTypeOps.Transparent[FirstName]
val firstName = FirstName("whatever")
Opaque new types
The aliased type of an opaque type is only known in its definition file. It is not considered like a type alias outside of it:
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]
val x: Double :| Positive = 5
val temperature: Temperature = x //Error: Temperature expected, got Double :| Positive
Such encapsulation is especially useful to avoid mixing different domain types with the same refinement:
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]
opaque type Moisture = Double :| Positive
object Moisture extends RefinedTypeOps[Double, Positive, Moisture]
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]
opaque type Moisture = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Moisture]
case class Info(temperature: Temperature, moisture: Moisture)
val temperature: Temperature = ???
val moisture: Moisture = ???
Info(moisture, temperature) //Compile-time error
Therefore, it also forces the user to convert the value explicitly, for example using a smart constructor from RefinedTypeOps
:
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]
val value: Double :| Positive = ???
val a: Temperature = value //Compile-time error
val b: Temperature = Temperature(value) //OK
Inheriting base type
Assuming the following new type:
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*
opaque type FirstName = String :| ForAll[Letter]
object FirstName extends RefinedTypeOps[String, ForAll[Letter], FirstName]
We cannot use java.lang.String
's methods neither pass FirstName
as a String without using the value
extension method. In Scala 3, opaque types can be a subtype of their underlying type:
opaque type Foo <: String = String
object Foo:
def apply(value: String): Foo = value
opaque type Foo <: String = String
object Foo:
def apply(value: String): Foo = value
val x = Foo("abcd")
x.toUpperCase //"ABCD"
Therefore, you can combine it with RefinedTypeOps
:
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*
opaque type FirstName <: String :| ForAll[Letter] = String :| ForAll[Letter]
object FirstName extends RefinedTypeOps[String, ForAll[Letter], FirstName]
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*
opaque type FirstName <: String :| ForAll[Letter] = String :| ForAll[Letter]
object FirstName extends RefinedTypeOps[String, ForAll[Letter], FirstName]
val x = FirstName("Raphael")
x.toUpperCase //"RAPHAEL"
Typeclass derivation
Usually, transparent type aliases do not need a special handling for typeclass derivation as they can use the given instances for IronType
. However, this is not the case for opaque type aliases, for instance:
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]
To support such type, you can use the RefinedTypeOps.Mirror provided by each RefinedTypeOps
. It works the same as Scala 3's Mirror. Here is an example from the ZIO JSON module:
import zio.json.*
import io.github.iltotore.iron.*
inline given[T](using mirror: RefinedTypeOps.Mirror[T], ev: JsonDecoder[mirror.IronType]): JsonDecoder[T] =
ev.asInstanceOf[JsonDecoder[T]]
In this example, given a new type T
(like Temperature
defined above), an implicit instance of JsonEncoder
for its underlying IronType
(e.g Double :| Positive
) is got and returned.
The types provided by RefinedTypeOps.Mirror are:
BaseType
: the base (unrefined) type of the mirrored type.ConstraintType
: the constraint type of the mirrored new type.IronType
: an alias forBaseType :| ConstraintType
FinalType
: the underlying type of the mirrored new type. Equivalent to itsIronType
if the alias is not opaque.