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 option to use underscore symbol _ instead of * to define anonymous type lambdas #188

Merged
merged 5 commits into from
May 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ jobs:
matrix:
os: [ubuntu-latest]
scala:
- 2.10.7
- 2.11.12
- 2.12.8
- 2.12.9
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,37 @@ lambda is only used in the body once, and in the same order. For more
complex type lambda expressions, you will need to use the function
syntax.

#### Inline Underscore Syntax

Since version `0.13.0` kind-projector adds an option to use underscore symbol `_` instead of `*` to define anonymous type lambdas.
The syntax roughly follows the [proposed new syntax for wildcards and placeholders](https://dotty.epfl.ch/docs/reference/changed-features/wildcards.html#migration-strategy) for Scala 3.2+ and is designed to allow cross-compilation of libraries between Scala 2 and Scala 3 while using the new Scala 3 syntax for both versions.

To enable this mode, add `-P:kind-projector:underscore-placeholders` to your scalac command-line. In sbt you may do this as follows:

```scala
ThisBuild / scalacOptions += "-P:kind-projector:underscore-placeholders"
```

This mode is designed to be used with scalac versions `2.12.14`+ and `2.13.6`+, these versions add an the ability to use `?` as the existential type wildcard ([scala/scala#9560](https://github.com/scala/scala/pull/9560)), allowing to repurpose the underscore without losing the ability to write existential types. It is not advised that you use this mode with older versions of scalac or without `-Xsource:3` flag, since you will lose the underscore syntax entirely.

Here are a few examples:

```scala
Tuple2[_, Double] // equivalent to: type R[A] = Tuple2[A, Double]
Either[Int, +_] // equivalent to: type R[+A] = Either[Int, A]
Function2[-_, Long, +_] // equivalent to: type R[-A, +B] = Function2[A, Long, B]
EitherT[_[_], Int, _] // equivalent to: type R[F[_], B] = EitherT[F, Int, B]
```

Examples with `-Xsource:3`'s `?`-wildcard:

```scala
Tuple2[_, ?] // equivalent to: type R[A] = Tuple2[A, x] forSome { type x }
Either[?, +_] // equivalent to: type R[+A] = Either[x, A] forSome { type x }
Function2[-_, ?, +_] // equivalent to: type R[-A, +B] = Function2[A, x, B] forSome { type x }
EitherT[_[_], ?, _] // equivalent to: type R[F[_], B] = EitherT[F, x, B] forSome { type x }
```

### Function Syntax

The more powerful syntax to use is the function syntax. This syntax
Expand Down
14 changes: 3 additions & 11 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ inThisBuild {
resolvers in Global += "scala-integration" at "https://scala-ci.typesafe.com/artifactory/scala-integration/",
githubWorkflowPublishTargetBranches := Seq(),
crossScalaVersions := Seq(
"2.10.7",
"2.11.12",
"2.12.8",
"2.12.9",
Expand All @@ -18,7 +17,7 @@ inThisBuild {
"2.13.2",
"2.13.3",
"2.13.4",
"2.13.5"
"2.13.5",
),
organization := "org.typelevel",
licenses += ("MIT", url("http://opensource.org/licenses/MIT")),
Expand Down Expand Up @@ -63,7 +62,7 @@ inThisBuild {

val HasScalaVersion = {
object Matcher {
def unapply(versionString: String) =
def unapply(versionString: String) =
versionString.takeWhile(ch => ch != '-').split('.').toList.map(str => scala.util.Try(str.toInt).toOption) match {
case List(Some(epoch), Some(major), Some(minor)) => Some((epoch, major, minor))
case _ => None
Expand Down Expand Up @@ -105,13 +104,6 @@ lazy val `kind-projector` = project
}
},
libraryDependencies += scalaOrganization.value % "scala-compiler" % scalaVersion.value,
libraryDependencies ++= (scalaBinaryVersion.value match {
case "2.10" => List(
compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full),
"org.scalamacros" %% "quasiquotes" % "2.1.1"
)
case _ => Nil
}),
scalacOptions ++= Seq(
"-Xlint",
"-feature",
Expand All @@ -127,7 +119,7 @@ lazy val `kind-projector` = project
Test / scalacOptions ++= (scalaVersion.value match {
case HasScalaVersion(2, 13, n) if n >= 2 => List("-Wconf:src=WarningSuppression.scala:error")
case _ => Nil
}),
}) ++ List("-P:kind-projector:underscore-placeholders"),
console / initialCommands := "import d_m._",
Compile / console / scalacOptions := Seq("-language:_", "-Xplugin:" + (Compile / packageBin).value),
Test / console / scalacOptions := (Compile / console / scalacOptions).value,
Expand Down
52 changes: 50 additions & 2 deletions src/main/scala/KindProjector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ import scala.collection.mutable
class KindProjector(val global: Global) extends Plugin {
val name = "kind-projector"
val description = "Expand type lambda syntax"

override val optionsHelp = Some(Seq(
"-P:kind-projector:underscore-placeholders - treat underscore as a type lambda placeholder,",
"disables Scala 2 wildcards, you must separately enable `-Xsource:3` option to be able to",
"write wildcards using `?` symbol").mkString(" "))


override def init(options: List[String], error: String => Unit): Boolean = {
if (options.exists(_ != "underscore-placeholders")) {
error(s"Error: $name takes no options except `-P:kind-projector:underscore-placeholders`, but got ${options.mkString(",")}")
}
true
}

val components = new KindRewriter(this, global) :: Nil
}

Expand All @@ -32,6 +46,10 @@ class KindRewriter(plugin: Plugin, val global: Global)
lazy val useAsciiNames: Boolean =
System.getProperty("kp:genAsciiNames") == "true"

lazy val useUnderscoresForTypeLambda: Boolean = {
plugin.options.contains("underscore-placeholders")
}

def newTransformer(unit: CompilationUnit): MyTransformer =
new MyTransformer(unit)

Expand All @@ -54,15 +72,28 @@ class KindRewriter(plugin: Plugin, val global: Global)
val InvPlaceholder = newTypeName("$times")
val CoPlaceholder = newTypeName("$plus$times")
val ContraPlaceholder = newTypeName("$minus$times")

val TermLambda1 = TypeLambda1.toTermName
val TermLambda2 = TypeLambda2.toTermName

object InvPlaceholderScala3 {
def apply(n: Name): Boolean = n match { case InvPlaceholderScala3() => true; case _ => false }
def unapply(t: TypeName): Boolean = t.startsWith("_$") && t.drop(2).decoded.forall(_.isDigit)
}
val CoPlaceholderScala3 = newTypeName("$plus_")
val ContraPlaceholderScala3 = newTypeName("$minus_")

object Placeholder {
def unapply(name: TypeName): Option[Variance] = name match {
case InvPlaceholder => Some(Invariant)
case CoPlaceholder => Some(Covariant)
case ContraPlaceholder => Some(Contravariant)
case _ if useUnderscoresForTypeLambda => name match {
case InvPlaceholderScala3() => Some(Invariant)
case CoPlaceholderScala3 => Some(Covariant)
case ContraPlaceholderScala3 => Some(Contravariant)
case _ => None
}
case _ => None
}
}
Expand Down Expand Up @@ -248,9 +279,20 @@ class KindRewriter(plugin: Plugin, val global: Global)
case (ExistentialTypeTree(AppliedTypeTree(Ident(Placeholder(variance)), ps), _), i) =>
(Ident(newParamName(i)), Some(Left((variance, ps.map(makeComplexTypeParam)))))
case (a, i) =>
(super.transform(a), None)
// Using super.transform in existential type case in underscore mode
// skips the outer `ExistentialTypeTree` (reproduces in nested.scala test)
// and produces invalid trees where the unused underscore variables are not cleaned up
// by the current transformer
// I do not know why! Using `this.transform` instead works around the issue,
// however it seems to have worked correctly all this time non-underscore mode, so
// we keep calling super.transform to not change anything for existing code in classic mode.
val transformedA =
if (useUnderscoresForTypeLambda) this.transform(a)
else super.transform(a)
(transformedA, None)
}


// for each placeholder, create a type parameter
val innerTypes = xyz.collect {
case (Ident(name), Some(Right(variance))) =>
Expand Down Expand Up @@ -331,6 +373,12 @@ class KindRewriter(plugin: Plugin, val global: Global)
case AppliedTypeTree(Ident(TypeLambda2), AppliedTypeTree(target, a :: as) :: Nil) =>
validateLambda(tree.pos, target, a, as)

// Either[_, Int] case (if `underscore-placeholders` is enabled)
case ExistentialTypeTree(AppliedTypeTree(t, as), params) if useUnderscoresForTypeLambda =>
val nonUnderscoreExistentials = params.filter(p => !InvPlaceholderScala3(p.name))
val nt = atPos(tree.pos.makeTransparent)(handlePlaceholders(t, as))
if (nonUnderscoreExistentials.isEmpty) nt else ExistentialTypeTree(nt, nonUnderscoreExistentials)

// Either[?, Int] case (if no ? present this is a noop)
case AppliedTypeTree(t, as) =>
atPos(tree.pos.makeTransparent)(handlePlaceholders(t, as))
Expand Down
1 change: 1 addition & 0 deletions src/test/scala/issue80.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ object Coproduct {
case Right(bx) => Coproduct(Right(g(bx)))
}
}
def test[X[_]]: Bifunctor[({ type L[F[_[_]], G[_[_]]] = Coproduct[F, G, X] })#L] = coproductBifunctor[X]
}
13 changes: 13 additions & 0 deletions src/test/scala/underscores/functor.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package underscores

trait Functor[M[_]] {
def fmap[A, B](fa: M[A])(f: A => B): M[B]
}

class EitherRightFunctor[L] extends Functor[Either[L, _]] {
def fmap[A, B](fa: Either[L, A])(f: A => B): Either[L, B] =
fa match {
case Right(a) => Right(f(a))
case Left(l) => Left(l)
}
}
23 changes: 23 additions & 0 deletions src/test/scala/underscores/issue80.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package underscores

trait ~~>[A[_[_]], B[_[_]]] {
def apply[X[_]](a: A[X]): B[X]
}

trait Bifunctor[F[_[_[_]], _[_[_]]]] {
def bimap[A[_[_]], B[_[_]], C[_[_]], D[_[_]]](fab: F[A, B])(f: A ~~> C, g: B ~~> D): F[C, D]
}

final case class Coproduct[A[_[_]], B[_[_]], X[_]](run: Either[A[X], B[X]])

object Coproduct {
def coproductBifunctor[X[_]]: Bifunctor[Coproduct[_[_[_]], _[_[_]], X]] =
new Bifunctor[Coproduct[_[_[_]], _[_[_]], X]] {
def bimap[A[_[_]], B[_[_]], C[_[_]], D[_[_]]](abx: Coproduct[A, B, X])(f: A ~~> C, g: B ~~> D): Coproduct[C, D, X] =
abx.run match {
case Left(ax) => Coproduct(Left(f(ax)))
case Right(bx) => Coproduct(Right(g(bx)))
}
}
def test[X[_]]: Bifunctor[({ type L[F[_[_]], G[_[_]]] = Coproduct[F, G, X] })#L] = coproductBifunctor[X]
}
13 changes: 13 additions & 0 deletions src/test/scala/underscores/nested.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package underscores

// // From https://github.com/non/kind-projector/issues/20
// import scala.language.higherKinds

object KindProjectorWarnings {
trait Foo[F[_], A]
trait Bar[A, B]

def f[G[_]]: Unit = ()

f[Foo[Bar[Int, _], _]] // shadowing warning
}
67 changes: 67 additions & 0 deletions src/test/scala/underscores/polylambda.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package underscores

trait ~>[-F[_], +G[_]] {
def apply[A](x: F[A]): G[A]
}
trait ~>>[-F[_], +G[_]] {
def dingo[B](x: F[B]): G[B]
}
final case class Const[A, B](getConst: A)

class PolyLambdas {
type ToSelf[F[_]] = F ~> F

val kf1 = Lambda[Option ~> Vector](_.iterator.toVector)

val kf2 = λ[Vector ~> Option] {
case Vector(x) => Some(x)
case _ => None
}

val kf3 = λ[ToSelf[Vector]](_.reverse)

val kf4 = λ[Option ~>> Option].dingo(_ flatMap (_ => None))

val kf5 = λ[Map[_, Int] ~> Map[_, Long]](_.map { case (k, v) => (k, v.toLong) }.toMap)

val kf6 = λ[ToSelf[Map[_, Int]]](_.map { case (k, v) => (k, v * 2) }.toMap)

implicit class FGOps[F[_], A](x: F[A]) {
def ntMap[G[_]](kf: F ~> G): G[A] = kf(x)
}

// Scala won't infer the unary type constructor alias from a
// tuple. I'm not sure how it even could, so we'll let it slide.
type PairWithInt[A] = (A, Int)
def mkPair[A](x: A, y: Int): PairWithInt[A] = x -> y
val pairMap = λ[ToSelf[PairWithInt]] { case (k, v) => (k, v * 2) }
val tupleTakeFirst = λ[λ[A => (A, Int)] ~> List](x => List(x._1))

// All these formulations should be equivalent.
def const1[A] = λ[ToSelf[Const[A, _]]](x => x)
def const2[A] : ToSelf[Const[A, _]] = λ[Const[A, _] ~> Const[A, _]](x => x)
def const3[A] : Const[A, _] ~> Const[A, _] = λ[ToSelf[Const[A, _]]](x => x)
def const4[A] = λ[Const[A, _] ~> Const[A, _]](x => x)
def const5[A] : ToSelf[Const[A, _]] = λ[ToSelf[λ[B => Const[A, B]]]](x => x)
def const6[A] : Const[A, _] ~> Const[A, _] = λ[ToSelf[λ[B => Const[A, B]]]](x => x)

@org.junit.Test
def polylambda(): Unit = {
assert(kf1(None) == Vector())
assert(kf1(Some("a")) == Vector("a"))
assert(kf1(Some(5d)) == Vector(5d))
assert(kf2(Vector(5)) == Some(5))
assert(kf3(Vector(1, 2)) == Vector(2, 1))
assert(kf4.dingo(Some(5)) == None)
assert(kf5(Map("a" -> 5)) == Map("a" -> 5))
assert(kf6(Map("a" -> 5)) == Map("a" -> 10))

assert((mkPair("a", 1) ntMap pairMap) == ("a" -> 2))
assert((mkPair(Some(true), 1) ntMap pairMap) == (Some(true) -> 2))

assert(mkPair('a', 1).ntMap(tupleTakeFirst) == List('a'))
// flatten works, whereas it would be a static error in the
// line above. That's pretty poly!
assert(mkPair(Some(true), 1).ntMap(tupleTakeFirst).flatten == List(true))
}
}
Loading