Skip to content

Commit

Permalink
Add 🌟 Scala 3 🌟 support (#5)
Browse files Browse the repository at this point in the history
* Add Scala 3 to the build
* Migrate Selector from shapeless-2 to Scala 3
* Migrate Replacer from shapeless-2 to Scala 3
* Migrate ForImpl
* Fix UnzipDerivationSpec for Scala 3
* Apply DRY to ForImpl for Scala 2 sources
* Add some notes for Replacer and Selector
* Add rectification on the site about available Scala versions
  • Loading branch information
danicheg authored Jul 28, 2024
1 parent 68b88a2 commit c86cc07
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 28 deletions.
35 changes: 24 additions & 11 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down Expand Up @@ -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
)
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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](
Expand Down
16 changes: 16 additions & 0 deletions shared/src/main/scala-2/zipper/ForImpl.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
43 changes: 43 additions & 0 deletions shared/src/main/scala-3/zipper/ForImpl.scala
Original file line number Diff line number Diff line change
@@ -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]
}
33 changes: 33 additions & 0 deletions shared/src/main/scala-3/zipper/contrib/shapeless3/Replacer.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
18 changes: 18 additions & 0 deletions shared/src/main/scala-3/zipper/contrib/shapeless3/Selector.scala
Original file line number Diff line number Diff line change
@@ -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)
}
14 changes: 1 addition & 13 deletions shared/src/main/scala/zipper/GenericUnzipInstances.scala
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion shared/src/test/scala/zipper/UnzipDerivationSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit c86cc07

Please sign in to comment.