Skip to content

Commit

Permalink
Macro-accelerated record selection and update. Fixes #50, #102, #420.
Browse files Browse the repository at this point in the history
  • Loading branch information
milessabin committed Aug 30, 2015
1 parent 4d135ec commit d4c3c71
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 31 deletions.
3 changes: 3 additions & 0 deletions core/src/main/scala/shapeless/coproduct.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package shapeless
import scala.language.dynamics
import scala.language.experimental.macros

import scala.annotation.tailrec

/** Encodes a coproduct type, such as a sealed family of case classes.
*
* Each constructor from the family gets an encoding in terms of nested Inr and Inl.
Expand Down Expand Up @@ -108,6 +110,7 @@ object Coproduct extends Dynamic {
def unsafeMkCoproduct(length: Int, value: Any) =
(0 until length).foldLeft[Coproduct](Inl(value))((accum, _) => Inr(accum))

@tailrec
def unsafeGet(c: Coproduct): Any = c match {
case Inl(h) => h
case Inr(c) => unsafeGet(c)
Expand Down
26 changes: 26 additions & 0 deletions core/src/main/scala/shapeless/generic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ trait ReprTypes {

def atatTpe = typeOf[tag.@@[_,_]].typeConstructor
def fieldTypeTpe = typeOf[shapeless.labelled.FieldType[_, _]].typeConstructor
def keyTagTpe = typeOf[shapeless.labelled.KeyTag[_, _]].typeConstructor

This comment has been minimized.

Copy link
@retronym

retronym Aug 30, 2015

Contributor

I'd be inclined to make all these defintions into lazy vals so you only pay the cost to look them up once per macro expansion. (It would be better to make it once-per-compiler run, but the macro engine doesn't make that sort of caching super easy...)

This comment has been minimized.

Copy link
@milessabin

milessabin Aug 30, 2015

Author Owner

There was a problem with making these lazy vals ... I don't recall the details right now.

}

trait CaseClassMacros extends ReprTypes {
Expand Down Expand Up @@ -447,6 +448,31 @@ trait CaseClassMacros extends ReprTypes {
def mkCoproductTpe(items: List[Type]): Type =
mkCompoundTpe(cnilTpe, cconsTpe, items)

def unpackHListTpe(tpe: Type): List[Type] = {
@tailrec
def unfold(u: Type, acc: List[Type]): List[Type] = {
val HNilTpe = hnilTpe
val HConsPre = prefix(hconsTpe)
val HConsSym = hconsTpe.typeSymbol
u.dealias match {

This comment has been minimized.

Copy link
@retronym

retronym Aug 30, 2015

Contributor

Better:

if (u <:< HNilTpe) ..
else (u baseType HConsSym) match {
  case TypeRef(pre, _, List(hd, tl)) => ...
  case _ => ...
}

Rule of thumb: avoid using dealias, where you can instead use baseType.

case t if t <:< HNilTpe => acc
case TypeRef(pre, HConsSym, List(hd, tl)) if pre =:= HConsPre => unfold(tl, hd :: acc)
case _ => abort(s"$tpe is not an HList type")
}
}

unfold(tpe, List()).reverse
}

def unpackFieldType(tpe: Type): (Type, Type) = {
val KeyTagPre = prefix(keyTagTpe)
val KeyTagSym = keyTagTpe.typeSymbol
tpe.dealias match {
case RefinedType(List(v0, TypeRef(pre, KeyTagSym, List(k, v1))), _) if pre =:= KeyTagPre && v0 =:= v1 => (k, v0)
case _ => abort(s"$tpe is not a field type")
}
}

def mkTypTree(tpe: Type): Tree = {
tpe match {
case SingleType(pre @ SingleType(_, _), sym) =>
Expand Down
19 changes: 19 additions & 0 deletions core/src/main/scala/shapeless/hlists.scala
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,25 @@ object HList extends Dynamic {
*/
def selectDynamic(tpeSelector: String): Any = macro LabelledMacros.hlistTypeImpl

@tailrec
def unsafeGet(l: HList, i: Int): Any = {
val c = l.asInstanceOf[::[Any, HList]]
if(i == 0) c.head
else unsafeGet(c.tail, i-1)
}

def unsafeUpdate(l: HList, i: Int, e: Any): HList = {
@tailrec
def loop(l: HList, i: Int, prefix: List[Any]): (List[Any], HList) =
l match {
case HNil => (prefix, e :: HNil)
case hd :: (tl : HList) if i == 0 => (prefix, e :: tl)
case hd :: (tl : HList) => loop(tl, i-1, hd :: prefix)
}

val (prefix, suffix) = loop(l, i, Nil)
prefix.foldLeft(suffix) { (tl, hd) => hd :: tl }
}
}


Expand Down
86 changes: 55 additions & 31 deletions core/src/main/scala/shapeless/ops/records.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
package shapeless
package ops

import scala.language.experimental.macros
import scala.reflect.macros.{ blackbox, whitebox }

import poly._

//object record {
Expand All @@ -37,25 +40,39 @@ package record {
def apply(l : L): Out
}

trait LowPrioritySelector {
object Selector {
type Aux[L <: HList, K, Out0] = Selector[L, K] { type Out = Out0 }

implicit def hlistSelect[H, T <: HList, K]
(implicit st : Selector[T, K]): Aux[H :: T, K, st.Out] =
new Selector[H :: T, K] {
type Out = st.Out
def apply(l : H :: T): Out = st(l.tail)
}
def apply[L <: HList, K](implicit selector: Selector[L, K]): Aux[L, K, selector.Out] = selector

implicit def mkSelector[L <: HList, K, O]: Aux[L, K, O] = macro SelectorMacros.applyImpl[L, K]
}

object Selector extends LowPrioritySelector {
def apply[L <: HList, K](implicit selector: Selector[L, K]): Aux[L, K, selector.Out] = selector
class SelectorMacros(val c: whitebox.Context) extends CaseClassMacros {
import c.universe._

def applyImpl[L <: HList, K](implicit lTag: WeakTypeTag[L], kTag: WeakTypeTag[K]): Tree = {
val lTpe = lTag.tpe.dealias
val kTpe = kTag.tpe.dealias
if(!(lTpe <:< hlistTpe))
abort(s"$lTpe is not a record type")

implicit def hlistSelect1[K, V, T <: HList]: Aux[FieldType[K, V] :: T, K, V] =
new Selector[FieldType[K, V] :: T, K] {
type Out = V
def apply(l : FieldType[K, V] :: T): Out = l.head
val lTpes = unpackHListTpe(lTpe).zipWithIndex.flatMap { case (fTpe, i) =>
val (k, v) = unpackFieldType(fTpe)
if(k.dealias =:= kTpe) Some((v, i)) else None
}
lTpes.headOption match {
case Some((vTpe, i)) =>
q"""
new _root_.shapeless.ops.record.Selector[$lTpe, $kTpe] {
type Out = $vTpe
def apply(l: $lTpe): $vTpe = _root_.shapeless.HList.unsafeGet(l, $i).asInstanceOf[$vTpe]
}: _root_.shapeless.ops.record.Selector.Aux[$lTpe, $kTpe, $vTpe]
"""
case _ =>
abort(s"No field $kTpe in record type $lTpe")
}
}
}

/**
Expand Down Expand Up @@ -95,31 +112,38 @@ package record {
*/
trait Updater[L <: HList, F] extends DepFn2[L, F] with Serializable { type Out <: HList }

trait LowPriorityUpdater {
object Updater {
type Aux[L <: HList, F, Out0 <: HList] = Updater[L, F] { type Out = Out0 }

implicit def hlistUpdater1[H, T <: HList, K, V]
(implicit ut : Updater[T, FieldType[K, V]]): Aux[H :: T, FieldType[K, V], H :: ut.Out] =
new Updater[H :: T, FieldType[K, V]] {
type Out = H :: ut.Out
def apply(l: H :: T, f: FieldType[K, V]): Out = l.head :: ut(l.tail, f)
}
def apply[L <: HList, F](implicit updater: Updater[L, F]): Aux[L, F, updater.Out] = updater

implicit def mkUpdater[L <: HList, F, O]: Aux[L, F, O] = macro UpdaterMacros.applyImpl[L, F]
}

object Updater extends LowPriorityUpdater {
def apply[L <: HList, F](implicit updater: Updater[L, F]): Aux[L, F, updater.Out] = updater
class UpdaterMacros(val c: whitebox.Context) extends CaseClassMacros {
import c.universe._

implicit def hnilUpdater[L <: HNil, F]: Aux[L, F, F :: HNil] =
new Updater[L, F] {
type Out = F :: HNil
def apply(l: L, f: F): Out = f :: HNil
}
def applyImpl[L <: HList, F](implicit lTag: WeakTypeTag[L], fTag: WeakTypeTag[F]): Tree = {
val lTpe = lTag.tpe.dealias
val fTpe = fTag.tpe.dealias
if(!(lTpe <:< hlistTpe))
abort(s"$lTpe is not a record type")

implicit def hlistUpdater2[K, V, T <: HList]: Aux[FieldType[K, V] :: T, FieldType[K, V], FieldType[K, V] :: T] =
new Updater[FieldType[K, V] :: T, FieldType[K, V]] {
type Out = FieldType[K, V] :: T
def apply(l: FieldType[K, V] :: T, f: FieldType[K, V]): Out = f :: l.tail
val lTpes = unpackHListTpe(lTpe)
val (uTpes, i) = {
val i0 = lTpes.indexWhere(_.dealias =:= fTpe)

This comment has been minimized.

Copy link
@retronym

retronym Aug 30, 2015

Contributor

Dealiasing here should not change the result of =:=, what was your motivation to use it?

This comment has been minimized.

Copy link
@milessabin

milessabin Aug 30, 2015

Author Owner

Excessive caution most likely ... I'll try removing it.

if(i0 < 0) (lTpes :+ fTpe, lTpes.length)
else (lTpes.updated(i0, fTpe), i0)
}
val uTpe = mkHListTpe(uTpes)
q"""
new _root_.shapeless.ops.record.Updater[$lTpe, $fTpe] {
type Out = $uTpe
def apply(l: $lTpe, f: $fTpe): $uTpe =
_root_.shapeless.HList.unsafeUpdate(l, $i, f).asInstanceOf[$uTpe]
}: _root_.shapeless.ops.record.Updater.Aux[$lTpe, $fTpe, $uTpe]
"""
}
}

/**
Expand Down

0 comments on commit d4c3c71

Please sign in to comment.