diff --git a/core/src/main/scala/shapeless/hlists.scala b/core/src/main/scala/shapeless/hlists.scala index de6ecbb1f..aa11ade4b 100644 --- a/core/src/main/scala/shapeless/hlists.scala +++ b/core/src/main/scala/shapeless/hlists.scala @@ -18,7 +18,6 @@ package shapeless import scala.language.dynamics import scala.language.experimental.macros - import scala.annotation.tailrec import scala.reflect.macros.whitebox @@ -105,12 +104,14 @@ object HList extends Dynamic { def selectDynamic(tpeSelector: String): Any = macro LabelledMacros.hlistTypeImpl @tailrec + @deprecated("used for binary compatibility with 2.3.0", "2.3.1") def unsafeGet(l: HList, i: Int): Any = { val c = l.asInstanceOf[::[Any, HList]] if(i == 0) c.head else unsafeGet(c.tail, i-1) } + @deprecated("used for binary compatibility with 2.3.0", "2.3.1") def unsafeUpdate(l: HList, i: Int, e: Any): HList = { @tailrec def loop(l: HList, i: Int, prefix: List[Any]): (List[Any], HList) = @@ -123,6 +124,24 @@ object HList extends Dynamic { val (prefix, suffix) = loop(l, i, Nil) prefix.foldLeft(suffix) { (tl, hd) => hd :: tl } } + + //TODO: remove @noinline after switching to ScalaJs 0.69+ + @noinline + def unsafeCrud(l: HList, i: Int, f: Any => Any, remove:Boolean): (Any, HList) = { + @tailrec + def loop(l: HList, i: Int, prefix: List[Any]): (List[Any], HList, Any) = + l match { + //add case + case HNil => if(remove)throw new Exception("Index out of bounds.Cannot remove.") else (prefix, f(null) :: HNil, null) + //remove / modify case + case hd :: (tl: HList) if i == 0 => if(remove)(prefix, tl, hd) else (prefix, f(hd) :: tl, hd) + //recursion step + case hd :: (tl: HList) => loop(tl, i - 1, hd :: prefix) + } + + val (prefix, suffix, v) = loop(l, i, Nil) + v -> prefix.foldLeft(suffix) { (tl, hd) => hd :: tl } + } } diff --git a/core/src/main/scala/shapeless/ops/records.scala b/core/src/main/scala/shapeless/ops/records.scala index d0195777d..0d3edbac3 100644 --- a/core/src/main/scala/shapeless/ops/records.scala +++ b/core/src/main/scala/shapeless/ops/records.scala @@ -19,6 +19,7 @@ package ops import scala.language.experimental.macros import scala.reflect.macros.{ blackbox, whitebox } +import scala.annotation.tailrec import poly._ @@ -49,37 +50,35 @@ package record { } + //used for binary compatibility with 2.3.0 class UnsafeSelector(i: Int) extends Selector[HList, Any] { type Out = Any - def apply(l: HList): Any = HList.unsafeGet(l, i) + def apply(l: HList): Any = HList.unsafeCrud(l, i, identity, false)._1 } @macrocompat.bundle - class SelectorMacros(val c: whitebox.Context) extends CaseClassMacros { + class SelectorMacros(val c: whitebox.Context) extends CrudMacros { 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") - val lTpes = unpackHListTpe(lTpe).zipWithIndex.flatMap { case (fTpe, i) => - val (k, v) = unpackFieldType(fTpe) - if(k =:= kTpe) Some((v, i)) else None - } - lTpes.headOption match { - case Some((vTpe, i)) => - q""" - new _root_.shapeless.ops.record.UnsafeSelector($i). - asInstanceOf[_root_.shapeless.ops.record.Selector.Aux[$lTpe, $kTpe, $vTpe]] + val(ind, vTpe) = getValue(kTpe, unpackHListTpe(lTpe)) + if(ind == -1) abort(s"No field $kTpe in record type $lTpe") + else { + q""" + (new _root_.shapeless.ops.record.Selector[$lTpe, $kTpe]{ + type Out = Any + def apply(l : $lTpe): Out= _root_.shapeless.HList.unsafeCrud(l, $ind, v => v, false)._1 + }).asInstanceOf[_root_.shapeless.ops.record.Selector.Aux[$lTpe, $kTpe, $vTpe]] """ - case _ => - abort(s"No field $kTpe in record type $lTpe") } } } + /** * Type class supporting multiple record field selection. * @@ -110,6 +109,93 @@ package record { } } + /** + * Type class supporting record field addition. + * Works only if record does not have given key. + * + * @author Ievgen Garkusha + */ + @annotation.implicitNotFound(msg = "Field ${K} is already present in record ${L}") + trait Adder[L <: HList, K, V] extends DepFn2[L, V] with Serializable { + type Out <: HList + def apply(l : L, v: V): Out + } + + object Adder { + type Aux[L <: HList, K, V, Out0 <: HList] = Adder[L, K, V] { type Out = Out0 } + + def apply[L <: HList, K, V](implicit adder: Adder[L, K, V]): Aux[L, K, V, adder.Out] = adder + + implicit def mkAdder[L <: HList, K, V, O]: Aux[L, K, V, O] = macro AdderMacros.applyImpl[L, K, V] + } + + @macrocompat.bundle + class AdderMacros(val c: whitebox.Context) extends CrudMacros { + import c.universe._ + + def applyImpl[L <: HList, K, V](implicit lTag: WeakTypeTag[L], kTag: WeakTypeTag[K], vTag : WeakTypeTag[V]): Tree = { + + val lTpe = lTag.tpe.dealias + val vTpe = vTag.tpe.dealias + val kTpe = kTag.tpe.dealias + val lTpes = unpackHListTpe(lTpe) + + if(getValue(kTpe, lTpes)._1 != -1) abort(s"Record $lTpe alredy contains key $kTpe") + val aTpe = add(lTpes ,kTpe, vTpe) + + q""" + new _root_.shapeless.ops.record.Adder[$lTpe, $kTpe, $vTpe]{ + type Out = HList + def apply(l: $lTpe, v: $vTpe): HList = HList.unsafeCrud(l, Int.MaxValue, _ => v, false)._2 + }.asInstanceOf[_root_.shapeless.ops.record.Adder.Aux[$lTpe, $kTpe, $vTpe, $aTpe]] + """ + } + } + + /** + * Type class supporting the replacement of a value in given record. + * Works only if a field with provided key exists and provided value is of the same type as existing value. + * + * @author Ievgen Garkusha + */ + @annotation.implicitNotFound(msg = "no field ${K} or value type differs from ${V} in record ${L}") + trait Replacer[L <: HList, K, V] extends DepFn2[L, V] with Serializable { type Out <: HList } + + object Replacer { + type Aux[L <: HList, K, V, Out0 <: HList] = Replacer[L, K, V] { type Out = Out0 } + + def apply[L <: HList, K, V](implicit replacer: Replacer[L, K, V]): Aux[L, K, V, replacer.Out] = replacer + + implicit def mkReplacer[L <: HList, K, V, O]: Aux[L, K, V, O] = macro ReplacerMacros.applyImpl[L, K, V] + } + + + @macrocompat.bundle + class ReplacerMacros(val c: whitebox.Context) extends CrudMacros { + import c.universe._ + + def applyImpl[L <: HList, K, V](implicit lTag: WeakTypeTag[L], kTag: WeakTypeTag[K], vTag: WeakTypeTag[V]): Tree = { + val lTpe = lTag.tpe.dealias + val kTpe = kTag.tpe.dealias + val replaceTpe = vTag.tpe.dealias + + val lTpes = unpackHListTpe(lTpe) + + val(ind, vTpe) = getValue(kTpe, lTpes) + + val rTpe = if (ind == -1) abort(s"No field $kTpe in record type $lTpe") + else if (!(vTpe =:= replaceTpe)) abort(s"provided value type $replaceTpe differs from existing value type $vTpe.") + else lTpe + + q""" + new _root_.shapeless.ops.record.Replacer[$lTpe, $kTpe, $vTpe]{ + type Out = HList + def apply(l: $lTpe, f: $vTpe): HList = HList.unsafeCrud(l, $ind, _ => f, false)._2 + }.asInstanceOf[_root_.shapeless.ops.record.Replacer.Aux[$lTpe, $kTpe, $vTpe, $lTpe]] + """ + } + } + /** * Type class supporting record update and extension. * @@ -125,31 +211,36 @@ package record { implicit def mkUpdater[L <: HList, F, O]: Aux[L, F, O] = macro UpdaterMacros.applyImpl[L, F] } + //used for binary compatibility with 2.3.0 class UnsafeUpdater(i: Int) extends Updater[HList, Any] { type Out = HList - def apply(l: HList, f: Any): HList = HList.unsafeUpdate(l, i, f) + def apply(l: HList, f: Any): HList = HList.unsafeCrud(l, i, _ => f,false)._2 } @macrocompat.bundle - class UpdaterMacros(val c: whitebox.Context) extends CaseClassMacros { + class UpdaterMacros(val c: whitebox.Context) extends CrudMacros { import c.universe._ 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") + val (kTpe, vUpdTpe) = unpackFieldType(fTpe) + + //commenting this out because the similar check is made in unpackHListTpe + //if(!(lTpe <:< hlistTpe)) abort(s"$lTpe is not a record type") val lTpes = unpackHListTpe(lTpe) - val (uTpes, i) = { - val i0 = lTpes.indexWhere(_ =:= fTpe) - if(i0 < 0) (lTpes :+ fTpe, lTpes.length) - else (lTpes.updated(i0, fTpe), i0) - } - val uTpe = mkHListTpe(uTpes) + + val(ind, vTpe) = getValue(kTpe, lTpes) + + val (uTpe,i) = if(ind == -1 || ! (vUpdTpe =:= vTpe)) add(lTpes, kTpe, vUpdTpe) -> lTpes.length + else lTpe -> ind + q""" - new _root_.shapeless.ops.record.UnsafeUpdater($i) - .asInstanceOf[_root_.shapeless.ops.record.Updater.Aux[$lTpe, $fTpe, $uTpe]] + (new _root_.shapeless.ops.record.Updater[$lTag, $fTpe]{ + type Out = HList + def apply(l: ${lTag.tpe}, f: $fTpe): HList = HList.unsafeCrud(l, $i, _ => f,false)._2 + }).asInstanceOf[_root_.shapeless.ops.record.Updater.Aux[$lTpe, $fTpe, $uTpe]] """ } } @@ -196,6 +287,27 @@ package record { } } + @macrocompat.bundle + trait CrudMacros extends CaseClassMacros{ + + import c.universe._ + + def getValue(kTpe: Type, lTpes:List[Type]):(Int, Type) ={ + lTpes.iterator.map(tpe => unpackFieldType(tpe)) + .zipWithIndex + .collectFirst { case ((k, v), i) if k =:= kTpe => i -> v } + .getOrElse (-1 -> null) + } + + private def newFld(kTpe:Type, rTpe:Type) = appliedType(fieldTypeTpe, List(kTpe, rTpe.widen)) + + def replace(lTpes:List[Type], kTpe:Type, rTpe:Type, modInd:Int) = mkHListTpe(lTpes.updated(modInd, newFld(kTpe, rTpe))) + + def add(lTpes:List[Type], kTpe:Type, vTpe:Type) = mkHListTpe(lTpes :+ newFld(kTpe, vTpe)) + + def remove(lTpes:List[Type], modInd:Int) = mkHListTpe(lTpes.patch(modInd, Nil, 1)) + } + /** * Type class supporting modification of a record field by given function. * @@ -209,52 +321,108 @@ package record { type Aux[L <: HList, F, A, B, Out0 <: HList] = Modifier[L, F, A, B] { type Out = Out0 } - implicit def hlistModify1[F, A, B, T <: HList]: Aux[FieldType[F, A] :: T, F, A, B, FieldType[F, B] :: T] = + implicit def macroModify[L<:HList, K, V, R, O<:HList]: Aux[L, K, V, R, O] = macro ModifierMacros.applyImpl[L, K, R] + + @deprecated("used for binary compatibility with 2.3.0","2.3.1") + def hlistModify1[F, A, B, T <: HList]: Aux[FieldType[F, A] :: T, F, A, B, FieldType[F, B] :: T] = new Modifier[FieldType[F, A] :: T, F, A, B] { type Out = FieldType[F, B] :: T def apply(l: FieldType[F, A] :: T, f: A => B): Out = field[F](f(l.head)) :: l.tail } - implicit def hlistModify[H, T <: HList, F, A, B] - (implicit mt: Modifier[T, F, A, B]): Aux[H :: T, F, A, B, H :: mt.Out] = - new Modifier[H :: T, F, A, B] { - type Out = H :: mt.Out - def apply(l: H :: T, f: A => B): Out = l.head :: mt(l.tail, f) - } + @deprecated("used for binary compatibility with 2.3.0","2.3.1") + def hlistModify[H, T <: HList, F, A, B] + (implicit mt: Modifier[T, F, A, B]): Aux[H :: T, F, A, B, H :: mt.Out] = + new Modifier[H :: T, F, A, B] { + type Out = H :: mt.Out + def apply(l: H :: T, f: A => B): Out = l.head :: mt(l.tail, f) + } } + @macrocompat.bundle + class ModifierMacros(val c: whitebox.Context) extends CrudMacros { + import c.universe._ + + def applyImpl[L <: HList, K, R](implicit lTag: WeakTypeTag[L], kTag: WeakTypeTag[K], vModTag: WeakTypeTag[R]): Tree = { + + val List(lTpe, kTpe, vModTpe) = List(lTag, kTag, vModTag).map(_.tpe.dealias) + + val lTpes = unpackHListTpe(lTpe) + + val(ind, vTpe) = getValue(kTpe, lTpes) + val mTpe = if(ind == -1)abort(s"$lTpe does not contain key $kTpe") + else replace(lTpes, kTpe, vModTpe, ind) + + q""" + new _root_.shapeless.ops.record.Modifier[$lTpe, $kTpe, $vTpe, $vModTpe]{ + type Out = HList + def apply(l: $lTpe, f: $vTpe => $vModTpe): HList = HList.unsafeCrud(l, $ind, f.asInstanceOf[Any => Any], false)._2 + }.asInstanceOf[_root_.shapeless.ops.record.Modifier.Aux[$lTpe, $kTpe, $vTpe, $vModTpe, $mTpe]] + """ + } + } + + /** * Type class supporting record field removal. * * @author Miles Sabin */ + @annotation.implicitNotFound(msg = "No field ${K} in record ${L}") trait Remover[L <: HList, K] extends DepFn1[L] with Serializable trait LowPriorityRemover { type Aux[L <: HList, K, Out0] = Remover[L, K] { type Out = Out0 } - implicit def hlistRemove[H, T <: HList, K, V, OutT <: HList] - (implicit rt: Aux[T, K, (V, OutT)]): Aux[H :: T, K, (V, H :: OutT)] = - new Remover[H :: T, K] { - type Out = (V, H :: OutT) - def apply(l : H :: T): Out = { - val (v, tail) = rt(l.tail) - (v, l.head :: tail) - } + @deprecated("used for binary compatibility with 2.3.0","2.3.1") + def hlistRemove[H, T <: HList, K, V, OutT <: HList] + (implicit rt: Aux[T, K, (V, OutT)]): Aux[H :: T, K, (V, H :: OutT)] = + new Remover[H :: T, K] { + type Out = (V, H :: OutT) + def apply(l : H :: T): Out = { + val (v, tail) = rt(l.tail) + (v, l.head :: tail) } + } } object Remover extends LowPriorityRemover { def apply[L <: HList, K](implicit remover: Remover[L, K]): Aux[L, K, remover.Out] = remover - implicit def hlistRemove1[K, V, T <: HList]: Aux[FieldType[K, V] :: T, K, (V, T)] = + implicit def macroRemove[L<:HList,K, V, O <: HList]:Aux[L,K,(V,O)] = macro RemoverMacros.applyImpl[L,K] + + @deprecated("used for binary compatibility with 2.3.0","2.3.1") + def hlistRemove1[K, V, T <: HList]: Aux[FieldType[K, V] :: T, K, (V, T)] = new Remover[FieldType[K, V] :: T, K] { type Out = (V, T) def apply(l: FieldType[K, V] :: T): Out = (l.head, l.tail) } } + @macrocompat.bundle + class RemoverMacros(val c: whitebox.Context) extends CrudMacros { + 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 + val lTpes = unpackHListTpe(lTpe) + + val(ind, vTpe) = getValue(kTpe, lTpes) + + val rTpe = if(ind == -1) abort(s"No field $kTpe in record type $lTpe") + else remove(lTpes, ind) + + q""" + new _root_.shapeless.ops.record.Remover[$lTpe, $kTpe]{ + type Out = Any + def apply(l: $lTpe): Any = HList.unsafeCrud(l, $ind, v => v, true) + }.asInstanceOf[_root_.shapeless.ops.record.Remover.Aux[$lTpe, $kTpe, ($vTpe, $rTpe)]] + """ + } + } + /** * Type class supporting removal and re-insertion of an element (possibly unlabelled). * diff --git a/core/src/main/scala/shapeless/syntax/records.scala b/core/src/main/scala/shapeless/syntax/records.scala index f180be19c..938355671 100644 --- a/core/src/main/scala/shapeless/syntax/records.scala +++ b/core/src/main/scala/shapeless/syntax/records.scala @@ -56,6 +56,15 @@ final class RecordOps[L <: HList](val l : L) extends AnyVal with Serializable { */ def updated[V](k: Witness, v: V)(implicit updater: Updater[L, FieldType[k.T, V]]) : updater.Out = updater(l, field[k.T](v)) + /* + * Replaces the value of a field with key type F preserving the same value type. + */ + def replace[V](k: Witness, v: V)(implicit replacer: Replacer.Aux[L, k.T, V, L]) : L = replacer(l, v) + + /** + * Adds a field only if record does not contain the given key. + */ + def add[V](k: Witness, v: V)(implicit adder: Adder[L, k.T, V]) : adder.Out = adder(l, v) /** * Updates a field having a value with type A by given function. diff --git a/core/src/test/scala/shapeless/records.scala b/core/src/test/scala/shapeless/records.scala index a69554f13..952d6c94d 100644 --- a/core/src/test/scala/shapeless/records.scala +++ b/core/src/test/scala/shapeless/records.scala @@ -478,6 +478,19 @@ class RecordTests { assertEquals(("intField1" ->> 23) :: ("stringField1" ->> "foo") :: ("boolField1" ->> true) :: HNil, r5) } + @Test + def addReplaceTest { + val a = Record(a = 1, b = "2") + //covering only replace and add scenarios because others are covered by existing Selector, Remover, Modifier tests + assertEquals(a.replace('a, 2), Record(a = 2, b = "2")) + assertEquals(a.add('z, Nil), Record(a =1, b= "2", z = Nil)) + illTyped( + """ + a.add('a, Nil) + a.replace('a, Nil) + """) + } + @Test def testRemoveAll { type R = Record.`'i -> Int, 's -> String, 'c -> Char, 'j -> Int`.T