Skip to content

Commit 2b84018

Browse files
authored
Add option to use underscore symbol _ instead of * to define anonymous type lambdas (#188)
1 parent 7ad46d6 commit 2b84018

File tree

11 files changed

+330
-14
lines changed

11 files changed

+330
-14
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ jobs:
2323
matrix:
2424
os: [ubuntu-latest]
2525
scala:
26-
- 2.10.7
2726
- 2.11.12
2827
- 2.12.8
2928
- 2.12.9

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,37 @@ lambda is only used in the body once, and in the same order. For more
131131
complex type lambda expressions, you will need to use the function
132132
syntax.
133133

134+
#### Inline Underscore Syntax
135+
136+
Since version `0.13.0` kind-projector adds an option to use underscore symbol `_` instead of `*` to define anonymous type lambdas.
137+
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.
138+
139+
To enable this mode, add `-P:kind-projector:underscore-placeholders` to your scalac command-line. In sbt you may do this as follows:
140+
141+
```scala
142+
ThisBuild / scalacOptions += "-P:kind-projector:underscore-placeholders"
143+
```
144+
145+
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.
146+
147+
Here are a few examples:
148+
149+
```scala
150+
Tuple2[_, Double] // equivalent to: type R[A] = Tuple2[A, Double]
151+
Either[Int, +_] // equivalent to: type R[+A] = Either[Int, A]
152+
Function2[-_, Long, +_] // equivalent to: type R[-A, +B] = Function2[A, Long, B]
153+
EitherT[_[_], Int, _] // equivalent to: type R[F[_], B] = EitherT[F, Int, B]
154+
```
155+
156+
Examples with `-Xsource:3`'s `?`-wildcard:
157+
158+
```scala
159+
Tuple2[_, ?] // equivalent to: type R[A] = Tuple2[A, x] forSome { type x }
160+
Either[?, +_] // equivalent to: type R[+A] = Either[x, A] forSome { type x }
161+
Function2[-_, ?, +_] // equivalent to: type R[-A, +B] = Function2[A, x, B] forSome { type x }
162+
EitherT[_[_], ?, _] // equivalent to: type R[F[_], B] = EitherT[F, x, B] forSome { type x }
163+
```
164+
134165
### Function Syntax
135166

136167
The more powerful syntax to use is the function syntax. This syntax

build.sbt

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ inThisBuild {
55
resolvers in Global += "scala-integration" at "https://scala-ci.typesafe.com/artifactory/scala-integration/",
66
githubWorkflowPublishTargetBranches := Seq(),
77
crossScalaVersions := Seq(
8-
"2.10.7",
98
"2.11.12",
109
"2.12.8",
1110
"2.12.9",
@@ -18,7 +17,7 @@ inThisBuild {
1817
"2.13.2",
1918
"2.13.3",
2019
"2.13.4",
21-
"2.13.5"
20+
"2.13.5",
2221
),
2322
organization := "org.typelevel",
2423
licenses += ("MIT", url("http://opensource.org/licenses/MIT")),
@@ -63,7 +62,7 @@ inThisBuild {
6362

6463
val HasScalaVersion = {
6564
object Matcher {
66-
def unapply(versionString: String) =
65+
def unapply(versionString: String) =
6766
versionString.takeWhile(ch => ch != '-').split('.').toList.map(str => scala.util.Try(str.toInt).toOption) match {
6867
case List(Some(epoch), Some(major), Some(minor)) => Some((epoch, major, minor))
6968
case _ => None
@@ -105,13 +104,6 @@ lazy val `kind-projector` = project
105104
}
106105
},
107106
libraryDependencies += scalaOrganization.value % "scala-compiler" % scalaVersion.value,
108-
libraryDependencies ++= (scalaBinaryVersion.value match {
109-
case "2.10" => List(
110-
compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full),
111-
"org.scalamacros" %% "quasiquotes" % "2.1.1"
112-
)
113-
case _ => Nil
114-
}),
115107
scalacOptions ++= Seq(
116108
"-Xlint",
117109
"-feature",
@@ -127,7 +119,7 @@ lazy val `kind-projector` = project
127119
Test / scalacOptions ++= (scalaVersion.value match {
128120
case HasScalaVersion(2, 13, n) if n >= 2 => List("-Wconf:src=WarningSuppression.scala:error")
129121
case _ => Nil
130-
}),
122+
}) ++ List("-P:kind-projector:underscore-placeholders"),
131123
console / initialCommands := "import d_m._",
132124
Compile / console / scalacOptions := Seq("-language:_", "-Xplugin:" + (Compile / packageBin).value),
133125
Test / console / scalacOptions := (Compile / console / scalacOptions).value,

src/main/scala/KindProjector.scala

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ import scala.collection.mutable
1515
class KindProjector(val global: Global) extends Plugin {
1616
val name = "kind-projector"
1717
val description = "Expand type lambda syntax"
18+
19+
override val optionsHelp = Some(Seq(
20+
"-P:kind-projector:underscore-placeholders - treat underscore as a type lambda placeholder,",
21+
"disables Scala 2 wildcards, you must separately enable `-Xsource:3` option to be able to",
22+
"write wildcards using `?` symbol").mkString(" "))
23+
24+
25+
override def init(options: List[String], error: String => Unit): Boolean = {
26+
if (options.exists(_ != "underscore-placeholders")) {
27+
error(s"Error: $name takes no options except `-P:kind-projector:underscore-placeholders`, but got ${options.mkString(",")}")
28+
}
29+
true
30+
}
31+
1832
val components = new KindRewriter(this, global) :: Nil
1933
}
2034

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

49+
lazy val useUnderscoresForTypeLambda: Boolean = {
50+
plugin.options.contains("underscore-placeholders")
51+
}
52+
3553
def newTransformer(unit: CompilationUnit): MyTransformer =
3654
new MyTransformer(unit)
3755

@@ -54,15 +72,28 @@ class KindRewriter(plugin: Plugin, val global: Global)
5472
val InvPlaceholder = newTypeName("$times")
5573
val CoPlaceholder = newTypeName("$plus$times")
5674
val ContraPlaceholder = newTypeName("$minus$times")
57-
75+
5876
val TermLambda1 = TypeLambda1.toTermName
5977
val TermLambda2 = TypeLambda2.toTermName
6078

79+
object InvPlaceholderScala3 {
80+
def apply(n: Name): Boolean = n match { case InvPlaceholderScala3() => true; case _ => false }
81+
def unapply(t: TypeName): Boolean = t.startsWith("_$") && t.drop(2).decoded.forall(_.isDigit)
82+
}
83+
val CoPlaceholderScala3 = newTypeName("$plus_")
84+
val ContraPlaceholderScala3 = newTypeName("$minus_")
85+
6186
object Placeholder {
6287
def unapply(name: TypeName): Option[Variance] = name match {
6388
case InvPlaceholder => Some(Invariant)
6489
case CoPlaceholder => Some(Covariant)
6590
case ContraPlaceholder => Some(Contravariant)
91+
case _ if useUnderscoresForTypeLambda => name match {
92+
case InvPlaceholderScala3() => Some(Invariant)
93+
case CoPlaceholderScala3 => Some(Covariant)
94+
case ContraPlaceholderScala3 => Some(Contravariant)
95+
case _ => None
96+
}
6697
case _ => None
6798
}
6899
}
@@ -248,9 +279,20 @@ class KindRewriter(plugin: Plugin, val global: Global)
248279
case (ExistentialTypeTree(AppliedTypeTree(Ident(Placeholder(variance)), ps), _), i) =>
249280
(Ident(newParamName(i)), Some(Left((variance, ps.map(makeComplexTypeParam)))))
250281
case (a, i) =>
251-
(super.transform(a), None)
282+
// Using super.transform in existential type case in underscore mode
283+
// skips the outer `ExistentialTypeTree` (reproduces in nested.scala test)
284+
// and produces invalid trees where the unused underscore variables are not cleaned up
285+
// by the current transformer
286+
// I do not know why! Using `this.transform` instead works around the issue,
287+
// however it seems to have worked correctly all this time non-underscore mode, so
288+
// we keep calling super.transform to not change anything for existing code in classic mode.
289+
val transformedA =
290+
if (useUnderscoresForTypeLambda) this.transform(a)
291+
else super.transform(a)
292+
(transformedA, None)
252293
}
253294

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

376+
// Either[_, Int] case (if `underscore-placeholders` is enabled)
377+
case ExistentialTypeTree(AppliedTypeTree(t, as), params) if useUnderscoresForTypeLambda =>
378+
val nonUnderscoreExistentials = params.filter(p => !InvPlaceholderScala3(p.name))
379+
val nt = atPos(tree.pos.makeTransparent)(handlePlaceholders(t, as))
380+
if (nonUnderscoreExistentials.isEmpty) nt else ExistentialTypeTree(nt, nonUnderscoreExistentials)
381+
334382
// Either[?, Int] case (if no ? present this is a noop)
335383
case AppliedTypeTree(t, as) =>
336384
atPos(tree.pos.makeTransparent)(handlePlaceholders(t, as))

src/test/scala/issue80.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ object Coproduct {
1717
case Right(bx) => Coproduct(Right(g(bx)))
1818
}
1919
}
20+
def test[X[_]]: Bifunctor[({ type L[F[_[_]], G[_[_]]] = Coproduct[F, G, X] })#L] = coproductBifunctor[X]
2021
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package underscores
2+
3+
trait Functor[M[_]] {
4+
def fmap[A, B](fa: M[A])(f: A => B): M[B]
5+
}
6+
7+
class EitherRightFunctor[L] extends Functor[Either[L, _]] {
8+
def fmap[A, B](fa: Either[L, A])(f: A => B): Either[L, B] =
9+
fa match {
10+
case Right(a) => Right(f(a))
11+
case Left(l) => Left(l)
12+
}
13+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package underscores
2+
3+
trait ~~>[A[_[_]], B[_[_]]] {
4+
def apply[X[_]](a: A[X]): B[X]
5+
}
6+
7+
trait Bifunctor[F[_[_[_]], _[_[_]]]] {
8+
def bimap[A[_[_]], B[_[_]], C[_[_]], D[_[_]]](fab: F[A, B])(f: A ~~> C, g: B ~~> D): F[C, D]
9+
}
10+
11+
final case class Coproduct[A[_[_]], B[_[_]], X[_]](run: Either[A[X], B[X]])
12+
13+
object Coproduct {
14+
def coproductBifunctor[X[_]]: Bifunctor[Coproduct[_[_[_]], _[_[_]], X]] =
15+
new Bifunctor[Coproduct[_[_[_]], _[_[_]], X]] {
16+
def bimap[A[_[_]], B[_[_]], C[_[_]], D[_[_]]](abx: Coproduct[A, B, X])(f: A ~~> C, g: B ~~> D): Coproduct[C, D, X] =
17+
abx.run match {
18+
case Left(ax) => Coproduct(Left(f(ax)))
19+
case Right(bx) => Coproduct(Right(g(bx)))
20+
}
21+
}
22+
def test[X[_]]: Bifunctor[({ type L[F[_[_]], G[_[_]]] = Coproduct[F, G, X] })#L] = coproductBifunctor[X]
23+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package underscores
2+
3+
// // From https://github.com/non/kind-projector/issues/20
4+
// import scala.language.higherKinds
5+
6+
object KindProjectorWarnings {
7+
trait Foo[F[_], A]
8+
trait Bar[A, B]
9+
10+
def f[G[_]]: Unit = ()
11+
12+
f[Foo[Bar[Int, _], _]] // shadowing warning
13+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package underscores
2+
3+
trait ~>[-F[_], +G[_]] {
4+
def apply[A](x: F[A]): G[A]
5+
}
6+
trait ~>>[-F[_], +G[_]] {
7+
def dingo[B](x: F[B]): G[B]
8+
}
9+
final case class Const[A, B](getConst: A)
10+
11+
class PolyLambdas {
12+
type ToSelf[F[_]] = F ~> F
13+
14+
val kf1 = Lambda[Option ~> Vector](_.iterator.toVector)
15+
16+
val kf2 = λ[Vector ~> Option] {
17+
case Vector(x) => Some(x)
18+
case _ => None
19+
}
20+
21+
val kf3 = λ[ToSelf[Vector]](_.reverse)
22+
23+
val kf4 = λ[Option ~>> Option].dingo(_ flatMap (_ => None))
24+
25+
val kf5 = λ[Map[_, Int] ~> Map[_, Long]](_.map { case (k, v) => (k, v.toLong) }.toMap)
26+
27+
val kf6 = λ[ToSelf[Map[_, Int]]](_.map { case (k, v) => (k, v * 2) }.toMap)
28+
29+
implicit class FGOps[F[_], A](x: F[A]) {
30+
def ntMap[G[_]](kf: F ~> G): G[A] = kf(x)
31+
}
32+
33+
// Scala won't infer the unary type constructor alias from a
34+
// tuple. I'm not sure how it even could, so we'll let it slide.
35+
type PairWithInt[A] = (A, Int)
36+
def mkPair[A](x: A, y: Int): PairWithInt[A] = x -> y
37+
val pairMap = λ[ToSelf[PairWithInt]] { case (k, v) => (k, v * 2) }
38+
val tupleTakeFirst = λ[λ[A => (A, Int)] ~> List](x => List(x._1))
39+
40+
// All these formulations should be equivalent.
41+
def const1[A] = λ[ToSelf[Const[A, _]]](x => x)
42+
def const2[A] : ToSelf[Const[A, _]] = λ[Const[A, _] ~> Const[A, _]](x => x)
43+
def const3[A] : Const[A, _] ~> Const[A, _] = λ[ToSelf[Const[A, _]]](x => x)
44+
def const4[A] = λ[Const[A, _] ~> Const[A, _]](x => x)
45+
def const5[A] : ToSelf[Const[A, _]] = λ[ToSelf[λ[B => Const[A, B]]]](x => x)
46+
def const6[A] : Const[A, _] ~> Const[A, _] = λ[ToSelf[λ[B => Const[A, B]]]](x => x)
47+
48+
@org.junit.Test
49+
def polylambda(): Unit = {
50+
assert(kf1(None) == Vector())
51+
assert(kf1(Some("a")) == Vector("a"))
52+
assert(kf1(Some(5d)) == Vector(5d))
53+
assert(kf2(Vector(5)) == Some(5))
54+
assert(kf3(Vector(1, 2)) == Vector(2, 1))
55+
assert(kf4.dingo(Some(5)) == None)
56+
assert(kf5(Map("a" -> 5)) == Map("a" -> 5))
57+
assert(kf6(Map("a" -> 5)) == Map("a" -> 10))
58+
59+
assert((mkPair("a", 1) ntMap pairMap) == ("a" -> 2))
60+
assert((mkPair(Some(true), 1) ntMap pairMap) == (Some(true) -> 2))
61+
62+
assert(mkPair('a', 1).ntMap(tupleTakeFirst) == List('a'))
63+
// flatten works, whereas it would be a static error in the
64+
// line above. That's pretty poly!
65+
assert(mkPair(Some(true), 1).ntMap(tupleTakeFirst).flatten == List(true))
66+
}
67+
}

0 commit comments

Comments
 (0)