NewTypeNewType
NewTypes for Scala with no runtime overhead.
Getting NewTypeGetting NewType
If you are using SBT, add the following line to your build file -
libraryDependencies += "io.estatico" %% "newtype" % "0.4.2"
For Maven or other build tools, see the Maven Central badge at the top of this README.
UsageUsage
For generating newtypes via the
@newtype macro, see @newtype macro For non-macro usage, see the section on Legacy encoding.
@newtype macro@newtype macro
As of newtype 0.2, you can now encode newtypes using the
@newtype macro. Its implementation and usage aligns closely with idiomatic Scala syntax, so IDE support just works out of the box.
import io.estatico.newtype.macros.newtype package object types { @newtype case class WidgetId(toInt: Int) }
This expands into a
type and companion
object definition, so newtypes must be defined in an
object or
package object.
The example above will generate code similar to the following -
package object types { type WidgetId = WidgetId.Type object WidgetId { type Repr = Int type Base = Any { type WidgetId$newtype } trait Tag extends Any type Type <: Base with Tag def apply(x: Int): WidgetId = x.asInstanceOf[WidgetId] implicit final class Ops$newtype(val $this$: Type) extends AnyVal { def toInt: Int = $this$.asInstanceOf[Int] } } }
You can also create newtypes which have type parameters -
@newtype case class EitherT[F[_], L, R](x: F[Either[L, R]])
Note that it is impossible to have your newtype extend any types, which makes sense since it has its own distinct type at compile time and at runtime is just the underlying value.
Also, since the
@newtype annotation gives your type a distinct type at compile-time, primitives will naturally box as they do when they are applied in any generic context. See the following section on
@newsubtype for unboxed primitive newtypes.
@newsubtype macro@newsubtype macro
As of newtype 0.4 you now have access to the
@newsubtype macro. Its usage is identical to
@newtype. The difference is that it functions as a subtype of the underlying type as opposed to having a completely different type at compile time. This may or may not be desirable, and it's recommended to use
@newtype if you're not entirely sure you actually need
@newsubtype.
The difference in the generated code is that
@newsubtype defines its
Base type defined as -
type Base = Repr
The main benefit of
@newsubtype is that primitives are unboxed. For example, the following
@newtype definition will box the
Int, making it a
java.lang.Integer at runtime -
@newtype case class Foo(x: Int)
However, the following
@newsubtype definition will be a primitive
int at runtime -
@newsubtype case class Bar(x: Int)
Note however that calling
getClass on a newsubtype will fool you -
scala> Bar(1).getClass res2: Class[_ <: Bar] = class java.lang.Integer
Reason is that scalac boxes unnecessarily when calling
getClass, see
We can confirm that we do in fact have a primitive
int at runtime back by inspecting the byte code -
scala> class Test { def test = Bar(1) } scala> :javap Test
... public int test(); ...
Another "feature" of
@newsubtype is that its values can be passed to functions which accept its
Repr type without needing to convert them first -
scala> def half(b: Int): Int = b / 2 scala> half(Bar(12)) res6: Int = 6
Note that this feature can be undesirable since the newsubtype will be automatically unwrapped, even when you might not mean to. Again, unless you have a good reason to use
@newsubtype, it's recommend to use
@newtype by default.
Smart Constructors and Accessor MethodsSmart Constructors and Accessor Methods
This library gives you a few choices when it comes to defining smart constructors and accessor methods for your newtypes. Efforts have been made to keep things idiomatic. Note that extractors (
unapply methods) are not generated by newtypes.
Using
case class gives us a smart constructor (an
apply method on the companion object) that will accept a value of type
A and return the newtype
N.
@newtype case class N(a: A)
You also get an accessor extension method to get the underlying
A. Note that you can prevent this by defining the field as private.
@newtype case class N(private val a: A)
Using
class will not generate a smart constructor (no
apply method). This allows you to specify your own. Note that
new never works for newtypes and will fail to compile.
@newtype class N(a: A) object N { def apply(a: A): N = ??? }
If you wish to generate an accessor method for your underlying value, you can define it as
val just as if you were dealing with a normal class.
@newtype class N(val a: A)
Extension MethodsExtension Methods
Defining extension methods are as simple as defining normal methods in any class -
@newtype case class OptionT[F[_], A](value: F[Option[A]]) { def fold[B](default: => B)(f: A => B)(implicit F: Functor[F]): F[B] = F.map(value)(_.fold(default)(f)) def cata[B](default: => B, f: A => B)(implicit F: Functor[F]): F[B] = fold(default)(f) def map[B](f: A => B)(implicit F: Functor[F]): OptionT[F, B] = OptionT(F.map(value)(_.map(f))) }
Companion ObjectsCompanion Objects
The companion object works just as you'd expect. You can place your type class instances there and implicit resolution just works.
Companion objects also contain special
deriving and
derivingK methods to auto-derive instances for you if one exists for your underlying type. This is similar to GHC Haskell's
GeneralizedNewtypeDeriving extension.
deriving is used for type classes whose type parameter is not higher kinded.
@newtype case class Text(s: String) object Text { implicit val arb: Arbitrary[Text] = deriving }
derivingK is used for type classes whose type parameter is higher kinded.
@newtype class Nel[A](val toList: List[A]) object Nel { def apply[A](head: A, tail: List[A]): Nel[A] = (head +: tail).coerce[Nel[A]] implicit val functor: Functor[Nel] = derivingK }
Note that since these methods are created by the
@newtype macro, IDEs will generally not be able to resolve them. If the red highlighting bothers you, you can use
.coerce to safely cast the base type class to support your newtype -
import io.estatico.newtype.ops._ @newtype case class Text(s: String) object Text { implicit val arb: Arbitrary[Text] = implicitly[Arbitrary[String]].coerce } @newtype class Nel[A](val toList: List[A]) object Nel { def apply[A](head: A, tail: List[A]): Nel[A] = (head +: tail).coerce[Nel[A]] implicit val functor: Functor[Nel] = implicitly[Functor[List]].coerce }
Automatically Deriving InstancesAutomatically Deriving Instances
In some cases, you want may want to automatically derive type class instances for all newtypes. You can take advantage of the
Coercible type class to achieve that.
Here is an example of automatically deriving
Eq instances for all newtypes.
scala> :paste import cats._, cats.implicits._ /** If we have an Eq instance for Repr type R, derive an Eq instance for NewType N. */ implicit def coercibleEq[R, N](implicit ev: Coercible[Eq[R], Eq[N]], R: Eq[R]): Eq[N] = ev(R) @newtype case class Foo(x: Int) // Exiting paste mode, now interpreting. scala> Foo(1) === Foo(2) res0: Boolean = false
Legacy encodingLegacy encoding
If you don't wish to use the macro API, you can still use the legacy API for building newtypes manually via companion objects. Note that this method does not support newtypes with type parameters. If you need type parameters, use the macro API.
The easiest way to get going with the legacy encoding is to create an object that extends from
NewType.Default -
import io.estatico.newtype.NewType object WidgetId extends NewType.Default[Int]
This will be the companion object for your newtype. Use the
.Type type member to get access to the type for signatures. A common pattern is to include this in a package object so it can be easily imported.
package object types { type WidgetId = WidgetId.Type object WidgetId extends NewType.Default[Int] }
Now you can import
types.WidgetId and use it in type signatures as well as the companion object.
NewType.Of vs.
NewType.Default
Extending
NewType.Of simply creates the newtype wrapper; however, you will often want to extend
NewType.Default to provide some helper methods on the companion object -
// Safely casts an Int to a WidgetId scala> WidgetId(1) res0: WidgetId.Type = 1 // Safely casts M[Int] to M[WidgetId] scala> WidgetId.applyM(List(1, 2)) res1: List[WidgetId.Type] = List(1, 2)
See
NewTypeExtras for the available mixins for creating newtype wrappers.
If you wish to do something different, you can supply your own smart-constructor instead -
object Nat extends NewType.Of[Int] { def apply(n: Int): Option[Type] = if (n < 0) None else Some(wrap(n)) }
The
wrap method you see here is actually just explicit usage of the implicit instance of
Coercible[Int, Nat.Type]. See the section on Coercible for more info.
Legacy extension methodsLegacy extension methods
You probably want to be able to add methods to your newtypes. You can do this using Scala's extension methods via implicit classes -
type Point = Point.Type object Point extends NewType.Of[(Int, Int)] { def apply(x: Int, y: Int): Type = wrap((x, y)) implicit final class Ops(val self: Type) extends AnyVal { def toTuple: (Int, Int) = unwrap(self) def x: Int = toTuple._1 def y: Int = toTuple._2 } }
scala> val p = Point(1, 2) p: Point.Type = (1,2) scala> p.toTuple res7: (Int, Int) = (1,2) scala> p.x res8: Int = 1 scala> p.y res9: Int = 2
Legacy type class instances and implicitsLegacy type class instances and implicits
As mentioned, the object you create via extending one of the
NewType helpers functions as the companion object for your newtype. As such, you can leverage this for type class instances to avoid orphan instances -
object Nat extends NewType.Of[Int] { implicit val show: Show[Type] = Show.instance(_.toString) }
If you use
NewType.Default, you can use the
deriving method to derive type class instances for those that exist for your newtype's base type.
object Nat extends NewType.Default[Int] { implicit def show: Show[Type] = deriving }
As long as an implicit instance of
Show[Int] exists in scope,
deriving will cast the instance to one suitable for your newtype. This is similar to GHC Haskell's
GeneralizedNewtypeDeriving extension.
Legacy NewSubTypeLegacy NewSubType
With
NewType, you get a brand new type that can't be used as the type you are wrapping.
type Nat = Nat.Type object Nat extends NewType.Default[Int] def plus(x: Int, y: Int): Int = x + y
scala> plus(Nat(1), Nat(2)) <console>:19: error: type mismatch; found : Nat.Type required: Int
If you wish for your newtype to be a subtype of the type you are wrapping, you can use
NewSubType -
type Nat = Nat.Type object Nat extends NewSubType.Default[Int] def plus(x: Int, y: Int): Int = x + y
scala> plus(Nat(1), Nat(2)) res0: Int = 3
CoercibleCoercible
This library introduces the
Coercible type class for types that can safely be cast to/from newtypes. This is mostly useful when you want to write code that can work generically with newtypes or to simply leverage the compiler to tell you when you can do
.asInstanceOf.
NOTE: You generally shouldn't be creating instances of Coercible yourself. This library is designed to create the instances needed for you which are safe. If you manually create instances, you may be permitting unsafe operations which will lead to runtime casting errors.
With that out of the way, here's how we can do safe casting with Coercible -
type Point = Point.Type object Point extends NewType.Of[(Int, Int)]
scala> Coercible[Point, (Int, Int)] res10: io.estatico.newtype.Coercible[Point,(Int, Int)] = io.estatico.newtype.Coercible$$anon$1@56c24c2a scala> Coercible[(Int, Int), Point] res11: io.estatico.newtype.Coercible[(Int, Int),Point] = io.estatico.newtype.Coercible$$anon$1@56c24c2a scala> Coercible[String, Point] <console>:21: error: could not find implicit value for parameter ev: io.estatico.newtype.Coercible[String,Point] Coercible[String, Point]
This library provides extension methods for safe casting as well -
scala> import io.estatico.newtype.ops._ import io.estatico.newtype.ops._ scala> val p = Point(1, 2) p: Point.Type = (1,2) scala> p.coerce[(Int, Int)] res14: (Int, Int) = (1,2) scala> (3, 4).coerce[Point] res15: Point = (3,4) scala> (3.2, 4.3).coerce[Point] <console>:24: error: could not find implicit value for parameter ev: io.estatico.newtype.Coercible[(Double, Double),Point] (3.2, 4.3).coerce[Point] ^
MotivationMotivation
The Haskell language provides a
newtype keyword for creating new types from existing ones without runtime overhead.
newtype WidgetId = WidgetId Int lookupWidget :: WidgetId -> Maybe Widget lookupWidget (WidgetId wId) = lookup wId widgetDB
In the example above, the
WidgetId type is simply an
Int at runtime; however, the compiler will treat it as its own type at compile time, helping you to avoid errors. In this case, we can be sure that the ID we are providing to our
lookupWidget function refers to a
WidgetId and not some other entity nor an arbitrary
Int value.
This library attempts to bring newtypes to Scala.
Tagged TypesTagged Types
Both Scalaz and Shapeless provide a feature known as Tagged Types. This library operates on roughly the same principle except provides the proper infrastructure needed to -
- Control whether newtypes are or are not subtypes of their wrapped type instead of picking a side (Shapeless' are subtypes, Scalaz's are not)
- Easily provide methods for newtypes
- Resolve implicits and type class instances defined in the companion object
- Optimize constructing newtypes via casting with automatic smart constructors
- Provide facilities to operate generically on newtypes
- Support safe casting generically via the
Coercibletype class