Creating New Types
You can create no-overhead new types like scala-newtype in Scala 2 with Iron.
RefinedType
Iron provides a convenient trait called RefinedType
to easily add smart constructors to your type:
type Temperature = Temperature.T
object Temperature extends RefinedType[Double, Positive]
val temperature = Temperature(15) //Compiles
println(temperature) //15
val positive: Double :| Positive = 15
val tempFromIron = Temperature(positive) //Compiles too
Runtime refinement
RefinedType
supports all refinement methods provided by Iron:
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:
val zioValidation: Validation[String, Temperature] = Temperature.validation(15)
Note: just like IronType, [[RefinedType|io.github.iltotore.iron.RefinedType] compiles to its base type.
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 = Temperature.applyUnsafe(runtimeValue)
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.
type FirstName = FirstName.T
object FirstName extends RefinedType[String, Pure]
val firstName = FirstName("whatever")
Opacity
A newtype is opaque. Therefore, it is only known in its definition file. It is not considered to be a type alias outside of it:
type Temperature = Temperature.T
object Temperature extends RefinedType[Double, Positive]
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:
type Temperature = Temperature.T
object Temperature extends RefinedType[Double, Positive]
type Moisture = Moisture.T
object Moisture extends RefinedType[Double, Positive]
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 RefinedType
:
type Temperature = Temperature.T
object Temperature extends RefinedType[Double, Positive]
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.*
type FirstName = FirstNamle
object FirstName extends RefinedType[String, ForAll[Letter]]
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:
type Temperature = Temperature.T
object Temperature extends RefinedType[Double, Positive]
To support such type, you can use the RefinedType.Mirror provided by each RefinedType
. It works the same as Scala 3's Mirror. Here is an example from the ZIO JSON module:
inline given[T](using mirror: RefinedType.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 RefinedType.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.