Skip to content

Commit

Permalink
refs #10: Add NewTypeArray, support Array coercion
Browse files Browse the repository at this point in the history
  • Loading branch information
carymrobbins committed Aug 11, 2018
1 parent 7fde63c commit e9830f5
Show file tree
Hide file tree
Showing 3 changed files with 276 additions and 26 deletions.
34 changes: 34 additions & 0 deletions shared/src/main/scala/io/estatico/newtype/NewTypeArray.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.estatico.newtype

import scala.reflect.ClassTag

/** Type class for building arrays for newtypes. */
trait NewTypeArray[N] {
type Repr
def clsTag: ClassTag[Repr]

final def empty: Array[N] = Array.empty(clsTag).asInstanceOf[Array[N]]

final def apply(xs: N*): Array[N] =
Array(xs.asInstanceOf[Seq[Repr]]: _*)(clsTag).asInstanceOf[Array[N]]

final def upcast(a: Array[Repr]): Array[N] = a.asInstanceOf[Array[N]]

final def downcast(a: Array[N]): Array[Repr] = a.asInstanceOf[Array[Repr]]
}

object NewTypeArray {

type Aux[N, R] = NewTypeArray[N] { type Repr = R }

def apply[N](implicit ev: NewTypeArray[N]): Aux[N, ev.Repr] = ev

def apply[N](xs: N*)(implicit ev: NewTypeArray[N]): Array[N] = ev(xs: _*)

def empty[N](implicit ev: NewTypeArray[N]): Array[N] = ev.empty

def unsafeDerive[N, R](implicit ct: ClassTag[R]): Aux[N, R] = new NewTypeArray[N] {
type Repr = R
override def clsTag: ClassTag[Repr] = ct
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.estatico.newtype.macros

import io.estatico.newtype.Coercible
import io.estatico.newtype.{Coercible, NewTypeArray}
import scala.reflect.ClassTag
import scala.reflect.macros.blackbox

Expand Down Expand Up @@ -39,6 +39,8 @@ private[macros] class NewTypeMacros(val c: blackbox.Context)

val CoercibleCls = typeOf[Coercible[Nothing, Nothing]].typeSymbol
val CoercibleObj = CoercibleCls.companion
val NewTypeArrayCls = typeOf[NewTypeArray[Nothing]].typeSymbol
val NewTypeArrayObj = NewTypeArrayCls.companion
val ClassTagCls = typeOf[ClassTag[Nothing]].typeSymbol
val ClassTagObj = ClassTagCls.companion
val ObjectCls = typeOf[Object].typeSymbol
Expand Down Expand Up @@ -123,7 +125,8 @@ private[macros] class NewTypeMacros(val c: blackbox.Context)
maybeGenerateUnapplyMethod(clsDef, valDef, tparamsNoVar, tparamNames) :::
maybeGenerateOpsDef(clsDef, valDef, tparamsNoVar, tparamNames) :::
generateCoercibleInstances(tparamsNoVar, tparamNames, tparamsWild) :::
generateDerivingMethods(tparamsNoVar, tparamNames, tparamsWild)
generateDerivingMethods(tparamsNoVar, tparamNames, tparamsWild) :::
List(generateNewTypeArrayInstance(clsDef, valDef, tparamsNoVar, tparamNames))

val newtypeObjParents = objParents :+ tq"$typesTraitName"
val newtypeObjDef = ModuleDef(
Expand Down Expand Up @@ -290,29 +293,49 @@ private[macros] class NewTypeMacros(val c: blackbox.Context)
def generateCoercibleInstances(
tparamsNoVar: List[TypeDef], tparamNames: List[TypeName], tparamsWild: List[TypeDef]
): List[Tree] = {
if (tparamsNoVar.isEmpty) List(
q"@_root_.scala.inline implicit def unsafeWrap: $CoercibleCls[Repr, Type] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def unsafeUnwrap: $CoercibleCls[Type, Repr] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def unsafeWrapM[M[_]]: $CoercibleCls[M[Repr], M[Type]] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def unsafeUnwrapM[M[_]]: $CoercibleCls[M[Type], M[Repr]] = $CoercibleObj.instance",
// Avoid ClassCastException with Array types by prohibiting Array coercing.
q"@_root_.scala.inline implicit def cannotWrapArrayAmbiguous1: $CoercibleCls[_root_.scala.Array[Repr], _root_.scala.Array[Type]] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def cannotWrapArrayAmbiguous2: $CoercibleCls[_root_.scala.Array[Repr], _root_.scala.Array[Type]] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def cannotUnwrapArrayAmbiguous1: $CoercibleCls[_root_.scala.Array[Type], _root_.scala.Array[Repr]] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def cannotUnwrapArrayAmbiguous2: $CoercibleCls[_root_.scala.Array[Type], _root_.scala.Array[Repr]] = $CoercibleObj.instance"
) else List(
val stdWraps = List(
q"@_root_.scala.inline implicit def unsafeWrap[..$tparamsNoVar]: $CoercibleCls[Repr[..$tparamNames], Type[..$tparamNames]] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def unsafeUnwrap[..$tparamsNoVar]: $CoercibleCls[Type[..$tparamNames], Repr[..$tparamNames]] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def unsafeWrapM[M[_], ..$tparamsNoVar]: $CoercibleCls[M[Repr[..$tparamNames]], M[Type[..$tparamNames]]] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def unsafeUnwrapM[M[_], ..$tparamsNoVar]: $CoercibleCls[M[Type[..$tparamNames]], M[Repr[..$tparamNames]]] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def unsafeUnwrapM[M[_], ..$tparamsNoVar]: $CoercibleCls[M[Type[..$tparamNames]], M[Repr[..$tparamNames]]] = $CoercibleObj.instance"
)
val wrapKs = if (tparamsNoVar.isEmpty) Nil else List(
q"@_root_.scala.inline implicit def unsafeWrapK[T[_[..$tparamsNoVar]]]: $CoercibleCls[T[Repr], T[Type]] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def unsafeUnwrapK[T[_[..$tparamsNoVar]]]: $CoercibleCls[T[Type], T[Repr]] = $CoercibleObj.instance",
// Avoid ClassCastException with Array types by prohibiting Array coercing.
q"@_root_.scala.inline implicit def cannotWrapArrayAmbiguous1[..$tparamsNoVar]: $CoercibleCls[_root_.scala.Array[Repr[..$tparamNames]], _root_.scala.Array[Type[..$tparamNames]]] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def cannotWrapArrayAmbiguous2[..$tparamsNoVar]: $CoercibleCls[_root_.scala.Array[Repr[..$tparamNames]], _root_.scala.Array[Type[..$tparamNames]]] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def cannotUnwrapArrayAmbiguous1[..$tparamsNoVar]: $CoercibleCls[_root_.scala.Array[Type[..$tparamNames]], _root_.scala.Array[Repr[..$tparamNames]]] = $CoercibleObj.instance",
q"@_root_.scala.inline implicit def cannotUnwrapArrayAmbiguous2[..$tparamsNoVar]: $CoercibleCls[_root_.scala.Array[Type[..$tparamNames]], _root_.scala.Array[Repr[..$tparamNames]]] = $CoercibleObj.instance"
q"@_root_.scala.inline implicit def unsafeUnwrapK[T[_[..$tparamsNoVar]]]: $CoercibleCls[T[Type], T[Repr]] = $CoercibleObj.instance"
)
stdWraps ++ wrapKs
}

def generateNewTypeArrayInstance(
clsDef: ClassDef, valDef: ValDef, tparamsNoVar: List[TypeDef], tparamNames: List[TypeName]
): Tree = {
val Repr = tq"${valDef.tpt}"
val Type = if (tparamNames.isEmpty) tq"${clsDef.name}" else tq"${clsDef.name}[..$tparamNames]"
summonImplicit(tq"$ClassTagCls[$Repr]") match {
case Some(Typed(ct, _)) =>
if (tparamNames.isEmpty) {
q"""implicit val newtypeArray: $NewTypeArrayObj.Aux[$Type, $Repr] =
$NewTypeArrayObj.unsafeDerive[$Type, $Repr]($ct)"""
} else {
q"""implicit def newtypeArray[..$tparamsNoVar]: $NewTypeArrayObj.Aux[$Type, $Repr] =
__newtypeArray.asInstanceOf[$NewTypeArrayObj.Aux[$Type, $Repr]]
private val __newtypeArray = $NewTypeArrayObj.unsafeDerive[Any, $Repr]($ct)"""
}
case _ =>
q"""implicit def newtypeArray[..$tparamsNoVar](
implicit ct: $ClassTagCls[$Repr]
): $NewTypeArrayObj.Aux[$Type, $Repr] =
$NewTypeArrayObj.unsafeDerive[$Type, $Repr]"""
}
}

/** Return the implicit value, if exists, for the given type `tpt`. */
def summonImplicit(tpt: Tree): Option[Tree] = {
val typeResult = c.typecheck(tpt, c.TYPEmode, silent = true)
if (typeResult.isEmpty) None else {
val implicitResult = c.inferImplicitValue(typeResult.tpe)
if (implicitResult.isEmpty) None else Some(implicitResult)
}
}

def getConstructor(body: List[Tree]): DefDef = body.collectFirst {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package io.estatico.newtype.macros

import io.estatico.newtype.NewTypeArray
import org.scalatest.{FlatSpec, Matchers}
import io.estatico.newtype.ops._
import org.scalacheck.Arbitrary
import org.scalatest.exceptions.TestFailedException
import scala.reflect.runtime.universe.WeakTypeTag

class NewTypeMacrosTest extends FlatSpec with Matchers {

Expand Down Expand Up @@ -39,9 +42,7 @@ class NewTypeMacrosTest extends FlatSpec with Matchers {

it should "work in arrays" in {
val foo = Foo(313)
// See https://github.com/estatico/scala-newtype/issues/25
// Array(foo).apply(0) shouldBe foo
Array[Int](313).asInstanceOf[Array[Foo]].apply(0) shouldBe foo
NewTypeArray(foo).apply(0) shouldBe foo
}

behavior of "@newtype class"
Expand Down Expand Up @@ -102,9 +103,7 @@ class NewTypeMacrosTest extends FlatSpec with Matchers {
import scala.collection.immutable.Set
val repr = Set(Option("newtypes"))
val ot = OptionT(repr)
// See https://github.com/estatico/scala-newtype/issues/25
// Array(ot).apply(0) shouldBe ot
Array(repr).asInstanceOf[Array[OptionT[Set, String]]].apply(0) shouldBe ot
Array(repr).coerce[Array[OptionT[Set, String]]].apply(0) shouldBe ot
}

behavior of "@newtype with type bounds"
Expand Down Expand Up @@ -363,6 +362,200 @@ class NewTypeMacrosTest extends FlatSpec with Matchers {
assertTypeError(""" 1 match { case Y1(x) => x }""")
}

behavior of "NewTypeArray"

it should "work with @newtype X(Int)" in {
@newtype case class X(private val value: Int)

val cls = scala.Predef.classOf[Array[Int]]
val v: Int = 1

val a = NewTypeArray(X(v))
assertCompiles("a: Array[X]")
a.apply(0) shouldBe v
a.getClass shouldBe cls

val a2 = NewTypeArray[X].downcast(a)
assertCompiles("a2: Array[Int]")
a2.apply(0) shouldBe v
a2.getClass shouldBe cls

val a3 = NewTypeArray[X].upcast(a2)
assertCompiles("a3: Array[X]")
a3.apply(0) shouldBe v
a3.getClass shouldBe cls

val a4 = a.coerce[Array[Int]]
assertCompiles("a4: Array[Int]")
a4.apply(0) shouldBe v
a4.getClass shouldBe cls

val a5 = a4.coerce[Array[X]]
assertCompiles("a5: Array[X]")
a5.apply(0) shouldBe v
a5.getClass shouldBe cls
}

it should "work with @newsubtype X(Int)" in {
@newsubtype case class X(private val value: Int)

val cls = scala.Predef.classOf[Array[Int]]
val v: Int = 1

val a = NewTypeArray(X(v))
assertCompiles("a: Array[X]")
a.apply(0) shouldBe v
a.getClass shouldBe cls

val a2 = NewTypeArray[X].downcast(a)
assertCompiles("a2: Array[Int]")
a2.apply(0) shouldBe v
a2.getClass shouldBe cls

val a3 = NewTypeArray[X].upcast(a2)
assertCompiles("a3: Array[X]")
a3.apply(0) shouldBe v
a3.getClass shouldBe cls

val a4 = a.coerce[Array[Int]]
assertCompiles("a4: Array[Int]")
a4.apply(0) shouldBe v
a4.getClass shouldBe cls

val a5 = a4.coerce[Array[X]]
assertCompiles("a5: Array[X]")
a5.apply(0) shouldBe v
a5.getClass shouldBe cls
}

it should "work with @newtype X[A](List[A])" in {
@newtype case class X[A](private val value: List[A])

val cls = scala.Predef.classOf[Array[List[Int]]]
val v: List[Int] = List(1)

val a = NewTypeArray(X(v))
assertCompiles("a: Array[X[Int]]")
a.apply(0) shouldBe v
a.getClass shouldBe cls

val a2 = NewTypeArray[X[Int]].downcast(a)
assertCompiles("a2: Array[List[Int]]")
a2.apply(0) shouldBe v
a2.getClass shouldBe cls

val a3 = NewTypeArray[X[Int]].upcast(a2)
assertCompiles("a3: Array[X[Int]]")
a3.apply(0) shouldBe v
a3.getClass shouldBe cls

val a4 = a.coerce[Array[List[Int]]]
assertCompiles("a4: Array[List[Int]]")
a4.apply(0) shouldBe v
a4.getClass shouldBe cls

val a5 = a4.coerce[Array[X[Int]]]
assertCompiles("a5: Array[X[Int]]")
a5.apply(0) shouldBe v
a5.getClass shouldBe cls
}

it should "work with @newsubtype X[A](List[A])" in {
@newsubtype case class X[A](private val value: List[A])

val cls = scala.Predef.classOf[Array[List[Int]]]
val v: List[Int] = List(1)

val a = NewTypeArray(X(v))
assertCompiles("a: Array[X[Int]]")
a.apply(0) shouldBe v
a.getClass shouldBe cls

val a2 = NewTypeArray[X[Int]].downcast(a)
assertCompiles("a2: Array[List[Int]]")
a2.apply(0) shouldBe v
a2.getClass shouldBe cls

val a3 = NewTypeArray[X[Int]].upcast(a2)
assertCompiles("a3: Array[X[Int]]")
a3.apply(0) shouldBe v
a3.getClass shouldBe cls

val a4 = a.coerce[Array[List[Int]]]
assertCompiles("a4: Array[List[Int]]")
a4.apply(0) shouldBe v
a4.getClass shouldBe cls

val a5 = a4.coerce[Array[X[Int]]]
assertCompiles("a5: Array[X[Int]]")
a5.apply(0) shouldBe v
a5.getClass shouldBe cls
}

it should "work with @newtype X[F[_]](Functor[F])" in {
@newtype case class X[F[_]](private val value: Functor[F])

val cls = scala.Predef.classOf[Array[Functor[List]]]
val v: Functor[List] = Functor.list

val a = NewTypeArray(X(v))
assertCompiles("a: Array[X[List]]")
a.apply(0) shouldBe v
a.getClass shouldBe cls

val a2 = NewTypeArray[X[List]].downcast(a)
assertCompiles("a2: Array[Functor[List]]")
a2.apply(0) shouldBe v
a2.getClass shouldBe cls

val a3 = NewTypeArray[X[List]].upcast(a2)
assertCompiles("a3: Array[X[List]]")
a3.apply(0) shouldBe v
a3.getClass shouldBe cls

val a4 = a.coerce[Array[Functor[List]]]
assertCompiles("a4: Array[Functor[List]]")
a4.apply(0) shouldBe v
a4.getClass shouldBe cls

val a5 = a4.coerce[Array[X[List]]]
assertCompiles("a5: Array[X[List]]")
a5.apply(0) shouldBe v
a5.getClass shouldBe cls
}

it should "work with @newsubtype X[F[_]](Functor[F])" in {
@newsubtype case class X[F[_]](private val value: Functor[F])

val cls = scala.Predef.classOf[Array[Functor[List]]]
val v: Functor[List] = Functor.list

val a = NewTypeArray(X(v))
assertCompiles("a: Array[X[List]]")
a.apply(0) shouldBe v
a.getClass shouldBe cls

val a2 = NewTypeArray[X[List]].downcast(a)
assertCompiles("a2: Array[Functor[List]]")
a2.apply(0) shouldBe v
a2.getClass shouldBe cls

val a3 = NewTypeArray[X[List]].upcast(a2)
assertCompiles("a3: Array[X[List]]")
a3.apply(0) shouldBe v
a3.getClass shouldBe cls

val a4 = a.coerce[Array[Functor[List]]]
assertCompiles("a4: Array[Functor[List]]")
a4.apply(0) shouldBe v
a4.getClass shouldBe cls

val a5 = a4.coerce[Array[X[List]]]
assertCompiles("a5: Array[X[List]]")
a5.apply(0) shouldBe v
a5.getClass shouldBe cls
}

// Unfortunately, we don't have a way to assert on compiler warnings, which is
// what happens with the code below. If we run with -Xfatal-warnings, the test
// won't compile at all, so leaving here to do manual checking until scalatest
Expand Down

0 comments on commit e9830f5

Please sign in to comment.