├── .gitignore ├── project ├── build.properties └── plugins.sbt ├── version.sbt ├── shared └── src │ ├── main │ └── scala │ │ └── io │ │ └── estatico │ │ └── newtype │ │ ├── ops │ │ ├── package.scala │ │ └── NewTypeOps.scala │ │ ├── NewSubType.scala │ │ ├── macros │ │ ├── newtype.scala │ │ ├── newsubtype.scala │ │ └── NewTypeMacros.scala │ │ ├── Coercible.scala │ │ ├── NewType.scala │ │ └── BaseNewType.scala │ └── test │ └── scala │ └── io │ └── estatico │ └── newtype │ ├── macros │ ├── package.scala │ └── NewTypeMacrosTest.scala │ └── NewTypeTest.scala ├── .travis.yml ├── jvm └── src │ └── test │ └── scala │ └── io │ └── estatico │ └── newtype │ └── macros │ └── NewTypeMacrosJVMTest.scala ├── cats-tests └── shared │ └── src │ └── test │ └── scala │ └── io │ └── estatico │ └── newtype │ └── NewTypeCatsTest.scala ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.3.2 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.4.5-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /shared/src/main/scala/io/estatico/newtype/ops/package.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype 2 | 3 | package object ops 4 | extends ops.ToNewTypeOps 5 | with ToCoercibleIdOps 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | scala: 4 | - 2.11.12 5 | - 2.12.10 6 | - 2.13.1 7 | 8 | jdk: 9 | - openjdk8 10 | 11 | branches: 12 | only: 13 | - master 14 | 15 | cache: 16 | directories: 17 | - $HOME/.ivy2/cache 18 | - $HOME/.sbt/boot 19 | - $HOME/.coursier 20 | -------------------------------------------------------------------------------- /shared/src/main/scala/io/estatico/newtype/NewSubType.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype 2 | 3 | /** A newtype which is a subtype of its Repr. */ 4 | trait NewSubType extends BaseNewType { 5 | type Base = Repr 6 | } 7 | 8 | object NewSubType { 9 | trait Of[R] extends BaseNewType.Of[R] with NewSubType 10 | trait Default[R] extends Of[R] with NewTypeExtras 11 | } 12 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1") 2 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.13") 3 | addSbtPlugin("com.dwijnand" % "sbt-travisci" % "1.2.0") 4 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.8.1") 5 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.1") 6 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") 7 | -------------------------------------------------------------------------------- /shared/src/main/scala/io/estatico/newtype/macros/newtype.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype.macros 2 | 3 | import scala.annotation.StaticAnnotation 4 | 5 | class newtype( 6 | optimizeOps: Boolean = true, 7 | unapply: Boolean = false, 8 | debug: Boolean = false, 9 | debugRaw: Boolean = false 10 | ) extends StaticAnnotation { 11 | def macroTransform(annottees: Any*): Any = macro NewTypeMacros.newtypeAnnotation 12 | } 13 | -------------------------------------------------------------------------------- /shared/src/main/scala/io/estatico/newtype/macros/newsubtype.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype.macros 2 | 3 | import scala.annotation.StaticAnnotation 4 | 5 | class newsubtype( 6 | optimizeOps: Boolean = true, 7 | unapply: Boolean = false, 8 | debug: Boolean = false, 9 | debugRaw: Boolean = false 10 | ) extends StaticAnnotation { 11 | def macroTransform(annottees: Any*): Any = macro NewTypeMacros.newsubtypeAnnotation 12 | } 13 | 14 | -------------------------------------------------------------------------------- /shared/src/test/scala/io/estatico/newtype/macros/package.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype 2 | 3 | package object macros { 4 | 5 | // This will trigger "it is not recommended to define 6 | // classes/objects inside of package objects" if the macro 7 | // generates an indirection trait on scala 2.11+ 8 | @newtype case class TestForUnwantedIndirection(x: Int) 9 | @newsubtype case class TestForUnwantedIndirectionSubtype(x: Int) 10 | 11 | } 12 | -------------------------------------------------------------------------------- /shared/src/main/scala/io/estatico/newtype/ops/NewTypeOps.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype 2 | package ops 3 | 4 | final class NewTypeOps[B, T, R]( 5 | private val self: BaseNewType.Aux[B, T, R] 6 | ) extends AnyVal { 7 | type Type = BaseNewType.Aux[B, T, R] 8 | def repr: R = self.asInstanceOf[R] 9 | def withRepr(f: R => R): Type = f(self.asInstanceOf[R]).asInstanceOf[Type] 10 | } 11 | 12 | trait ToNewTypeOps { 13 | implicit def toNewTypeOps[B, T, R]( 14 | x: BaseNewType.Aux[B, T, R] 15 | ): NewTypeOps[B, T, R] = new NewTypeOps[B, T, R](x) 16 | } 17 | -------------------------------------------------------------------------------- /jvm/src/test/scala/io/estatico/newtype/macros/NewTypeMacrosJVMTest.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype.macros 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class NewTypeMacrosJVMTest extends AnyFlatSpec with Matchers { 7 | 8 | behavior of "@newsubtype" 9 | 10 | it should "not box primitives" in { 11 | // Introspect the runtime type returned by the `apply` method 12 | def ctorReturnType(o: Any) = scala.Predef.genericArrayOps(o.getClass.getMethods).find(_.getName == "apply").get.getReturnType 13 | 14 | // newtypes will box primitive values. 15 | @newtype case class BoxedInt(private val x: Int) 16 | ctorReturnType(BoxedInt) shouldBe scala.Predef.classOf[Object] 17 | 18 | // newsubtypes will NOT box primitive values. 19 | @newsubtype case class UnboxedInt(private val x: Int) 20 | ctorReturnType(UnboxedInt) shouldBe scala.Predef.classOf[Int] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /shared/src/main/scala/io/estatico/newtype/Coercible.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype 2 | 3 | /** Safe type casting from A to B. */ 4 | trait Coercible[A, B] { 5 | @inline final def apply(a: A): B = a.asInstanceOf[B] 6 | } 7 | 8 | object Coercible { 9 | 10 | def apply[A, B](implicit ev: Coercible[A, B]): Coercible[A, B] = ev 11 | 12 | def instance[A, B]: Coercible[A, B] = _instance.asInstanceOf[Coercible[A, B]] 13 | 14 | private val _instance = new Coercible[Any, Any] {} 15 | 16 | // Support nested type constructors 17 | implicit def unsafeWrapMM[M1[_], M2[_], A, B]( 18 | implicit ev: Coercible[M2[A], M2[B]] 19 | ): Coercible[M1[M2[A]], M1[M2[B]]] = Coercible.instance 20 | } 21 | 22 | final class CoercibleIdOps[A](private val repr: A) extends AnyVal { 23 | @inline def coerce[B](implicit ev: Coercible[A, B]): B = repr.asInstanceOf[B] 24 | } 25 | 26 | trait ToCoercibleIdOps { 27 | @inline implicit def toCoercibleIdOps[A](a: A): CoercibleIdOps[A] = new CoercibleIdOps(a) 28 | } 29 | -------------------------------------------------------------------------------- /shared/src/main/scala/io/estatico/newtype/NewType.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype 2 | 3 | import io.estatico.newtype.ops.NewTypeOps 4 | 5 | trait NewType extends BaseNewType { self => 6 | type Base = { type Repr = self.Repr } 7 | } 8 | 9 | object NewType { 10 | trait Of[R] extends BaseNewType.Of[R] with NewType 11 | trait Default[R] extends Of[R] with NewTypeExtras 12 | } 13 | 14 | trait NewTypeAutoOps extends BaseNewType { 15 | implicit def toNewTypeOps( 16 | x: Type 17 | ): NewTypeOps[Base, Tag, Repr] = new NewTypeOps[Base, Tag, Repr](x) 18 | } 19 | 20 | trait NewTypeApply extends BaseNewType { 21 | /** Convert a `Repr` to a `Type`. */ 22 | @inline final def apply(x: Repr): Type = x.asInstanceOf[Type] 23 | } 24 | 25 | trait NewTypeApplyM extends NewTypeApply { 26 | /** Convert an `M[Repr]` to a `M[Type]`. */ 27 | @inline final def applyM[M[_]](mx: M[Repr]): M[Type] = mx.asInstanceOf[M[Type]] 28 | } 29 | 30 | trait NewTypeDeriving extends BaseNewType { 31 | /** Derive an instance of type class `T` if one exists for `Repr`. */ 32 | def deriving[T[_]](implicit ev: T[Repr]): T[Type] = ev.asInstanceOf[T[Type]] 33 | } 34 | 35 | trait NewTypeExtras 36 | extends NewTypeApplyM 37 | with NewTypeDeriving 38 | with NewTypeAutoOps 39 | -------------------------------------------------------------------------------- /shared/src/main/scala/io/estatico/newtype/BaseNewType.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype 2 | 3 | import scala.reflect.ClassTag 4 | 5 | /** Base skeleton for building newtypes. */ 6 | trait BaseNewType { 7 | type Base 8 | type Repr 9 | trait Tag 10 | final type Type = BaseNewType.Aux[Base, Tag, Repr] 11 | 12 | // Define Coercible instances for which we can safely cast to/from. 13 | @inline implicit def wrap: Coercible[Repr, Type] = Coercible.instance 14 | @inline implicit def unwrap: Coercible[Type, Repr] = Coercible.instance 15 | @inline implicit def wrapM[M[_]]: Coercible[M[Repr], M[Type]] = Coercible.instance 16 | @inline implicit def unwrapM[M[_]]: Coercible[M[Type], M[Repr]] = Coercible.instance 17 | @inline implicit def convert[N <: BaseNewType.Aux[_, _, Repr]]: Coercible[Type, N] = Coercible.instance 18 | // Avoid ClassCastException with Array types by prohibiting Array coercing. 19 | @inline implicit def cannotWrapArrayAmbiguous1: Coercible[Array[Repr], Array[Type]] = Coercible.instance 20 | @inline implicit def cannotWrapArrayAmbiguous2: Coercible[Array[Repr], Array[Type]] = Coercible.instance 21 | @inline implicit def cannotUnwrapArrayAmbiguous1: Coercible[Array[Type], Array[Repr]] = Coercible.instance 22 | @inline implicit def cannotUnwrapArrayAmbiguous2: Coercible[Array[Type], Array[Repr]] = Coercible.instance 23 | } 24 | 25 | object BaseNewType { 26 | /** `Type` implementation for all newtypes; see `BaseNewType`. */ 27 | type Aux[B, T, R] <: B with Meta[T, R] 28 | trait Meta[T, R] 29 | 30 | /** Helper trait to refine Repr via a type parameter. */ 31 | trait Of[R] extends BaseNewType { 32 | final type Repr = R 33 | } 34 | 35 | // Since Aux is abstract, this is necessary to make Arrays work. 36 | @inline implicit def classTag[B, T, R](implicit base: ClassTag[B]): ClassTag[Aux[B, T, R]] = 37 | ClassTag(base.runtimeClass) 38 | } 39 | -------------------------------------------------------------------------------- /shared/src/test/scala/io/estatico/newtype/NewTypeTest.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype 2 | 3 | import org.scalacheck.Arbitrary 4 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | class NewTypeTest extends AnyFlatSpec with ScalaCheckPropertyChecks with Matchers { 9 | 10 | import NewTypeTest._ 11 | 12 | "NewType" should "create a type with no runtime overhead" in { 13 | object NatInt extends NewType.Of[Int] { 14 | def apply(i: Int): Option[Type] = if (i < 0) None else wrapM(Some(i)) 15 | } 16 | NatInt(1) shouldEqual Some(1) 17 | NatInt(-1) shouldEqual None 18 | } 19 | 20 | it should "not be a subtype of its Repr" in { 21 | type Foo = Foo.Type 22 | object Foo extends NewType.Default[Int] 23 | assertCompiles("Foo(1): Foo") 24 | assertDoesNotCompile("Foo(1): Int") 25 | } 26 | 27 | it should "find implicit instances" in { 28 | type Box = Box.Type 29 | object Box extends NewType.Of[String] with NewTypeDeriving { 30 | implicit val arb: Arbitrary[Type] = deriving[Arbitrary] 31 | } 32 | scala.Predef.implicitly[Arbitrary[Box]].arbitrary.sample shouldBe defined 33 | } 34 | 35 | it should "support user ops" in { 36 | GoodInt(3).cube shouldEqual 27 37 | } 38 | 39 | it should "be Coercible" in { 40 | type Foo = Foo.Type 41 | object Foo extends NewType.Default[Int] 42 | 43 | // Using type annotations to prove that coerce methods return the right type. 44 | 45 | (Foo.wrap(1): Foo) shouldEqual 1 46 | (Foo.unwrap(Foo(1)): Int) shouldEqual 1 47 | (Foo.wrapM(List(1)): List[Foo]) shouldEqual List(1) 48 | (Foo.unwrapM(List(Foo(1))): List[Int]) shouldEqual List(1) 49 | 50 | import io.estatico.newtype.ops._ 51 | 52 | (1.coerce[Foo]: Foo) shouldEqual 1 53 | (Foo(1).coerce[Int]: Int) shouldEqual 1 54 | (List(1).coerce[List[Foo]]: List[Foo]) shouldEqual List(1) 55 | (List(Foo(1)).coerce[List[Int]]: List[Int]) shouldEqual List(1) 56 | } 57 | 58 | it should "work in Arrays" in { 59 | type Foo = Foo.Type 60 | object Foo extends NewType.Default[Int] 61 | 62 | val foo = Foo(42) 63 | Array(foo).apply(0) shouldEqual foo 64 | } 65 | 66 | "NewTypeApply" should "automatically create an apply method" in { 67 | object PersonId extends NewType.Of[Int] with NewTypeApply 68 | PersonId(1) shouldEqual 1 69 | } 70 | 71 | "DefaultNewType" should "get NewTypeOps" in { 72 | object Gold extends NewType.Default[Double] 73 | val gold = Gold(34.56) 74 | gold.repr shouldEqual 34.56 75 | gold.withRepr(_ / 2) shouldEqual Gold(17.28) 76 | } 77 | 78 | "NewTypeOps" should "not be available without extending NewTypeAutoOps or importing ops._" in { 79 | object Simple extends NewType.Of[Int] with NewTypeApply 80 | assertCompiles("Simple(1)") 81 | assertDoesNotCompile("Simple(1).repr") 82 | assertCompiles(""" 83 | import io.estatico.newtype.ops._ 84 | Simple(1).repr 85 | """) 86 | object HasOps extends NewType.Of[Int] with NewTypeApply with NewTypeAutoOps 87 | assertCompiles("HasOps(1).repr") 88 | assertCompiles(""" 89 | import io.estatico.newtype.ops._ 90 | HasOps(1).repr 91 | """) 92 | } 93 | 94 | "NewSubType" should "be a subtype of its Repr" in { 95 | type Foo = Foo.Type 96 | object Foo extends NewSubType.Of[String] with NewTypeApply 97 | assertCompiles("""Foo("bar"): Foo""") 98 | assertCompiles("""Foo("bar"): String""") 99 | Foo("bar").toUpperCase shouldEqual "BAR" 100 | Foo("bar").toUpperCase shouldEqual Foo("BAR") 101 | } 102 | 103 | it should "be Coercible" in { 104 | type Foo = Foo.Type 105 | object Foo extends NewSubType.Default[Int] 106 | 107 | // Using type annotations to prove that coerce methods return the right type. 108 | 109 | (Foo.wrap(1): Foo) shouldEqual 1 110 | (Foo.unwrap(Foo(1)): Int) shouldEqual 1 111 | (Foo.wrapM(List(1)): List[Foo]) shouldEqual List(1) 112 | (Foo.unwrapM(List(Foo(1))): List[Int]) shouldEqual List(1) 113 | 114 | import io.estatico.newtype.ops._ 115 | 116 | (1.coerce[Foo]: Foo) shouldEqual 1 117 | (Foo(1).coerce[Int]: Int) shouldEqual 1 118 | (List(1).coerce[List[Foo]]: List[Foo]) shouldEqual List(1) 119 | (List(Foo(1)).coerce[List[Int]]: List[Int]) shouldEqual List(1) 120 | } 121 | 122 | it should "work in Arrays" in { 123 | type Foo = Foo.Type 124 | object Foo extends NewSubType.Default[Int] 125 | 126 | val foo = Foo(-273) 127 | Array(foo).apply(0) shouldEqual foo 128 | } 129 | 130 | "Coercible" should "work across newtypes" in { 131 | type Foo = Foo.Type 132 | object Foo extends NewType.Default[Int] 133 | 134 | type Bar = Bar.Type 135 | object Bar extends NewType.Default[Int] 136 | 137 | import io.estatico.newtype.ops._ 138 | 139 | Foo(1).coerce[Bar] shouldEqual 1 140 | Bar(2).coerce[Foo] shouldEqual 2 141 | } 142 | 143 | "Coercible" should "not allow coercion of Array types" in { 144 | type Foo = Foo.Type 145 | object Foo extends NewType.Default[Int] 146 | 147 | // JVM will throw ClassCastException, JS will throw UndefinedBehaviorError 148 | a [Throwable] should be thrownBy Array(Foo(1)).asInstanceOf[Array[Int]] 149 | 150 | assertDoesNotCompile("Coercible[Array[Int], Array[Foo]]") 151 | assertDoesNotCompile("Coercible[Array[Foo], Array[Int]]") 152 | } 153 | } 154 | 155 | object NewTypeTest { 156 | 157 | type GoodInt = GoodInt.Type 158 | object GoodInt extends NewType.Default[Int] { 159 | implicit final class Ops(private val me: GoodInt) extends AnyVal { 160 | def cube: GoodInt = { 161 | val i = unwrap(me) 162 | wrap(i * i * i) 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /cats-tests/shared/src/test/scala/io/estatico/newtype/NewTypeCatsTest.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype 2 | 3 | import cats._ 4 | import cats.implicits._ 5 | import io.estatico.newtype.ops._ 6 | import io.estatico.newtype.macros.{newsubtype, newtype} 7 | import org.scalatest.flatspec.AnyFlatSpec 8 | import org.scalatest.matchers.should.Matchers 9 | import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks 10 | 11 | class NewTypeCatsTest extends AnyFlatSpec with Matchers with ScalaCheckPropertyChecks { 12 | 13 | import NewTypeCatsTest._ 14 | 15 | behavior of "Functor[Nel]" 16 | 17 | it should "be the same as Functor[List]" in { 18 | Functor[Nel] shouldBe Functor[List] 19 | } 20 | 21 | it should "get extension methods" in { 22 | Nel.of(1, 2, 3).map(_ * 2) shouldBe Nel.of(2, 4, 6) 23 | } 24 | 25 | behavior of "Monad[Nel]" 26 | 27 | it should "be the same as Monad[List]" in { 28 | Monad[Nel] shouldBe Monad[List] 29 | } 30 | 31 | it should "get extension methods" in { 32 | 1.pure[Nel] shouldBe Nel.of(1) 33 | Nel.of(1, 2, 3).flatMap(x => Nel.of(x, x * 2)) shouldBe 34 | Nel.of(1, 2, 2, 4, 3, 6) 35 | } 36 | 37 | it should "work in for comprehensions" in { 38 | val res = for { 39 | x <- Nel.of(1, 2, 3) 40 | y <- Nel.of(x, x * 2) 41 | } yield x + y 42 | 43 | res shouldBe Nel.of(2, 3, 4, 6, 6, 9) 44 | } 45 | 46 | it should "work in the same scope in which it is defined" in { 47 | testNelTypeAliasExpansion shouldBe testNelTypeAliasExpansionExpectedResult 48 | } 49 | 50 | "Monoid[Nel[A]]" should "work" in { 51 | Nel.of(1, 2, 3).combine(Nel.of(4, 5, 6)) shouldBe Nel.of(1, 2, 3, 4, 5, 6) 52 | } 53 | 54 | "Show[Nel]" should "work" in { 55 | Nel.of(1, 2, 3).show shouldBe "Nel(1,2,3)" 56 | } 57 | 58 | "Monoid[Sum]" should "work" in { 59 | Monoid[Sum].empty shouldBe 0 60 | List(2, 3, 4).coerce[List[Sum]].combineAll shouldBe 9 61 | } 62 | 63 | "Monoid[Prod]" should "work" in { 64 | Monoid[Prod].empty shouldBe 1 65 | List(2, 3, 4).coerce[List[Prod]].combineAll shouldBe 24 66 | } 67 | 68 | "Monoid[SumN[A]]" should "work" in { 69 | Monoid[SumN[Double]].empty shouldBe 0d 70 | List(2d, 3d, 4d).coerce[List[SumN[Double]]].combineAll shouldBe 9d 71 | } 72 | 73 | "Monoid[ProdN[A]]" should "work" in { 74 | Monoid[ProdN[Double]].empty shouldBe 1d 75 | List(2d, 3d, 4d).coerce[List[ProdN[Double]]].combineAll shouldBe 24d 76 | } 77 | 78 | behavior of "SubNel[A]" 79 | 80 | it should "be a subtype of List[A]" in { 81 | def unsafeHead[A](xs: List[A]) = xs.head 82 | unsafeHead(SubNel.of(1, 2, 3)) shouldBe 1 83 | 84 | def sum(xs: List[Int]) = xs.sum 85 | sum(SubNel.of(1, 2, 3)) shouldBe 6 86 | } 87 | 88 | behavior of "Functor[SubNel]" 89 | 90 | it should "be the same as Functor[List]" in { 91 | Functor[SubNel] shouldBe Functor[List] 92 | } 93 | 94 | it should "get extension methods" in { 95 | SubNel.of(1, 2, 3).map(_ * 2) shouldBe SubNel.of(2, 4, 6) 96 | } 97 | 98 | behavior of "Monad[SubNel]" 99 | 100 | it should "be the same as Monad[List]" in { 101 | Monad[SubNel] shouldBe Monad[List] 102 | } 103 | 104 | it should "get extension methods" in { 105 | 1.pure[SubNel] shouldBe SubNel.of(1) 106 | SubNel.of(1, 2, 3).flatMap(x => SubNel.of(x, x * 2)) shouldBe 107 | SubNel.of(1, 2, 2, 4, 3, 6) 108 | } 109 | 110 | it should "work in for comprehensions" in { 111 | val res = for { 112 | x <- SubNel.of(1, 2, 3) 113 | y <- SubNel.of(x, x * 2) 114 | } yield x + y 115 | 116 | res shouldBe SubNel.of(2, 3, 4, 6, 6, 9) 117 | } 118 | 119 | it should "work in the same scope in which it is defined" in { 120 | testSubNelTypeAliasExpansion shouldBe testSubNelTypeAliasExpansionExpectedResult 121 | } 122 | 123 | "Monoid[SubNel[A]]" should "work" in { 124 | SubNel.of(1, 2, 3).combine(SubNel.of(4, 5, 6)) shouldBe SubNel.of(1, 2, 3, 4, 5, 6) 125 | } 126 | 127 | "Coercible" should "support automatic type class derivation" in { 128 | implicit def coercibleShow[R, N](implicit ev: Coercible[Show[R], Show[N]], R: Show[R]): Show[N] = ev(R) 129 | @newtype case class Foo(private val x: Int) 130 | forAll { (n: Int) => Foo(n).show shouldBe n.show } 131 | Show[Foo] shouldBe Show[Int] 132 | } 133 | } 134 | 135 | object NewTypeCatsTest { 136 | 137 | @newtype class Nel[A](val toList: List[A]) { 138 | def head: A = toList.head 139 | def tail: List[A] = toList.tail 140 | def iterator: Iterator[A] = toList.iterator 141 | } 142 | object Nel { 143 | def apply[A](head: A, tail: List[A]): Nel[A] = (head +: tail).coerce 144 | def of[A](head: A, tail: A*): Nel[A] = (head +: tail.toList).coerce 145 | implicit def show[A](implicit A: Show[A]): Show[Nel[A]] = new Show[Nel[A]] { 146 | def show(nel: Nel[A]): String = "Nel(" + nel.iterator.map(A.show).mkString(",") + ")" 147 | } 148 | implicit def monoid[A]: Monoid[Nel[A]] = deriving 149 | implicit val monad: Monad[Nel] = derivingK 150 | } 151 | 152 | // See https://github.com/scala/bug/issues/10750 153 | private val testNelTypeAliasExpansion = for { 154 | x <- Nel.of(1, 2, 3) 155 | y <- Nel.of(x, x * 2) 156 | } yield x + y 157 | 158 | private val testNelTypeAliasExpansionExpectedResult = Nel.of(2, 3, 4, 6, 6, 9) 159 | 160 | @newsubtype case class Sum(value: Int) 161 | object Sum { 162 | implicit val monoid: Monoid[Sum] = new Monoid[Sum] { 163 | override def empty: Sum = Sum(0) 164 | override def combine(x: Sum, y: Sum): Sum = Sum(x.value + y.value) 165 | } 166 | } 167 | 168 | @newsubtype case class Prod(value: Int) 169 | object Prod { 170 | implicit val monoid: Monoid[Prod] = new Monoid[Prod] { 171 | override def empty: Prod = Prod(1) 172 | override def combine(x: Prod, y: Prod): Prod = Prod(x.value * y.value) 173 | } 174 | } 175 | 176 | @newsubtype case class SumN[A](value: A) 177 | object SumN { 178 | implicit def monoid[A](implicit A: Numeric[A]): Monoid[SumN[A]] = new Monoid[SumN[A]] { 179 | override def empty: SumN[A] = SumN[A](A.fromInt(0)) 180 | override def combine(x: SumN[A], y: SumN[A]): SumN[A] = SumN[A](A.plus(x, y)) 181 | } 182 | } 183 | 184 | @newsubtype case class ProdN[A](value: A) 185 | object ProdN { 186 | implicit def monoid[A](implicit A: Numeric[A]): Monoid[ProdN[A]] = new Monoid[ProdN[A]] { 187 | override def empty: ProdN[A] = ProdN[A](A.fromInt(1)) 188 | override def combine(x: ProdN[A], y: ProdN[A]): ProdN[A] = ProdN[A](A.times(x, y)) 189 | } 190 | } 191 | 192 | /** Same as [[Nel]] except also a subtype of List[A] */ 193 | @newsubtype case class SubNel[A](toList: List[A]) { 194 | def head: A = toList.head 195 | def tail: List[A] = toList.tail 196 | def iterator: Iterator[A] = toList.iterator 197 | } 198 | object SubNel { 199 | def apply[A](head: A, tail: List[A]): SubNel[A] = (head +: tail).coerce 200 | def of[A](head: A, tail: A*): SubNel[A] = (head +: tail.toList).coerce 201 | implicit def show[A](implicit A: Show[A]): Show[SubNel[A]] = new Show[SubNel[A]] { 202 | def show(nel: SubNel[A]): String = "SubNel(" + nel.iterator.map(A.show).mkString(",") + ")" 203 | } 204 | implicit def monoid[A]: Monoid[SubNel[A]] = deriving 205 | implicit val monad: Monad[SubNel] = derivingK 206 | } 207 | 208 | // See https://github.com/scala/bug/issues/10750 209 | private val testSubNelTypeAliasExpansion = for { 210 | x <- SubNel.of(1, 2, 3) 211 | y <- SubNel.of(x, x * 2) 212 | } yield x + y 213 | 214 | private val testSubNelTypeAliasExpansionExpectedResult = SubNel.of(2, 3, 4, 6, 6, 9) 215 | } 216 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /shared/src/main/scala/io/estatico/newtype/macros/NewTypeMacros.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype.macros 2 | 3 | import io.estatico.newtype.Coercible 4 | import scala.reflect.ClassTag 5 | import scala.reflect.macros.blackbox 6 | 7 | private[macros] class NewTypeMacros(val c: blackbox.Context) { 8 | 9 | import c.universe._ 10 | 11 | def newtypeAnnotation(annottees: Tree*): Tree = 12 | runAnnotation(subtype = false, annottees) 13 | 14 | def newsubtypeAnnotation(annottees: Tree*): Tree = 15 | runAnnotation(subtype = true, annottees) 16 | 17 | def runAnnotation(subtype: Boolean, annottees: Seq[Tree]): Tree = { 18 | val (name, result) = annottees match { 19 | case List(clsDef: ClassDef) => 20 | (clsDef.name, runClass(clsDef, subtype)) 21 | case List(clsDef: ClassDef, modDef: ModuleDef) => 22 | (clsDef.name, runClassWithObj(clsDef, modDef, subtype)) 23 | case _ => 24 | fail(s"Unsupported @$macroName definition") 25 | } 26 | if (debug) scala.Predef.println(s"Expanded @$macroName $name:\n" + show(result)) 27 | if (debugRaw) scala.Predef.println(s"Expanded @$macroName $name (raw):\n" + showRaw(result)) 28 | result 29 | } 30 | 31 | val CoercibleCls = typeOf[Coercible[Nothing, Nothing]].typeSymbol 32 | val CoercibleObj = CoercibleCls.companion 33 | val ClassTagCls = typeOf[ClassTag[Nothing]].typeSymbol 34 | val ClassTagObj = ClassTagCls.companion 35 | val ObjectCls = typeOf[Object].typeSymbol 36 | 37 | // We need to know if the newtype is defined in an object so we can report 38 | // an error message if methods are defined on it (otherwise, the user will 39 | // get a cryptic error of 'value class may not be a member of another class' 40 | // due to our generated extension methods. 41 | val isDefinedInObject = c.internal.enclosingOwner.isModuleClass 42 | 43 | val macroName: Tree = { 44 | c.prefix.tree match { 45 | case Apply(Select(New(name), _), _) => name 46 | case _ => c.abort(c.enclosingPosition, "Unexpected macro application") 47 | } 48 | } 49 | 50 | val (optimizeOps, unapply, debug, debugRaw) = c.prefix.tree match { 51 | case q"new ${`macroName`}(..$args)" => 52 | ( 53 | args.collectFirst { case q"optimizeOps = false" => }.isEmpty, 54 | args.collectFirst { case q"unapply = true" => }.isDefined, 55 | args.collectFirst { case q"debug = true" => }.isDefined, 56 | args.collectFirst { case q"debugRaw = true" => }.isDefined 57 | ) 58 | case _ => (true, false, false, false) 59 | } 60 | 61 | def fail(msg: String) = c.abort(c.enclosingPosition, msg) 62 | 63 | def runClass(clsDef: ClassDef, subtype: Boolean) = { 64 | runClassWithObj(clsDef, q"object ${clsDef.name.toTermName}".asInstanceOf[ModuleDef], subtype) 65 | } 66 | 67 | def runClassWithObj(clsDef: ClassDef, modDef: ModuleDef, subtype: Boolean) = { 68 | val valDef = extractConstructorValDef(getConstructor(clsDef.impl.body)) 69 | // Converts [F[_], A] to [F, A]; needed for applying the defined type params. 70 | val tparamNames: List[TypeName] = clsDef.tparams.map(_.name) 71 | // Type params with variance removed for building methods. 72 | val tparamsNoVar: List[TypeDef] = clsDef.tparams.map(td => 73 | TypeDef(Modifiers(Flag.PARAM), td.name, td.tparams, td.rhs) 74 | ) 75 | val tparamsWild = tparamsNoVar.map { 76 | case TypeDef(mods, _, args, tree) => TypeDef(mods, typeNames.WILDCARD, args, tree) 77 | } 78 | // Ensure we're not trying to inherit from anything. 79 | validateParents(clsDef.impl.parents) 80 | // Build the type and object definitions. 81 | generateNewType(clsDef, modDef, valDef, tparamsNoVar, tparamNames, tparamsWild, subtype) 82 | } 83 | 84 | def mkBaseTypeDef(clsDef: ClassDef, reprType: Tree, subtype: Boolean) = { 85 | val refinementName = TypeName(s"__${clsDef.name.decodedName.toString}__newtype") 86 | (clsDef.tparams, subtype) match { 87 | case (_, false) => q"type Base = _root_.scala.Any { type $refinementName } " 88 | case (Nil, true) => q"type Base = $reprType" 89 | case (tparams, true) => q"type Base[..$tparams] = $reprType" 90 | } 91 | } 92 | 93 | def mkTypeTypeDef(clsDef: ClassDef, tparamsNames: List[TypeName], subtype: Boolean) = 94 | (clsDef.tparams, subtype) match { 95 | case (Nil, false) => q"type Type <: Base with Tag" 96 | case (tparams, false) => q"type Type[..$tparams] <: Base with Tag[..$tparamsNames]" 97 | case (Nil, true) => q"type Type <: Base with Tag" 98 | case (tparams, true) => q"type Type[..$tparams] <: Base[..$tparamsNames] with Tag[..$tparamsNames]" 99 | } 100 | 101 | def generateNewType( 102 | clsDef: ClassDef, modDef: ModuleDef, valDef: ValDef, 103 | tparamsNoVar: List[TypeDef], tparamNames: List[TypeName], tparamsWild: List[TypeDef], 104 | subtype: Boolean 105 | ): Tree = { 106 | val ModuleDef(objMods, objName, Template(objParents, objSelf, objDefs)) = modDef 107 | val typeName = clsDef.name 108 | val clsName = clsDef.name.decodedName 109 | val reprType = valDef.tpt 110 | val typesTraitName = TypeName(s"${clsName.decodedName}__Types") 111 | val tparams = clsDef.tparams 112 | val companionExtraDefs = 113 | maybeGenerateApplyMethod(clsDef, valDef, tparamsNoVar, tparamNames) ::: 114 | maybeGenerateUnapplyMethod(clsDef, valDef, tparamsNoVar, tparamNames) ::: 115 | maybeGenerateOpsDef(clsDef, valDef, tparamsNoVar, tparamNames) ::: 116 | generateCoercibleInstances(tparamsNoVar, tparamNames, tparamsWild) ::: 117 | generateDerivingMethods(tparamsNoVar, tparamNames, tparamsWild) 118 | 119 | // Note that we use an abstract type alias 120 | // `type Type <: Base with Tag` and not `type Type = ...` to prevent 121 | // scalac automatically expanding the type alias. 122 | 123 | val baseTypeDef = mkBaseTypeDef(clsDef, reprType, subtype) 124 | val typeTypeDef = mkTypeTypeDef(clsDef, tparamNames, subtype) 125 | 126 | val newtypeObjParents = objParents 127 | val newtypeObjDef = ModuleDef( 128 | objMods, objName, Template(newtypeObjParents, objSelf, objDefs ++ companionExtraDefs ++ Seq( 129 | q"type Repr[..$tparams] = $reprType", 130 | baseTypeDef, 131 | q"trait Tag[..$tparams] extends _root_.scala.Any", 132 | typeTypeDef 133 | )) 134 | ) 135 | 136 | q""" 137 | type $typeName[..$tparams] = $objName.Type[..$tparamNames] 138 | $newtypeObjDef 139 | """ 140 | } 141 | 142 | def maybeGenerateApplyMethod( 143 | clsDef: ClassDef, valDef: ValDef, tparamsNoVar: List[TypeDef], tparamNames: List[TypeName] 144 | ): List[Tree] = { 145 | if (!clsDef.mods.hasFlag(Flag.CASE)) Nil else List( 146 | if (tparamsNoVar.isEmpty) { 147 | q"def apply(${valDef.name}: ${valDef.tpt}): ${clsDef.name} = ${valDef.name}.asInstanceOf[${clsDef.name}]" 148 | } else { 149 | q""" 150 | def apply[..$tparamsNoVar](${valDef.name}: ${valDef.tpt}): ${clsDef.name}[..$tparamNames] = 151 | ${valDef.name}.asInstanceOf[${clsDef.name}[..$tparamNames]] 152 | """ 153 | } 154 | ) 155 | } 156 | 157 | def maybeGenerateUnapplyMethod( 158 | clsDef: ClassDef, valDef: ValDef, tparamsNoVar: List[TypeDef], tparamNames: List[TypeName] 159 | ): List[Tree] = { 160 | if (!unapply) Nil else { 161 | // Note that our unapply method should Some since its isEmpty/get is constant. 162 | List( 163 | if (tparamsNoVar.isEmpty) { 164 | q"""def unapply(x: ${clsDef.name}): Some[${valDef.tpt}] = 165 | Some(x.asInstanceOf[${valDef.tpt}])""" 166 | } else { 167 | q"""def unapply[..$tparamsNoVar](x: ${clsDef.name}[..$tparamNames]): Some[${valDef.tpt}] = 168 | Some(x.asInstanceOf[${valDef.tpt}])""" 169 | } 170 | ) 171 | } 172 | } 173 | 174 | // We should expose the constructor argument as an extension method only if 175 | // it was defined as a public param. 176 | def shouldGenerateValMethod(clsDef: ClassDef, valDef: ValDef): Boolean = { 177 | clsDef.impl.body.collectFirst { 178 | case vd: ValDef 179 | if (vd.mods.hasFlag(Flag.CASEACCESSOR) || vd.mods.hasFlag(Flag.PARAMACCESSOR)) 180 | && !vd.mods.hasFlag(Flag.PRIVATE) 181 | && vd.name == valDef.name => () 182 | }.isDefined 183 | } 184 | 185 | def maybeGenerateValMethod( 186 | clsDef: ClassDef, valDef: ValDef 187 | ): Option[Tree] = { 188 | if (!shouldGenerateValMethod(clsDef, valDef)) { 189 | None 190 | } else if (!isDefinedInObject && optimizeOps) { 191 | c.abort(valDef.pos, List( 192 | "Fields can only be defined for newtypes defined in an object", 193 | s"Consider defining as: private val ${valDef.name.decodedName}" 194 | ).mkString(" ")) 195 | } else { 196 | Some(q"def ${valDef.name}: ${valDef.tpt} = $$this$$.asInstanceOf[${valDef.tpt}]") 197 | } 198 | } 199 | 200 | def maybeGenerateOpsDef( 201 | clsDef: ClassDef, valDef: ValDef, tparamsNoVar: List[TypeDef], tparamNames: List[TypeName] 202 | ): List[Tree] = { 203 | val extensionMethods = 204 | maybeGenerateValMethod(clsDef, valDef).toList ++ getInstanceMethods(clsDef) 205 | 206 | if (extensionMethods.isEmpty) { 207 | Nil 208 | } else { 209 | val parent = if (optimizeOps) typeOf[AnyVal].typeSymbol else typeOf[AnyRef].typeSymbol 210 | // Note that we generate the implicit class for extension methods and the 211 | // implicit def to convert `this` used in the Ops to our newtype value. 212 | if (clsDef.tparams.isEmpty) { 213 | List( 214 | q""" 215 | implicit final class Ops$$newtype(val $$this$$: Type) extends $parent { 216 | ..$extensionMethods 217 | } 218 | """, 219 | q"implicit def opsThis(x: Ops$$newtype): Type = x.$$this$$" 220 | ) 221 | } else { 222 | List( 223 | q""" 224 | implicit final class Ops$$newtype[..${clsDef.tparams}]( 225 | val $$this$$: Type[..$tparamNames] 226 | ) extends $parent { 227 | ..$extensionMethods 228 | } 229 | """, 230 | q""" 231 | implicit def opsThis[..$tparamsNoVar]( 232 | x: Ops$$newtype[..$tparamNames] 233 | ): Type[..$tparamNames] = x.$$this$$ 234 | """ 235 | ) 236 | } 237 | } 238 | } 239 | 240 | def generateDerivingMethods( 241 | tparamsNoVar: List[TypeDef], tparamNames: List[TypeName], tparamsWild: List[TypeDef] 242 | ): List[Tree] = { 243 | if (tparamsNoVar.isEmpty) { 244 | List(q"def deriving[TC[_]](implicit ev: TC[Repr]): TC[Type] = ev.asInstanceOf[TC[Type]]") 245 | } else { 246 | // Creating a fresh type name so it doesn't collide with the tparams passed in. 247 | val TC = TypeName(c.freshName("TC")) 248 | List( 249 | q""" 250 | def deriving[$TC[_], ..$tparamsNoVar]( 251 | implicit ev: $TC[Repr[..$tparamNames]] 252 | ): $TC[Type[..$tparamNames]] = ev.asInstanceOf[$TC[Type[..$tparamNames]]] 253 | """, 254 | q""" 255 | def derivingK[$TC[_[..$tparamsWild]]](implicit ev: $TC[Repr]): $TC[Type] = 256 | ev.asInstanceOf[$TC[Type]] 257 | """ 258 | ) 259 | 260 | } 261 | } 262 | 263 | def generateCoercibleInstances( 264 | tparamsNoVar: List[TypeDef], tparamNames: List[TypeName], tparamsWild: List[TypeDef] 265 | ): List[Tree] = { 266 | if (tparamsNoVar.isEmpty) List( 267 | q"@_root_.scala.inline implicit def unsafeWrap: $CoercibleCls[Repr, Type] = $CoercibleObj.instance", 268 | q"@_root_.scala.inline implicit def unsafeUnwrap: $CoercibleCls[Type, Repr] = $CoercibleObj.instance", 269 | q"@_root_.scala.inline implicit def unsafeWrapM[M[_]]: $CoercibleCls[M[Repr], M[Type]] = $CoercibleObj.instance", 270 | q"@_root_.scala.inline implicit def unsafeUnwrapM[M[_]]: $CoercibleCls[M[Type], M[Repr]] = $CoercibleObj.instance", 271 | // Avoid ClassCastException with Array types by prohibiting Array coercing. 272 | q"@_root_.scala.inline implicit def cannotWrapArrayAmbiguous1: $CoercibleCls[_root_.scala.Array[Repr], _root_.scala.Array[Type]] = $CoercibleObj.instance", 273 | q"@_root_.scala.inline implicit def cannotWrapArrayAmbiguous2: $CoercibleCls[_root_.scala.Array[Repr], _root_.scala.Array[Type]] = $CoercibleObj.instance", 274 | q"@_root_.scala.inline implicit def cannotUnwrapArrayAmbiguous1: $CoercibleCls[_root_.scala.Array[Type], _root_.scala.Array[Repr]] = $CoercibleObj.instance", 275 | q"@_root_.scala.inline implicit def cannotUnwrapArrayAmbiguous2: $CoercibleCls[_root_.scala.Array[Type], _root_.scala.Array[Repr]] = $CoercibleObj.instance" 276 | ) else List( 277 | q"@_root_.scala.inline implicit def unsafeWrap[..$tparamsNoVar]: $CoercibleCls[Repr[..$tparamNames], Type[..$tparamNames]] = $CoercibleObj.instance", 278 | q"@_root_.scala.inline implicit def unsafeUnwrap[..$tparamsNoVar]: $CoercibleCls[Type[..$tparamNames], Repr[..$tparamNames]] = $CoercibleObj.instance", 279 | q"@_root_.scala.inline implicit def unsafeWrapM[M[_], ..$tparamsNoVar]: $CoercibleCls[M[Repr[..$tparamNames]], M[Type[..$tparamNames]]] = $CoercibleObj.instance", 280 | q"@_root_.scala.inline implicit def unsafeUnwrapM[M[_], ..$tparamsNoVar]: $CoercibleCls[M[Type[..$tparamNames]], M[Repr[..$tparamNames]]] = $CoercibleObj.instance", 281 | q"@_root_.scala.inline implicit def unsafeWrapK[T[_[..$tparamsNoVar]]]: $CoercibleCls[T[Repr], T[Type]] = $CoercibleObj.instance", 282 | q"@_root_.scala.inline implicit def unsafeUnwrapK[T[_[..$tparamsNoVar]]]: $CoercibleCls[T[Type], T[Repr]] = $CoercibleObj.instance", 283 | // Avoid ClassCastException with Array types by prohibiting Array coercing. 284 | q"@_root_.scala.inline implicit def cannotWrapArrayAmbiguous1[..$tparamsNoVar]: $CoercibleCls[_root_.scala.Array[Repr[..$tparamNames]], _root_.scala.Array[Type[..$tparamNames]]] = $CoercibleObj.instance", 285 | q"@_root_.scala.inline implicit def cannotWrapArrayAmbiguous2[..$tparamsNoVar]: $CoercibleCls[_root_.scala.Array[Repr[..$tparamNames]], _root_.scala.Array[Type[..$tparamNames]]] = $CoercibleObj.instance", 286 | q"@_root_.scala.inline implicit def cannotUnwrapArrayAmbiguous1[..$tparamsNoVar]: $CoercibleCls[_root_.scala.Array[Type[..$tparamNames]], _root_.scala.Array[Repr[..$tparamNames]]] = $CoercibleObj.instance", 287 | q"@_root_.scala.inline implicit def cannotUnwrapArrayAmbiguous2[..$tparamsNoVar]: $CoercibleCls[_root_.scala.Array[Type[..$tparamNames]], _root_.scala.Array[Repr[..$tparamNames]]] = $CoercibleObj.instance" 288 | ) 289 | } 290 | 291 | def getConstructor(body: List[Tree]): DefDef = body.collectFirst { 292 | case dd: DefDef if dd.name == termNames.CONSTRUCTOR => dd 293 | }.getOrElse(fail("Failed to locate constructor")) 294 | 295 | def extractConstructorValDef(ctor: DefDef): ValDef = ctor.vparamss match { 296 | case List(List(vd)) => vd 297 | case _ => fail("Unsupported constructor, must have exactly one argument") 298 | } 299 | 300 | def getInstanceMethods(clsDef: ClassDef): List[DefDef] = { 301 | val res = clsDef.impl.body.flatMap { 302 | case vd: ValDef => 303 | if (vd.mods.hasFlag(Flag.CASEACCESSOR) || vd.mods.hasFlag(Flag.PARAMACCESSOR)) Nil 304 | else c.abort(vd.pos, "val definitions not supported, use def instead") 305 | case dd: DefDef => 306 | if (dd.name == termNames.CONSTRUCTOR) Nil else List(dd) 307 | case x => 308 | c.abort(x.pos, s"illegal definition in newtype: $x") 309 | } 310 | if (res.nonEmpty && !isDefinedInObject && optimizeOps) { 311 | c.abort(res.head.pos, "Methods can only be defined for newtypes defined in an object") 312 | } 313 | res 314 | } 315 | 316 | def validateParents(parents: List[Tree]): Unit = { 317 | val ignoredExtends = List(tq"scala.Product", tq"scala.Serializable", tq"scala.AnyRef") 318 | val unsupported = parents.filterNot(t => ignoredExtends.exists(t.equalsStructure)) 319 | if (unsupported.nonEmpty) { 320 | fail(s"newtypes do not support inheritance; illegal supertypes: ${unsupported.mkString(", ")}") 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /shared/src/test/scala/io/estatico/newtype/macros/NewTypeMacrosTest.scala: -------------------------------------------------------------------------------- 1 | package io.estatico.newtype.macros 2 | 3 | import io.estatico.newtype.ops._ 4 | import org.scalacheck.Arbitrary 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | class NewTypeMacrosTest extends AnyFlatSpec with Matchers { 9 | 10 | import NewTypeMacrosTest._ 11 | 12 | behavior of "@newtype case class" 13 | 14 | it should "generate a type alias, companion object, and constructor" in { 15 | 16 | // Ensure that we can access the type and the constructor. 17 | val res = Foo(1) 18 | assertCompiles("res: Foo") 19 | 20 | // Should have the same runtime representation as Int. 21 | res shouldBe 1 22 | res shouldBe Foo(1) 23 | } 24 | 25 | it should "generate an accessor extension method" in { 26 | Foo(1).value shouldBe 1 27 | } 28 | 29 | it should "not generate an accessor method if private" in { 30 | // This is also useful so we can define local newtypes. 31 | @newtype case class Foo0[A](private val value: A) 32 | Foo0('a') shouldBe 'a' 33 | assertDoesNotCompile("Foo0('a').value") 34 | } 35 | 36 | it should "convert instance methods into extension methods" in { 37 | val res: Bar = Bar(2).twice 38 | res shouldBe 4 39 | } 40 | 41 | it should "work in arrays" in { 42 | val foo = Foo(313) 43 | // See https://github.com/estatico/scala-newtype/issues/25 44 | // Array(foo).apply(0) shouldBe foo 45 | Array[Int](313).asInstanceOf[Array[Foo]].apply(0) shouldBe foo 46 | } 47 | 48 | behavior of "@newtype class" 49 | 50 | it should "not expose a default constructor" in { 51 | assertTypeError("""Baz("foo")""") 52 | Baz.create("foo") shouldBe "FOO" 53 | } 54 | 55 | it should "not expose its constructor argument by default" in { 56 | assertDoesNotCompile("""Baz.create("foo").value""") 57 | } 58 | 59 | it should "expose its constructor argument if defined as a val" in { 60 | Baz2.create("foo").value shouldBe "FOO" 61 | } 62 | 63 | behavior of "@newtype with type arguments" 64 | 65 | it should "generate a proper constructor" in { 66 | val repr = List(Option(1)) 67 | val ot = OptionT(repr) 68 | assertCompiles("ot: OptionT[List, Int]") 69 | ot shouldBe repr 70 | } 71 | 72 | it should "be Coercible" in { 73 | val repr = List(Option(1)) 74 | val ot = OptionT(repr) 75 | 76 | val x = ot.coerce[List[Option[Int]]] 77 | x shouldBe repr 78 | 79 | val y = Vector(repr).coerce[Vector[OptionT[List, Int]]] 80 | y shouldBe Vector(repr) 81 | } 82 | 83 | it should "not coerce array types" in { 84 | val repr = List(Option(1)) 85 | val ot = OptionT(repr) 86 | assertTypeError("Array(ot).coerce[Array[List[Option[Int]]]]") 87 | } 88 | 89 | it should "support covariance" in { 90 | val x = Cov(List(Some(1))) 91 | assertCompiles("x: Cov[Some[Int]]") 92 | 93 | val y = Cov(List(None)) 94 | assertCompiles("y: Cov[None.type]") 95 | 96 | def someOrZero[A](c: Cov[Option[Int]]): Cov[Int] = Cov(c.value.map(_.getOrElse(0))) 97 | 98 | someOrZero(x) shouldBe List(1) 99 | someOrZero(y) shouldBe List(0) 100 | } 101 | 102 | it should "work in arrays" in { 103 | import scala.collection.immutable.Set 104 | val repr = Set(Option("newtypes")) 105 | val ot = OptionT(repr) 106 | // See https://github.com/estatico/scala-newtype/issues/25 107 | // Array(ot).apply(0) shouldBe ot 108 | Array(repr).asInstanceOf[Array[OptionT[Set, String]]].apply(0) shouldBe ot 109 | } 110 | 111 | behavior of "@newtype with type bounds" 112 | 113 | it should "enforce type bounds" in { 114 | val x = Sub(new java.util.HashMap[String, Int]): Sub[java.util.HashMap[String, Int]] 115 | val y = Sub(new java.util.concurrent.ConcurrentHashMap[String, Int]) 116 | 117 | assertCompiles("x: Sub[java.util.HashMap[String, Int]]") 118 | assertCompiles("y: Sub[java.util.concurrent.ConcurrentHashMap[String, Int]]") 119 | 120 | assertDoesNotCompile("x: Sub[java.util.concurrent.ConcurrentHashMap[String, Int]]") 121 | assertDoesNotCompile("y: Sub[java.util.HashMap[String, Int]]") 122 | } 123 | 124 | behavior of "deriving" 125 | 126 | it should "support deriving type class instances for simple newtypes" in { 127 | @newtype case class Text(private val s: String) 128 | object Text { 129 | implicit val arb: Arbitrary[Text] = deriving 130 | } 131 | val x = scala.Predef.implicitly[Arbitrary[Text]].arbitrary.sample.get 132 | assertCompiles("x: Text") 133 | val y = x.coerce[String] 134 | assertCompiles("y: String") 135 | } 136 | 137 | it should "support deriving type class instances for simple newtypes via coerce" in { 138 | @newtype case class Text(private val s: String) 139 | object Text { 140 | implicit val arb: Arbitrary[Text] = scala.Predef.implicitly[Arbitrary[String]].coerce 141 | } 142 | val x = scala.Predef.implicitly[Arbitrary[Text]].arbitrary.sample.get 143 | assertCompiles("x: Text") 144 | val y = x.coerce[String] 145 | assertCompiles("y: String") 146 | } 147 | 148 | it should "support deriving type class instances for higher-kinded newtypes" in { 149 | @newtype class Nel[A](private val list: List[A]) 150 | object Nel { 151 | def apply[A](head: A, tail: List[A]): Nel[A] = (head +: tail).coerce[Nel[A]] 152 | implicit val functor: Functor[Nel] = derivingK 153 | } 154 | 155 | val x = scala.Predef.implicitly[Functor[Nel]].map(Nel(1, List(2, 3)))(_ * 2) 156 | assertCompiles("x: Nel[Int]") 157 | x shouldBe List(2, 4, 6) 158 | } 159 | 160 | it should "support deriving type class instances for higher-kinded newtypes via coerce" in { 161 | @newtype class Nel[A](private val list: List[A]) 162 | object Nel { 163 | def apply[A](head: A, tail: List[A]): Nel[A] = (head +: tail).coerce[Nel[A]] 164 | implicit val functor: Functor[Nel] = scala.Predef.implicitly[Functor[List]].coerce 165 | } 166 | 167 | val x = scala.Predef.implicitly[Functor[Nel]].map(Nel(1, List(2, 3)))(_ * 2) 168 | assertCompiles("x: Nel[Int]") 169 | x shouldBe List(2, 4, 6) 170 | } 171 | 172 | it should "support auto-deriving type class instances for simple newtypes" in { 173 | @newtype case class Text(private val s: String) 174 | object Text { 175 | implicit def typeclass[T[_]](implicit ev: T[String]): T[Text] = deriving 176 | } 177 | val x = scala.Predef.implicitly[Arbitrary[Text]].arbitrary.sample.get 178 | assertCompiles("x: Text") 179 | val y = x.coerce[String] 180 | assertCompiles("y: String") 181 | } 182 | 183 | it should "support auto-deriving type class instances for simple newtypes via coerce" in { 184 | @newtype case class Text(private val s: String) 185 | object Text { 186 | implicit def typeclass[T[_]](implicit ev: T[String]): T[Text] = ev.coerce 187 | } 188 | val x = scala.Predef.implicitly[Arbitrary[Text]].arbitrary.sample.get 189 | assertCompiles("x: Text") 190 | val y = x.coerce[String] 191 | assertCompiles("y: String") 192 | } 193 | 194 | it should "support deriving type class instances for newtypes with type params" in { 195 | @newtype case class EitherT[F[_], L, R](private val x: F[Either[L, R]]) 196 | object EitherT { 197 | // Derive the Arbitrary instance explicitly 198 | implicit def arb[F[_], L, R]( 199 | implicit a: Arbitrary[F[Either[L, R]]] 200 | ): Arbitrary[EitherT[F, L, R]] = deriving 201 | } 202 | val x = { 203 | import scala.Predef._ 204 | scala.Predef.implicitly[Arbitrary[EitherT[List, String, Int]]].arbitrary.sample.get 205 | } 206 | assertCompiles("x: EitherT[List, String, Int]") 207 | val y = x.coerce[List[Either[String, Int]]] 208 | assertCompiles("y: List[Either[String, Int]]") 209 | } 210 | 211 | it should "support deriving type class instances for newtypes with type params via coerce" in { 212 | @newtype case class EitherT[F[_], L, R](private val x: F[Either[L, R]]) 213 | object EitherT { 214 | // Derive the Arbitrary instance explicitly 215 | implicit def arb[F[_], L, R]( 216 | implicit a: Arbitrary[F[Either[L, R]]] 217 | ): Arbitrary[EitherT[F, L, R]] = a.coerce 218 | } 219 | val x = { 220 | import scala.Predef._ 221 | scala.Predef.implicitly[Arbitrary[EitherT[List, String, Int]]].arbitrary.sample.get 222 | } 223 | assertCompiles("x: EitherT[List, String, Int]") 224 | val y = x.coerce[List[Either[String, Int]]] 225 | assertCompiles("y: List[Either[String, Int]]") 226 | } 227 | 228 | it should "support auto-deriving type class instances for newtypes with type params" in { 229 | @newtype case class EitherT[F[_], L, R](private val x: F[Either[L, R]]) 230 | object EitherT { 231 | // Auto-derive all type classes of kind * -> * 232 | implicit def typeclass[T[_], F[_], L, R]( 233 | implicit t: T[F[Either[L, R]]] 234 | ): T[EitherT[F, L, R]] = deriving 235 | } 236 | val x = { 237 | import scala.Predef._ 238 | scala.Predef.implicitly[Arbitrary[EitherT[List, String, Int]]].arbitrary.sample.get 239 | } 240 | assertCompiles("x: EitherT[List, String, Int]") 241 | val y = x.coerce[List[Either[String, Int]]] 242 | assertCompiles("y: List[Either[String, Int]]") 243 | } 244 | 245 | it should "support auto-deriving type class instances for newtypes with type params via coerce" in { 246 | @newtype case class EitherT[F[_], L, R](private val x: F[Either[L, R]]) 247 | object EitherT { 248 | // Auto-derive all type classes of kind * -> * 249 | implicit def typeclass[T[_], F[_], L, R]( 250 | implicit t: T[F[Either[L, R]]] 251 | ): T[EitherT[F, L, R]] = t.coerce 252 | } 253 | val x = { 254 | import scala.Predef._ 255 | scala.Predef.implicitly[Arbitrary[EitherT[List, String, Int]]].arbitrary.sample.get 256 | } 257 | assertCompiles("x: EitherT[List, String, Int]") 258 | val y = x.coerce[List[Either[String, Int]]] 259 | assertCompiles("y: List[Either[String, Int]]") 260 | } 261 | 262 | behavior of "this" 263 | 264 | it should "work in extension methods" in { 265 | val x0 = Maybe(null: String) 266 | val x1 = x0.filter(_.contains("a")) 267 | x1.isEmpty shouldBe true 268 | x1 shouldBe Maybe.empty 269 | x1 shouldBe (null: Any) 270 | 271 | val y0 = Maybe("apple") 272 | val y1 = y0.filter(_.contains("a")) 273 | y1.isDefined shouldBe true 274 | y1 shouldBe "apple" 275 | 276 | val z0 = Maybe("apple") 277 | val z1 = z0.filter(_.contains("z")) 278 | z1.isEmpty shouldBe true 279 | z1 shouldBe Maybe.empty 280 | z1 shouldBe (null: Any) 281 | 282 | val n0 = Maybe(0) 283 | val n1 = n0.filter(_ > 0) 284 | n1.isEmpty shouldBe true 285 | n1 shouldBe Maybe.empty 286 | n1 shouldBe (null: Any) 287 | } 288 | 289 | behavior of "nested @newtypes" 290 | 291 | it should "work" in { 292 | val x = Nested(Foo(1)) 293 | assertCompiles("x: Nested") 294 | val y = x.coerce[Foo] 295 | assertCompiles("y: Foo") 296 | val z = y.coerce[Int] 297 | assertCompiles("z: Int") 298 | } 299 | 300 | behavior of "Id[A]" 301 | 302 | it should "work" in { 303 | val x = Id(1) 304 | x shouldBe 1 305 | assertCompiles("x: Id[Int]") 306 | val y = Id(1).map(_ + 2) 307 | y shouldBe 3 308 | assertCompiles("y: Id[Int]") 309 | val z = Id(2).flatMap(x => Id(x * 2)) 310 | z shouldBe 4 311 | assertCompiles("z: Id[Int]") 312 | } 313 | 314 | behavior of "optimizeOps = false" 315 | 316 | it should "work with @newtype" in { 317 | @newtype(optimizeOps = false) case class Foo(value: Int) 318 | Foo(1: Int).value shouldBe 1 319 | @newtype(optimizeOps = false) case class Bar[A](value: A) 320 | Bar("foo").value shouldBe "foo" 321 | } 322 | 323 | it should "work with @newsubtype" in { 324 | @newsubtype(optimizeOps = false) case class Foo(value: Int) 325 | Foo(1: Int).value shouldBe 1 326 | @newsubtype(optimizeOps = false) case class Bar[A](value: A) 327 | Bar("foo").value shouldBe "foo" 328 | } 329 | 330 | behavior of "Coercible" 331 | 332 | it should "work for nested type constructors" in { 333 | val x = List(Option(1)) 334 | val y = x.coerce[List[Option[Foo]]] 335 | } 336 | 337 | "unapply = true" should "generate an unapply method" in { 338 | @newtype (unapply = true) case class X0(private val x: String) 339 | @newtype (unapply = true) case class X1[A](private val x: A) 340 | @newsubtype(unapply = true) case class Y0(private val x: String) 341 | @newsubtype(unapply = true) case class Y1[A](private val x: A) 342 | 343 | // Note that we're using (x0: String) to assert the type of x0 at compile time. 344 | // Also checking that unapply doesn't compile for ill-typed expressions. 345 | 346 | val x0 = X0("x") match { case X0(x) => x } 347 | (x0: String) shouldBe "x" 348 | assertTypeError(""" "x" match { case X0(x) => x }""") 349 | assertTypeError(""" 1 match { case X0(x) => x }""") 350 | 351 | val x1 = X1("x") match { case X1(x) => x } 352 | (x1: String) shouldBe "x" 353 | assertTypeError(""" "x" match { case X1(x) => x }""") 354 | assertTypeError(""" 1 match { case X1(x) => x }""") 355 | 356 | val y0 = Y0("y") match { case Y0(x) => x } 357 | (y0: String) shouldBe "y" 358 | assertTypeError(""" "x" match { case Y0(x) => x }""") 359 | assertTypeError(""" 1 match { case Y0(x) => x }""") 360 | 361 | val y1 = Y1("y") match { case Y1(x) => x } 362 | (y1: String) shouldBe "y" 363 | assertTypeError(""" "x" match { case Y1(x) => x }""") 364 | assertTypeError(""" 1 match { case Y1(x) => x }""") 365 | } 366 | 367 | // Unfortunately, we don't have a way to assert on compiler warnings, which is 368 | // what happens with the code below. If we run with -Xfatal-warnings, the test 369 | // won't compile at all, so leaving here to do manual checking until scalatest 370 | // can provide support for this. 371 | // See https://github.com/scalatest/scalatest/issues/1352 372 | //"type-based pattern matching" should "emit compiler warnings" in { 373 | // assertDoesNotCompile("Foo(1) match { case x: Foo => x }") 374 | // assertDoesNotCompile("1 match { case x: Foo => x }") 375 | // assertDoesNotCompile(""" "foo" match { case x: Foo => x }""") 376 | // assertDoesNotCompile("(1: Any) match { case x: Foo => x }") 377 | //} 378 | } 379 | 380 | object NewTypeMacrosTest { 381 | 382 | @newtype case class Foo(value: Int) 383 | 384 | @newtype case class Nested(value: Foo) 385 | 386 | @newtype case class Bar(value: Int) { 387 | def twice: Bar = Bar(value * 2) 388 | } 389 | 390 | @newtype class Baz(value: String) 391 | object Baz { 392 | def create(value: String): Baz = value.toUpperCase.coerce[Baz] 393 | } 394 | 395 | @newtype class Baz2(val value: String) 396 | object Baz2 { 397 | def create(value: String): Baz2 = value.toUpperCase.coerce[Baz2] 398 | } 399 | 400 | @newtype case class OptionT[F[_], A](value: F[Option[A]]) 401 | 402 | @newtype case class Sub[A <: java.util.Map[String, Int]](value: A) 403 | 404 | @newtype case class Cov[+A](value: List[A]) 405 | 406 | @newtype case class Maybe[A](unsafeGet: A) { 407 | def isEmpty: Boolean = unsafeGet == null 408 | def isDefined: Boolean = unsafeGet != null 409 | def map[B](f: A => B): Maybe[B] = if (isEmpty) Maybe.empty else Maybe(f(unsafeGet)) 410 | def flatMap[B](f: A => Maybe[B]): Maybe[B] = if (isEmpty) Maybe.empty else f(unsafeGet) 411 | def filter(p: A => Boolean): Maybe[A] = if (isEmpty || !p(unsafeGet)) Maybe.empty else this 412 | def filterNot(p: A => Boolean): Maybe[A] = if (isEmpty || p(unsafeGet)) Maybe.empty else this 413 | def orElse(ma: => Maybe[A]): Maybe[A] = if (isDefined) this else ma 414 | def getOrElse(a: => A): A = if (isDefined) unsafeGet else a 415 | def getOrThrow: A = if (isDefined) unsafeGet else throw new NoSuchElementException("Maybe.empty.get") 416 | def cata[B](ifEmpty: => B, ifDefined: A => B): B = if (isEmpty) ifEmpty else ifDefined(unsafeGet) 417 | def fold[B](ifEmpty: => B)(ifDefined: A => B): B = cata(ifEmpty, ifDefined) 418 | def contains(a: A): Boolean = isDefined && unsafeGet == a 419 | def exists(p: A => Boolean): Boolean = isDefined && p(unsafeGet) 420 | } 421 | object Maybe { 422 | def empty[A]: Maybe[A] = null.asInstanceOf[Maybe[A]] 423 | def fromOption[A](x: Option[A]): Maybe[A] = (if (x.isDefined) x.get else null).asInstanceOf[Maybe[A]] 424 | } 425 | 426 | @newtype case class Id[A](value: A) { 427 | def map[B](f: A => B): Id[B] = Id(f(value)) 428 | def flatMap[B](f: A => Id[B]): Id[B] = f(value) 429 | } 430 | 431 | trait Functor[F[_]] { 432 | def map[A, B](fa: F[A])(f: A => B): F[B] 433 | } 434 | 435 | object Functor { 436 | implicit val list: Functor[List] = new Functor[List] { 437 | override def map[A, B](fa: List[A])(f: A => B): List[B] = fa.map(f) 438 | } 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NewType 2 | 3 | NewTypes for Scala with no runtime overhead. 4 | 5 | [![Build Status](https://travis-ci.org/estatico/scala-newtype.svg?branch=master)](https://travis-ci.org/estatico/scala-newtype) 6 | [![Gitter](https://img.shields.io/badge/gitter-join%20chat-green.svg)](https://gitter.im/estatico/scala-newtype) 7 | [![Maven Central](https://img.shields.io/maven-central/v/io.estatico/newtype_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/io.estatico/newtype_2.13) 8 | 9 | ## Getting NewType 10 | 11 | If you are using SBT, add the following line to your build file - 12 | 13 | ```scala 14 | libraryDependencies += "io.estatico" %% "newtype" % "0.4.4" 15 | ``` 16 | 17 | Make sure you have [macro-paradise](https://docs.scala-lang.org/overviews/macros/paradise.html) enabled 18 | - for Scala 2.13.0-M3 and lower add the following line to your build file 19 | ```scala 20 | addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full) 21 | ``` 22 | - for Scala 2.13.0-M4 and above via compiler flag [`-Ymacro-annotations`](https://github.com/scala/scala/pull/6606) 23 | 24 | For Maven or other build tools, see the Maven Central badge at the top of this README. 25 | 26 | ## Usage 27 | 28 | For generating newtypes via the `@newtype` macro, see [@newtype macro](#newtype-macro) 29 | For non-macro usage, see the section on [Legacy encoding](#legacy-encoding). 30 | 31 | ### @newtype macro 32 | 33 | As of newtype 0.2, you can now encode newtypes using the `@newtype` macro. Its implementation 34 | and usage aligns closely with idiomatic Scala syntax, so IDE support _just works_ out of the box. 35 | 36 | ```scala 37 | import io.estatico.newtype.macros.newtype 38 | 39 | package object types { 40 | 41 | @newtype case class WidgetId(toInt: Int) 42 | } 43 | ``` 44 | 45 | This expands into a `type` and companion `object` definition, so newtypes _must_ be defined 46 | in an `object` or `package object`. 47 | 48 | The example above will generate code similar to the following - 49 | 50 | ```scala 51 | package object types { 52 | type WidgetId = WidgetId.Type 53 | object WidgetId { 54 | type Repr = Int 55 | type Base = Any { type WidgetId$newtype } 56 | trait Tag extends Any 57 | type Type <: Base with Tag 58 | 59 | def apply(x: Int): WidgetId = x.asInstanceOf[WidgetId] 60 | 61 | implicit final class Ops$newtype(val $this$: Type) extends AnyVal { 62 | def toInt: Int = $this$.asInstanceOf[Int] 63 | } 64 | } 65 | } 66 | ``` 67 | 68 | You can also create newtypes which have type parameters - 69 | 70 | ```scala 71 | @newtype case class EitherT[F[_], L, R](x: F[Either[L, R]]) 72 | ``` 73 | 74 | Note that it is impossible to have your newtype _extend_ any types, which 75 | makes sense since it has its own distinct type at compile time and at runtime is just 76 | the underlying value. 77 | 78 | Also, since the `@newtype` annotation gives your type a distinct type at compile-time, 79 | primitives will naturally box as they do when they are applied in any generic context. 80 | See the following section on `@newsubtype` for unboxed primitive newtypes. 81 | 82 | #### @newsubtype macro 83 | 84 | As of newtype 0.4 you now have access to the `@newsubtype` macro. Its usage is identical 85 | to `@newtype`. The difference is that it functions as a _subtype_ of the underlying 86 | type as opposed to having a completely different type at compile time. This may or may 87 | not be desirable, and it's recommended to use `@newtype` if you're not entirely sure you 88 | actually need `@newsubtype`. 89 | 90 | The difference in the generated code is that `@newsubtype` defines its `Base` type defined as - 91 | 92 | ```scala 93 | type Base = Repr 94 | ``` 95 | 96 | The main benefit of `@newsubtype` is that primitives are unboxed. For example, the 97 | following `@newtype` definition will box the `Int`, making it a `java.lang.Integer` 98 | at runtime - 99 | 100 | ```scala 101 | @newtype case class Foo(x: Int) 102 | ``` 103 | 104 | However, the following `@newsubtype` definition will be a primitive `int` at runtime - 105 | 106 | ```scala 107 | @newsubtype case class Bar(x: Int) 108 | ``` 109 | 110 | Note however that calling `getClass` on a newsubtype will fool you - 111 | 112 | ```scala 113 | scala> Bar(1).getClass 114 | res2: Class[_ <: Bar] = class java.lang.Integer 115 | ``` 116 | 117 | Reason is that scalac boxes unnecessarily when calling `getClass`, see https://github.com/scala/bug/issues/10770 118 | 119 | We can confirm that we do in fact have a primitive `int` at runtime back by inspecting the byte code - 120 | 121 | ```scala 122 | scala> class Test { def test = Bar(1) } 123 | scala> :javap Test 124 | ``` 125 | ```java 126 | ... 127 | public int test(); 128 | ... 129 | ``` 130 | 131 | Another "feature" of `@newsubtype` is that its values can be passed to functions 132 | which accept its `Repr` type without needing to convert them first - 133 | 134 | ```scala 135 | scala> def half(b: Int): Int = b / 2 136 | scala> half(Bar(12)) 137 | res6: Int = 6 138 | ``` 139 | 140 | Note that this feature can be undesirable since the newsubtype will be automatically 141 | unwrapped, even when you might not mean to. Again, unless you have a good reason to use 142 | `@newsubtype`, it's recommend to use `@newtype` by default. 143 | 144 | #### Smart Constructors and Accessor Methods 145 | 146 | This library gives you a few choices when it comes to defining smart constructors 147 | and accessor methods for your newtypes. Efforts have been made to keep things idiomatic. 148 | Note that extractors (`unapply` methods) are **not** generated by newtypes. 149 | 150 | Using `case class` gives us a smart constructor (an `apply` method on the companion object) 151 | that will accept a value of type `A` and return the newtype `N`. 152 | 153 | ```scala 154 | @newtype case class N(a: A) 155 | ``` 156 | 157 | You also get an accessor extension method to get the underlying `A`. Note that you can 158 | prevent this by defining the field as private. 159 | 160 | ```scala 161 | @newtype case class N(private val a: A) 162 | ``` 163 | 164 | Using `class` will not generate a smart constructor (no `apply` method). This allows 165 | you to specify your own. Note that `new` never works for newtypes and will fail to compile. 166 | 167 | If you wish to generate an accessor method for your underlying value, you can define it as `val` 168 | just as if you were dealing with a normal class. 169 | 170 | ```scala 171 | @newtype class N(val a: A) 172 | ``` 173 | 174 | If you need to define your own smart constructor, use the 175 | `.coerce` extension method to cast to your newtype. 176 | 177 | ```scala 178 | import io.estatico.newtype.ops._ 179 | 180 | @newtype class Id(val strValue: String) 181 | 182 | object Id { 183 | def fromString(str: String): Either[String, Id] = { 184 | if (str.isEmpty) Left("Id cannot be empty") 185 | else Right(str.coerce) 186 | } 187 | } 188 | ``` 189 | 190 | #### Extension Methods 191 | 192 | Defining extension methods are as simple as defining normal methods in any class - 193 | 194 | ```scala 195 | @newtype case class OptionT[F[_], A](value: F[Option[A]]) { 196 | 197 | def fold[B](default: => B)(f: A => B)(implicit F: Functor[F]): F[B] = 198 | F.map(value)(_.fold(default)(f)) 199 | 200 | def cata[B](default: => B, f: A => B)(implicit F: Functor[F]): F[B] = 201 | fold(default)(f) 202 | 203 | def map[B](f: A => B)(implicit F: Functor[F]): OptionT[F, B] = 204 | OptionT(F.map(value)(_.map(f))) 205 | } 206 | ``` 207 | 208 | #### Companion Objects 209 | 210 | The companion object works just as you'd expect. You can place your type class instances 211 | there and implicit resolution just works. 212 | 213 | Companion objects also contain special `deriving` and `derivingK` 214 | methods to auto-derive instances for you if one exists for your underlying type. 215 | This is similar to GHC Haskell's `GeneralizedNewtypeDeriving` extension. 216 | 217 | `deriving` is used for type classes whose type parameter is _not_ higher kinded. 218 | 219 | ```scala 220 | @newtype case class Text(s: String) 221 | object Text { 222 | implicit val arb: Arbitrary[Text] = deriving 223 | } 224 | ``` 225 | 226 | `derivingK` is used for type classes whose type parameter _is_ higher kinded. 227 | 228 | ```scala 229 | @newtype class Nel[A](val toList: List[A]) 230 | object Nel { 231 | def apply[A](head: A, tail: List[A]): Nel[A] = (head +: tail).coerce[Nel[A]] 232 | implicit val functor: Functor[Nel] = derivingK 233 | } 234 | ``` 235 | 236 | Note that since these methods are created by the `@newtype` macro, IDEs will generally 237 | not be able to resolve them. If the red highlighting bothers you, you can use 238 | `.coerce` to safely cast the base type class to support your newtype - 239 | 240 | ```scala 241 | import io.estatico.newtype.ops._ 242 | 243 | @newtype case class Text(s: String) 244 | object Text { 245 | implicit val arb: Arbitrary[Text] = implicitly[Arbitrary[String]].coerce 246 | } 247 | 248 | @newtype class Nel[A](val toList: List[A]) 249 | object Nel { 250 | def apply[A](head: A, tail: List[A]): Nel[A] = (head +: tail).coerce[Nel[A]] 251 | implicit val functor: Functor[Nel] = implicitly[Functor[List]].coerce 252 | } 253 | ``` 254 | 255 | ### Coercible Instance Trick 256 | 257 | **Note that this is NOT recommended!** 258 | 259 | In some cases, you may want to automatically derive a type class instance 260 | for all newtypes by leveraging `Coercible`. While seemingly convenient, this is 261 | **NOT** recommended as it in some ways goes against the spirit of using 262 | a newtype in the first place. Specializing a specific instance for a 263 | newtype will be tricky and will require clever implicit scoping. Also, 264 | it can [greatly increase your compile times](https://github.com/estatico/scala-newtype/issues/64). 265 | Instead, it's generally better to explicitly define instances for your 266 | newtypes. 267 | 268 | **You have been warned!** 269 | 270 | The following example generates an `Eq` instance for all newtypes in which 271 | their underlying `Repr` type has an `Eq` instance. 272 | 273 | ```scala 274 | scala> :paste 275 | 276 | import cats._, cats.implicits._ 277 | 278 | /** If we have an Eq instance for Repr type R, derive an Eq instance for newtype N. */ 279 | implicit def coercibleEq[R, N](implicit ev: Coercible[Eq[R], Eq[N]], R: Eq[R]): Eq[N] = 280 | ev(R) 281 | 282 | @newtype case class Foo(x: Int) 283 | 284 | // Exiting paste mode, now interpreting. 285 | 286 | scala> Foo(1) === Foo(2) 287 | res0: Boolean = false 288 | ``` 289 | 290 | However, as mentioned, it's generally better to explicitly define your 291 | instances. 292 | 293 | ```scala 294 | @newtype case class Foo(x: Int) 295 | object Foo { 296 | implicit val eq: Eq[Foo] = deriving 297 | } 298 | ``` 299 | 300 | You may not always be able to put your instance in the companion object, likely 301 | because the type class is not available where you are defining your newtype. 302 | In this case, simply define an orphan instance and import it where you need. 303 | 304 | ```scala 305 | object EqOrphans { 306 | implicit val eqFoo: Eq[Foo] = implicitly[Eq[Int]].coerce 307 | } 308 | ``` 309 | 310 | ### Legacy encoding 311 | 312 | If you don't wish to use the macro API, you can still use the legacy API for building 313 | newtypes manually via companion objects. Note that this method does not support newtypes 314 | with type parameters. If you need type parameters, use the macro API. 315 | 316 | The easiest way to get going with the legacy encoding is to create an object that extends from 317 | `NewType.Default` - 318 | 319 | ```scala 320 | import io.estatico.newtype.NewType 321 | 322 | object WidgetId extends NewType.Default[Int] 323 | ``` 324 | 325 | This will be the companion object for your newtype. Use the `.Type` type member to get access 326 | to the type for signatures. A common pattern is to include this in a package object 327 | so it can be easily imported. 328 | 329 | ```scala 330 | package object types { 331 | type WidgetId = WidgetId.Type 332 | object WidgetId extends NewType.Default[Int] 333 | } 334 | ``` 335 | 336 | Now you can import `types.WidgetId` and use it in type signatures as well as the companion 337 | object. 338 | 339 | #### `NewType.Of` vs. `NewType.Default` 340 | 341 | Extending `NewType.Of` simply creates the newtype wrapper; however, you will often 342 | want to extend `NewType.Default` to provide some helper methods on the companion 343 | object - 344 | 345 | ```scala 346 | // Safely casts an Int to a WidgetId 347 | scala> WidgetId(1) 348 | res0: WidgetId.Type = 1 349 | 350 | // Safely casts M[Int] to M[WidgetId] 351 | scala> WidgetId.applyM(List(1, 2)) 352 | res1: List[WidgetId.Type] = List(1, 2) 353 | ``` 354 | 355 | See `NewTypeExtras` for the available mixins for creating newtype wrappers. 356 | 357 | If you wish to do something different, you can supply your own smart-constructor 358 | instead - 359 | 360 | ```scala 361 | object Nat extends NewType.Of[Int] { 362 | def apply(n: Int): Option[Type] = if (n < 0) None else Some(wrap(n)) 363 | } 364 | ``` 365 | 366 | The `wrap` method you see here is actually just explicit usage of the 367 | implicit instance of `Coercible[Int, Nat.Type]`. 368 | See the section on [Coercible](#coercible) for more info. 369 | 370 | #### Legacy extension methods 371 | 372 | You probably want to be able to add methods to your newtypes. You can do this using 373 | Scala's extension methods via implicit classes - 374 | 375 | ```scala 376 | type Point = Point.Type 377 | object Point extends NewType.Of[(Int, Int)] { 378 | 379 | def apply(x: Int, y: Int): Type = wrap((x, y)) 380 | 381 | implicit final class Ops(val self: Type) extends AnyVal { 382 | def toTuple: (Int, Int) = unwrap(self) 383 | def x: Int = toTuple._1 384 | def y: Int = toTuple._2 385 | } 386 | } 387 | ``` 388 | ```scala 389 | scala> val p = Point(1, 2) 390 | p: Point.Type = (1,2) 391 | 392 | scala> p.toTuple 393 | res7: (Int, Int) = (1,2) 394 | 395 | scala> p.x 396 | res8: Int = 1 397 | 398 | scala> p.y 399 | res9: Int = 2 400 | ``` 401 | 402 | #### Legacy type class instances and implicits 403 | 404 | As mentioned, the object you create via extending one of the `NewType` helpers 405 | functions as the companion object for your newtype. As such, you can leverage this 406 | for type class instances to avoid orphan instances - 407 | 408 | ```scala 409 | object Nat extends NewType.Of[Int] { 410 | implicit val show: Show[Type] = Show.instance(_.toString) 411 | } 412 | ``` 413 | 414 | If you use `NewType.Default`, you can use the `deriving` method to derive 415 | type class instances for those that exist for your newtype's base type. 416 | 417 | ```scala 418 | object Nat extends NewType.Default[Int] { 419 | implicit def show: Show[Type] = deriving 420 | } 421 | ``` 422 | 423 | As long as an implicit instance of `Show[Int]` exists in scope, `deriving` will 424 | cast the instance to one suitable for your newtype. This is similar to GHC Haskell's 425 | `GeneralizedNewtypeDeriving` extension. 426 | 427 | #### Legacy NewSubType 428 | 429 | With `NewType`, you get a brand new type that can't be used as the type you 430 | are wrapping. 431 | 432 | ```scala 433 | type Nat = Nat.Type 434 | object Nat extends NewType.Default[Int] 435 | 436 | def plus(x: Int, y: Int): Int = x + y 437 | ``` 438 | ```scala 439 | scala> plus(Nat(1), Nat(2)) 440 | :19: error: type mismatch; 441 | found : Nat.Type 442 | required: Int 443 | ``` 444 | 445 | If you wish for your newtype to be a subtype of the type you are wrapping, 446 | you can use `NewSubType` - 447 | 448 | ```scala 449 | type Nat = Nat.Type 450 | object Nat extends NewSubType.Default[Int] 451 | 452 | def plus(x: Int, y: Int): Int = x + y 453 | ``` 454 | ```scala 455 | scala> plus(Nat(1), Nat(2)) 456 | res0: Int = 3 457 | ``` 458 | 459 | ## Coercible 460 | 461 | This library introduces the `Coercible` type class for types that can safely 462 | be cast to/from newtypes. This is mostly useful when you want to write code 463 | that can work generically with newtypes or to simply leverage the compiler 464 | to tell you when you can do `.asInstanceOf`. 465 | 466 | **NOTE: You generally shouldn't be creating instances of Coercible yourself.** 467 | This library is designed to create the instances needed for you which are safe. 468 | If you manually create instances, you may be permitting unsafe operations which will 469 | lead to runtime casting errors. 470 | 471 | With that out of the way, here's how we can do safe casting with Coercible - 472 | 473 | ```scala 474 | type Point = Point.Type 475 | object Point extends NewType.Of[(Int, Int)] 476 | ``` 477 | ```scala 478 | scala> Coercible[Point, (Int, Int)] 479 | res10: io.estatico.newtype.Coercible[Point,(Int, Int)] = io.estatico.newtype.Coercible$$anon$1@56c24c2a 480 | 481 | scala> Coercible[(Int, Int), Point] 482 | res11: io.estatico.newtype.Coercible[(Int, Int),Point] = io.estatico.newtype.Coercible$$anon$1@56c24c2a 483 | 484 | scala> Coercible[String, Point] 485 | :21: error: could not find implicit value for parameter ev: io.estatico.newtype.Coercible[String,Point] 486 | Coercible[String, Point] 487 | ``` 488 | 489 | This library provides extension methods for safe casting as well - 490 | 491 | ```scala 492 | scala> import io.estatico.newtype.ops._ 493 | import io.estatico.newtype.ops._ 494 | 495 | scala> val p = Point(1, 2) 496 | p: Point.Type = (1,2) 497 | 498 | scala> p.coerce[(Int, Int)] 499 | res14: (Int, Int) = (1,2) 500 | 501 | scala> (3, 4).coerce[Point] 502 | res15: Point = (3,4) 503 | 504 | scala> (3.2, 4.3).coerce[Point] 505 | :24: error: could not find implicit value for parameter ev: io.estatico.newtype.Coercible[(Double, Double),Point] 506 | (3.2, 4.3).coerce[Point] 507 | ^ 508 | ``` 509 | 510 | ## Motivation 511 | 512 | The Haskell language provides a `newtype` keyword for creating new types from existing 513 | ones without runtime overhead. 514 | 515 | ```haskell 516 | newtype WidgetId = WidgetId Int 517 | 518 | lookupWidget :: WidgetId -> Maybe Widget 519 | lookupWidget (WidgetId wId) = lookup wId widgetDB 520 | ``` 521 | 522 | In the example above, the `WidgetId` type is simply an `Int` at runtime; however, the 523 | compiler will treat it as its own type at compile time, helping you to avoid errors. 524 | In this case, we can be sure that the ID we are providing to our `lookupWidget` function 525 | refers to a `WidgetId` and not some other entity nor an arbitrary `Int` value. 526 | 527 | This library attempts to bring newtypes to Scala. 528 | 529 | ### Tagged Types 530 | 531 | Both Scalaz and Shapeless provide a feature known as _Tagged Types_. This library 532 | operates on roughly the same principle except provides the proper infrastructure 533 | needed to - 534 | 535 | * Control whether newtypes are or are not subtypes of their wrapped type instead of picking 536 | a side (Shapeless' are subtypes, Scalaz's are not) 537 | * Easily provide methods for newtypes 538 | * Resolve implicits and type class instances defined in the companion object 539 | * Optimize constructing newtypes via casting with automatic smart constructors 540 | * Provide facilities to operate generically on newtypes 541 | * Support safe casting generically via the `Coercible` type class 542 | --------------------------------------------------------------------------------