Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 🌟 Scala 3 🌟 support #5

Merged
merged 10 commits into from
Jul 28, 2024
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.13"
val Scala3 = "3.3.3"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it's the most controversial thing. The recent version of Scala 3 is 3.4.2. But since Scala 3 doesn't keep forward compatibility between minor versions, we should barely stick with it. Also, Scala 3.3 is an LTS — https://virtuslab.com/blog/technology/scala-3-roadmap-for-2024/#at-least-one-lts-patch-for-every-two-next-patch-releases. Last but not least, many libraries I used to contribute have decided to stick with 3.3. I propose to act similarly.


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
10 changes: 10 additions & 0 deletions shared/src/main/scala-2.12/zipper/ForImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ import scala.collection.generic.CanBuildFrom
import scala.language.higherKinds

private[zipper] trait 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)
}
danicheg marked this conversation as resolved.
Show resolved Hide resolved

class For[A, Coll[X] <: Seq[X]] {
/** Derive an instance of `Unzip[A]` */
def derive[L <: HList](
Expand Down
10 changes: 10 additions & 0 deletions shared/src/main/scala-2.13/zipper/ForImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ import shapeless.ops.hlist.{Replacer, Selector}
import scala.collection.Factory

private[zipper] trait 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)
}

class For[A, Coll[X] <: Seq[X]] {
/** Derive an instance of `Unzip[A]` */
def derive[L <: HList](
Expand Down
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]
}
28 changes: 28 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,28 @@
package contrib.shapeless3

trait Replacer[L <: Tuple, U, V]:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a matter of fact, the full-fledged migration of shapeless-2 to Scala 3 is currently in progress (milessabin/shapeless#1200 — it's such a monumental work, I'm just shocked). So we need to migrate Replacer and Selector ourselves for a while.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But yeah, I think some clarifications should be put into the comments. I'll go over it tomorrow.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the native derivation capabilities of Scala 3 are sufficiently powerful to eschew shapeless altogether?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can imagine a solution powered by a Macro. However, it should also work with Scala 2 Macros. But I don't know if Scala 3 has anything out of pocket for replacing shapeless.Generic.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant this: https://docs.scala-lang.org/scala3/reference/contextual/derivation.html#how-to-write-a-type-class-derived-method-using-low-level-mechanisms-1. But after a closer look it does not seem powerful enough (yet?). And I don’t like that using it seems to require writing a lot of asInstanceOf code.

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)
}
}
13 changes: 13 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,13 @@
package contrib.shapeless3

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