diff --git a/build.sbt b/build.sbt index ee40f76..b03d1da 100644 --- a/build.sbt +++ b/build.sbt @@ -1,24 +1,30 @@ -val commonScalacOptions = - Seq( - "-feature", - "-deprecation", - "-Xlint", - "-Xfatal-warnings" - ) - val Scala212 = "2.12.19" val Scala213 = "2.13.14" +val Scala3 = "3.3.3" val commonSettings = Seq( scalaVersion := Scala213, - crossScalaVersions := Seq(Scala212, Scala213), + crossScalaVersions := Seq(Scala212, Scala213, Scala3), scalacOptions ++= { + val commonScalacOptions = + Seq( + "-feature", + "-deprecation", + "-Xfatal-warnings" + ) + val scala2Options = + Seq( + "-Xlint" + ) + scalaVersion.value match { case v if v.startsWith("2.12") => Seq( "-Ypartial-unification", "-Ywarn-unused-import" - ) ++ commonScalacOptions + ) ++ commonScalacOptions ++ scala2Options + case v if v.startsWith("2.13") => + commonScalacOptions ++ scala2Options case _ => commonScalacOptions } @@ -60,8 +66,15 @@ lazy val zipper = crossProject(JSPlatform, JVMPlatform).in(file(".")) .settings(commonSettings) .settings( name := "zipper", + libraryDependencies += { + scalaVersion.value match { + case v if v.startsWith("2") => + "com.chuusai" %%% "shapeless" % "2.3.10" + case _ => + "org.typelevel" %% "shapeless3-deriving" % "3.4.1" + } + }, libraryDependencies ++= Seq( - "com.chuusai" %%% "shapeless" % "2.3.10", "org.scalatest" %%% "scalatest-flatspec" % "3.2.18" % Test, "org.scalatest" %%% "scalatest-shouldmatchers" % "3.2.18" % Test ) diff --git a/docs/index.md b/docs/index.md index 69b19c1..9746d27 100644 --- a/docs/index.md +++ b/docs/index.md @@ -76,7 +76,7 @@ the unchanged parts are shared: ### Usage -Include these lines in your `build.sbt`: +`zipper` is available for Scala 2.12, 2.13 and 3.3+. Include these lines in your `build.sbt`: ```scala // for JVM diff --git a/shared/src/main/scala-2.12/zipper/ForImpl.scala b/shared/src/main/scala-2.12/zipper/ForImplScalaVersionSpecific.scala similarity index 95% rename from shared/src/main/scala-2.12/zipper/ForImpl.scala rename to shared/src/main/scala-2.12/zipper/ForImplScalaVersionSpecific.scala index 7739a31..56aae41 100644 --- a/shared/src/main/scala-2.12/zipper/ForImpl.scala +++ b/shared/src/main/scala-2.12/zipper/ForImplScalaVersionSpecific.scala @@ -6,7 +6,7 @@ import shapeless.ops.hlist.{Selector, Replacer} import scala.collection.generic.CanBuildFrom import scala.language.higherKinds -private[zipper] trait ForImpl { +private[zipper] trait ForImplScalaVersionSpecific { class For[A, Coll[X] <: Seq[X]] { /** Derive an instance of `Unzip[A]` */ def derive[L <: HList]( diff --git a/shared/src/main/scala-2.13/zipper/ForImpl.scala b/shared/src/main/scala-2.13/zipper/ForImplScalaVersionSpecific.scala similarity index 94% rename from shared/src/main/scala-2.13/zipper/ForImpl.scala rename to shared/src/main/scala-2.13/zipper/ForImplScalaVersionSpecific.scala index 00667af..05054ca 100644 --- a/shared/src/main/scala-2.13/zipper/ForImpl.scala +++ b/shared/src/main/scala-2.13/zipper/ForImplScalaVersionSpecific.scala @@ -5,7 +5,7 @@ import shapeless.ops.hlist.{Replacer, Selector} import scala.collection.Factory -private[zipper] trait ForImpl { +private[zipper] trait ForImplScalaVersionSpecific { class For[A, Coll[X] <: Seq[X]] { /** Derive an instance of `Unzip[A]` */ def derive[L <: HList]( diff --git a/shared/src/main/scala-2/zipper/ForImpl.scala b/shared/src/main/scala-2/zipper/ForImpl.scala new file mode 100644 index 0000000..325ece7 --- /dev/null +++ b/shared/src/main/scala-2/zipper/ForImpl.scala @@ -0,0 +1,16 @@ +package zipper + +import shapeless.{Generic, HList} +import shapeless.ops.hlist.{Replacer, Selector} + +private[zipper] trait ForImpl extends ForImplScalaVersionSpecific { + implicit def `Unzip List-based`[A, L <: HList]( + implicit generic: Generic.Aux[A, L], + select: Selector[L, List[A]], + replace: Replacer.Aux[L, List[A], List[A], (List[A], L)] + ): Unzip[A] = new Unzip[A] { + def unzip(node: A): List[A] = select(generic.to(node)) + + def zip(node: A, children: List[A]): A = generic.from(replace(generic.to(node), children)._2) + } +} diff --git a/shared/src/main/scala-3/zipper/ForImpl.scala b/shared/src/main/scala-3/zipper/ForImpl.scala new file mode 100644 index 0000000..9acfda3 --- /dev/null +++ b/shared/src/main/scala-3/zipper/ForImpl.scala @@ -0,0 +1,43 @@ +package zipper + +import contrib.shapeless3.{Replacer, Selector} +import shapeless3.deriving.K0 +import shapeless3.deriving.K0.* + +import scala.collection.Factory + +private[zipper] trait ForImpl { + given unzipListBased[A, L <: Tuple](using + generic: K0.ProductGeneric[A] { type MirroredElemTypes = L }, + selector: Selector[L, List[A]], + replacer: Replacer.Aux[L, List[A], List[A], (List[A], L)] + ): Unzip[A] with { + def unzip(node: A): List[A] = selector(generic.toRepr(node)) + def zip(node: A, children: List[A]): A = { + val repr = replacer(generic.toRepr(node), children) + generic.fromRepr(repr._2) + } + } + + class For[A, Coll[X] <: Seq[X]]: + /** Derive an instance of `Unzip[A]` */ + inline given derive[L <: Tuple](using + generic: K0.ProductGeneric[A] { type MirroredElemTypes = L }, + selector: Selector[L, Coll[A]], + replacer: Replacer.Aux[L, Coll[A], Coll[A], (Coll[A], L)], + factory: Factory[A, Coll[A]] + ): Unzip[A] with { + def unzip(node: A): List[A] = selector(generic.toRepr(node)).toList + def zip(node: A, children: List[A]): A = { + val repr = replacer(generic.toRepr(node), children.to(factory)) + generic.fromRepr(repr._2) + } + } + + object For: + /** + * @tparam A The type of the tree-like data structure + * @tparam Coll The type of the collection used for recursion (e.g. Vector) + */ + def apply[A, Coll[X] <: Seq[X]]: For[A, Coll] = new For[A, Coll] +} diff --git a/shared/src/main/scala-3/zipper/contrib/shapeless3/Replacer.scala b/shared/src/main/scala-3/zipper/contrib/shapeless3/Replacer.scala new file mode 100644 index 0000000..8c42f5e --- /dev/null +++ b/shared/src/main/scala-3/zipper/contrib/shapeless3/Replacer.scala @@ -0,0 +1,33 @@ +package contrib.shapeless3 + +/** + * This is ported from [[shapeless.ops.hlist.Replacer Replacer]] from shapeless-2. + * At the moment of implementation, there is no direct support in shapeless-3. + * We should give up on it once it arrives in the library. + */ +trait Replacer[L <: Tuple, U, V]: + type Out <: Tuple + def apply(t: L, v: V): Out + +object Replacer: + def apply[L <: Tuple, U, V](using r: Replacer[L, U, V]): Aux[L, U, V, r.Out] = r + + type Aux[L <: Tuple, U, V, Out0] = Replacer[L, U, V] { type Out = Out0 } + + given tupleReplacer1[T <: Tuple, U, V]: Aux[U *: T, U, V, (U, V *: T)] = + new Replacer[U *: T, U, V] { + type Out = (U, V *: T) + + def apply(l: U *: T, v: V): Out = (l.head, v *: l.tail) + } + + given tupleReplacer2[H, T <: Tuple, U, V, OutT <: Tuple](using + ut: Aux[T, U, V, (U, OutT)]): Aux[H *: T, U, V, (U, H *: OutT)] = + new Replacer[H *: T, U, V] { + type Out = (U, H *: OutT) + + def apply(l: H *: T, v: V): Out = { + val (u, outT) = ut(l.tail, v) + (u, l.head *: outT) + } + } diff --git a/shared/src/main/scala-3/zipper/contrib/shapeless3/Selector.scala b/shared/src/main/scala-3/zipper/contrib/shapeless3/Selector.scala new file mode 100644 index 0000000..b161fee --- /dev/null +++ b/shared/src/main/scala-3/zipper/contrib/shapeless3/Selector.scala @@ -0,0 +1,18 @@ +package contrib.shapeless3 + +/** + * This is ported from [[shapeless.ops.hlist.Selector Selector]] from shapeless-2. + * At the moment of implementation, there is no direct support in shapeless-3. + * We should give up on it once it arrives in the library. + */ +trait Selector[L <: Tuple, U]: + def apply(t: L): U + +object Selector: + given[H, T <: Tuple]: Selector[H *: T, H] with { + def apply(t: H *: T): H = t.head + } + + given[H, T <: Tuple, U] (using s: Selector[T, U]): Selector[H *: T, U] with { + def apply(t: H *: T): U = s (t.tail) +} diff --git a/shared/src/main/scala/zipper/GenericUnzipInstances.scala b/shared/src/main/scala/zipper/GenericUnzipInstances.scala index 893ac06..2f51fc5 100644 --- a/shared/src/main/scala/zipper/GenericUnzipInstances.scala +++ b/shared/src/main/scala/zipper/GenericUnzipInstances.scala @@ -1,15 +1,3 @@ package zipper -import shapeless.{HList, Generic} -import shapeless.ops.hlist.{Selector, Replacer} - -private[zipper] trait GenericUnzipInstances extends ForImpl { - implicit def `Unzip List-based`[A, L <: HList]( - implicit generic: Generic.Aux[A, L], - select: Selector[L, List[A]], - replace: Replacer.Aux[L, List[A], List[A], (List[A], L)] - ): Unzip[A] = new Unzip[A] { - def unzip(node: A): List[A] = select(generic.to(node)) - def zip(node: A, children: List[A]): A = generic.from(replace(generic.to(node), children)._2) - } -} +private[zipper] trait GenericUnzipInstances extends ForImpl diff --git a/shared/src/test/scala/zipper/UnzipDerivationSpec.scala b/shared/src/test/scala/zipper/UnzipDerivationSpec.scala index 9ca27b4..ab77c9a 100644 --- a/shared/src/test/scala/zipper/UnzipDerivationSpec.scala +++ b/shared/src/test/scala/zipper/UnzipDerivationSpec.scala @@ -19,7 +19,7 @@ class UnzipDerivationSpec extends AnyFlatSpec with Matchers { val before = Tree(1, Vector(Tree(2))) val after = Tree(1, Vector(Tree(2), Tree(3))) - implicit val unzip = Unzip.For[Tree, Vector].derive + implicit val unzip: Unzip[Tree] = Unzip.For[Tree, Vector].derive Zipper(before).moveDownRight.insertRight(Tree(3)).commit shouldEqual after }