Skip to content

Commit

Permalink
Add option to use underscore symbol _ instead of * to define anon…
Browse files Browse the repository at this point in the history
…ymous 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](scala/scala#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 }
```
  • Loading branch information
neko-kai committed May 7, 2021
1 parent 7ad46d6 commit adbd336
Show file tree
Hide file tree
Showing 12 changed files with 367 additions and 7 deletions.
31 changes: 31 additions & 0 deletions README.md
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
12 changes: 9 additions & 3 deletions build.sbt
Expand Up @@ -18,7 +18,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 +63,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 All @@ -84,6 +84,11 @@ def hasNewParser(versionString: String) = versionString match {
case _ => false
}

def hasNewPlugin(versionString: String) = versionString match {
case HasScalaVersion(2, 10, _) => false
case _ => true
}

lazy val `kind-projector` = project
.in(file("."))
.settings(
Expand All @@ -100,6 +105,7 @@ lazy val `kind-projector` = project
val suffices =
(if (hasNewParser(sv)) "-newParser" else "-oldParser") ::
(if (hasNewReporting(sv)) "-newReporting" else "-oldReporting") ::
(if (hasNewPlugin(sv)) "-newPlugin" else "-oldPlugin") ::
Nil
suffices.map(suffix => file(dir.getPath + suffix))
}
Expand Down Expand Up @@ -127,7 +133,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
10 changes: 10 additions & 0 deletions src/main/scala-newPlugin/PluginOptionsCompat.scala
@@ -0,0 +1,10 @@
package d_m

import scala.tools.nsc.plugins.Plugin

trait PluginOptionsCompat {
def pluginOptions(plugin: Plugin) = plugin.options
}

//compatibility stub
trait PluginCompat
16 changes: 16 additions & 0 deletions src/main/scala-oldPlugin/PluginOptionsCompat.scala
@@ -0,0 +1,16 @@
package d_m

import scala.tools.nsc.plugins.Plugin

trait PluginOptionsCompat {
def pluginOptions(plugin: Plugin) = plugin.asInstanceOf[PluginCompat].options
}

trait PluginCompat extends Plugin {
var options: List[String] = _
override def processOptions(options: List[String], error: String => Unit): Unit = {
this.options = options
init(options, error)
}
def init(options: List[String], error: String => Unit): Boolean
}
59 changes: 55 additions & 4 deletions src/main/scala/KindProjector.scala
Expand Up @@ -12,14 +12,28 @@ import nsc.ast.TreeDSL
import scala.reflect.NameTransformer
import scala.collection.mutable

class KindProjector(val global: Global) extends Plugin {
class KindProjector(val global: Global) extends Plugin with PluginCompat {
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
}

class KindRewriter(plugin: Plugin, val global: Global)
extends PluginComponent with Transform with TypingTransformers with TreeDSL with ReportingCompat {
extends PluginComponent with Transform with TypingTransformers with TreeDSL with PluginOptionsCompat with ReportingCompat {

import global._

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 = {
pluginOptions(plugin).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,15 @@ 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.filterNot {
case p: MemberDef => InvPlaceholderScala3(p.name)
case _ => true
}
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
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
@@ -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
@@ -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
@@ -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
@@ -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))
}
}

0 comments on commit adbd336

Please sign in to comment.