Skip to content

Commit

Permalink
Add support for variance.
Browse files Browse the repository at this point in the history
This commit finally adds support for variance annotations on type lambda
parameters. This was the last piece of type projection syntax which
kind-projector didn't support.

There are three ways to do this:

  Either[+_, Int]
  Lambda[`+A` => Either[A, Int]]
  Lambda[+[A] => Either[A, Int]]

It's possible we will deprecate one of the lambda forms if there is broad
agreement, but right now neither one is obviously better than the other.
  • Loading branch information
non committed Mar 9, 2014
1 parent 0c44f7f commit c5539f6
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 76 deletions.
163 changes: 102 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,70 +39,127 @@ One problem with this approach is that it changes the meaning of
(potentially) valid programs. In practice this means that you must
avoid defining the following identifiers:

1. Lambda
2. λ
3. ?
4. L_kp
5. X_kp0, X_kp1, ...
1. `Lambda` and `λ`
2. `?`, `+?`, and `-?`
4. `L_kp`
5. `X_kp0`, `X_kp1`, ...

If you find yourself using lots of type lambdas, and you don't mind reserving
those identifiers, then this compiler plugin is for you!
If you find yourself using lots of type lambdas, and you don't mind
reserving those identifiers, then this compiler plugin is for you!

### Examples
### Using the plugin

Two syntaxes are supported. The first resembles the `_ + _` syntax for
anonymous functions and turns things like:
To use this plugin in your own projects, add the following lines to
your `build.sbt` file:

```scala
Either[?, Double]
Tuple3[Int, ?, ?]
resolvers += "bintray/non" at "http://dl.bintray.com/non/maven"

// for scala 2.10
addCompilerPlugin("org.spire-math" % "kind-projector_2.10" % "0.5.0")

// for scala 2.9.3
//addCompilerPlugin("org.spire-math" % "kind-projector_2.9.3" % "0.5.0")
```

into type projections like:
That's it!

### Inline Syntax

The simplest syntax to use is the inline syntax. This syntax resembles
Scala's use of underscores to define anonymous functions like `_ + _`.

Since underscore is used for existential types in Scala (and it is
probably too late to change this syntax), we use `?` for the same
purpose. We also use `+?` and `-?` to handle covariant and
contravariant types parameters.

Here are a few examples:

```scala
({type L_kp[X_kp0] = Either[X_kp0, Double]})#L_kp
({type L_kp[X_kp1, X_kp2] = Tuple3[Int, X_kp1, X_kp2]})#L_kp
Tuple2[?, Double] // equivalent to: type R[A] = Tuple2[A, Double]
Either[Int, +?] // equivalent to: type R[+A] = Either[Int, A]
Tuple3[?, Long, ?] // equivalent to: type R[A, B] = Tuple3[A, Long, B]
```

The second resembles the `(x, y) => x + y` syntax for anonymous functions and
turns things like:
As you can see, this syntax works when each type parameter in the type
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.

### Function Syntax

The more powerful syntax to use is the function syntax. This syntax
resembles anonymous functions like `x => x + 1` or `(x, y) => x + y`.
In the case of type lambdas, we wrap the entire function type in a
`Lambda` or `λ` type. Both names are equivalent: the former may be
easier to type or say, and the latter is less verbose.

Here are some examples:

```scala
Lambda[A => (A, A)]
Lambda[(A, B) => Either[A, Option[B]]]
Lambda[A => (A, A)] // equivalent to: type R[A] = (A, A)
Lambda[(A, B) => Either[B, A]] // equivalent to: type R[A, B] = Either[B, A]
Lambda[A => Either[A, List[A]] // equivalent to: type R[A] = Either[A, List[A]]
```

into type projections like:
Since types like `(+A, +B) => Either[A, B]` are not syntactically
valid, we provide two alternate methods to specify variance when using
function syntax:

* Plus/minus: `(+[A], +[B]) => Either[A, B]`
* Backticks: ``(`+A`, `+B`) => Either[A, B]``

(Note that unlike names like `?`, `+` and `-` do not have to be
reserved. They will only be interpreted this way when used in
parameters to `Lambda[...]` types, which should never conflict with
other usage.)

Here are some examples with variance:

```scala
({type L_kp[A] = (A, A)})#L_kp
({type L_kp[A, B] = Either[A, Option[B]]})#L_kp
λ[`-A` => Function1[A, Double]] // equivalent to: type R[-A] = Function1[A, Double]
λ[(-[A], +[B]) => Function2[A, Int, B]] // equivalent to: type R[-A, +B] = Function2[A, Int, B]
λ[`+A` => Either[List[A], List[A]] // equivalent to: type R[+A] = Either[List[A], List[A]]
```

You can also use unicode if you like that sort of thing:
The function syntax also supports higher-kinded types as type
parameters. The syntax overloads the existential syntax in this case
(since the type parameters to a type lambda should never contain an
existential).

Here are a few examples with higher-kinded types:

```scala
λ[A => (A, A)]
λ[(A, B) => Either[A, Option[B]]]
Lambda[A[_] => List[A[Int]]] // equivalent to: type R[A[_]] = List[A[Int]]
Lambda[(A, B[_]) => B[A]] // equivalent to: type R[A, B[_]] = B[A]
```

### Using the plugin
### Under The Hood

To use this plugin in your own projects, add the following lines to
your `build.sbt` file:
This section shows the exact code produced for a few type lambda
expressions.

```scala
resolvers += "bintray/non" at "http://dl.bintray.com/non/maven"
Either[Int, ?]
({type L_kp[X_kp1] = (Int, X_kp1)})#L_kp

// for scala 2.10
addCompilerPlugin("org.spire-math" % "kind-projector_2.10" % "0.4.0")
Function2[-?, String, +?]
({type L_kp[-X_kp0, +X_kp2] = Function2[X_kp0, String, X_kp2)})#L_kp

// for scala 2.9.3
//addCompilerPlugin("org.spire-math" % "kind-projector_2.9.3" % "0.4.0")
Lambda[A => (A, A)]
({type L_kp[A] = (A, A)})#L_kp

Lambda[(`+A`, B) => Either[A, Option[B]]]
({type L_kp[+A, B] = Either[A, Option[B]]})#L_kp

Lambda[(A, B[_]) => B[A]]
({type L_kp[A, B[_]] = B[A]})#L_kp
```

That's it!
As you can see, the reason that names like `L_kp` and `X_kp0` are
forbidden is that they would potentially conflict with the names of
types generated by the plugin.

### Building the plugin

Expand All @@ -113,13 +170,13 @@ Here are some useful targets:
* compile: compile the code
* package: build the plugin jar
* test: compile the test files (no tests run; compilation is the test)
* console: you can play around with the plugin using the console
* console: launch a REPL with the plugin loaded so you can play around

You can use the plugin with `scalac` by specifying it on the
command-line. For instance:

```
scalac -Xplugin:kind-projector_2.10-0.4.0.jar test.scala
scalac -Xplugin:kind-projector_2.10-0.5.0.jar test.scala
```

### Known issues & errata
Expand All @@ -137,37 +194,21 @@ to define a type lambda the way we use `3 + _` to define a
function. Unfortunately, it's probably too late to modify the meaning
of _, which is why we chose to use `?` instead.

Support for existentials has recently been added. The syntax is as
follows:

```scala
Lambda[A[_] => List[A[Int]]]
Lambda[(A, B[_]) => B[A]]
```

### Future Work

Variance annotations are not yet supported. It's likely that the
wilcard syntax will use `+?` and `-?`, e.g. `Function2[-?, Int, +?]`.

It's a bit less clear what the lambda syntax should use. Possible
candidates are:
As of 0.5.0, kind-projector should be able to support any type lambda
that can be expressed via type projections. If you come across a type
for which kind-projector lacks a syntax, please report it.

* `Lambda[(-[A], +[B]) => Function2[A, Int, B]]`
* `Lambda[(-_A, +_B) => Function2[A, Int, B]]`

(Obviously, other names could be used instead of `+` and `-` here.)

### Disclaimers

This is only working in the most fragile sense. If you try "fancy"
things like `Either[Int, ?][Double]`, you will probably not like the
result. This project is clearly an abuse of the compiler plugin
framework and the author disclaims all warranty or liability of any
kind.
Kind projector is an unusual compiler plugin in that it runs *before*
the `typer` phase. This means that the rewrites and renaming we are
doing is relatively fragile, and the author disclaims all warranty or
liability of any kind.

That said, if you end up using this plugin, even in a toy project,
please let me know!
If you are using kind-projector in one of your projects, please feel
free to get in touch to report problems (or a lack of problems)!

### Copyright and License

Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name := "kind-projector"

organization := "org.spire-math"

version := "0.4.0"
version := "0.5.0"

scalaVersion := "2.10.3"

Expand Down
65 changes: 51 additions & 14 deletions src/main/scala/KindProjector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ class KindRewriter(plugin: Plugin, val global: Global)
class MyTransformer(unit: CompilationUnit) extends TypingTransformer(unit) {

// reserve some names
val tlambda1 = newTypeName("Lambda")
val tlambda2 = newTypeName("λ")
val placeholder = newTypeName("$qmark")
val TypeLambda1 = newTypeName("Lambda")
val TypeLambda2 = newTypeName("λ")
val Placeholder = newTypeName("$qmark")
val CoPlaceholder = newTypeName("$plus$qmark")
val ContraPlaceholder = newTypeName("$minus$qmark")

val covariant = newTypeName("$plus$qmark")
val contravariant = newTypeName("$minus$qmark")
// these will be used for matching but aren't reserved
val Plus = newTypeName("$plus")
val Minus = newTypeName("$minus")

override def transform(tree: Tree): Tree = {

Expand All @@ -55,11 +58,33 @@ class KindRewriter(plugin: Plugin, val global: Global)
def makeTypeParam(name: Name) =
TypeDef(Modifiers(PARAM), makeTypeName(name), Nil, bounds)

// Like makeTypeParam but with covariance, e.g.
// ({type L[+A] = ... })#L.
def makeTypeParamCo(name: Name) =
TypeDef(Modifiers(PARAM | COVARIANT), makeTypeName(name), Nil, bounds)

// Like makeTypeParam but with contravariance, e.g.
// ({type L[-A] = ... })#L.
def makeTypeParamContra(name: Name) =
TypeDef(Modifiers(PARAM | CONTRAVARIANT), makeTypeName(name), Nil, bounds)

// Detects which makeTypeParam* method to call based on name.
// Names like +A are covariant, names like -A are contravariant,
// all others are invariant.
def makeTypeParamFromName(name: Name) =
if (name.startsWith("$plus")) {
makeTypeParamCo(newTypeName(name.toString.substring(5)))
} else if (name.startsWith("$minus")) {
makeTypeParamContra(newTypeName(name.toString.substring(6)))
} else {
makeTypeParam(name)
}

// Like makeTypeParam, but can be used recursively in the case of types
// that are themselves parameterized.
def makeComplexTypeParam(t: Tree): TypeDef = t match {
case Ident(name) =>
makeTypeParam(name)
makeTypeParamFromName(name)

case TypeDef(m, nm, ps, bs) =>
TypeDef(Modifiers(PARAM), nm, ps.map(makeComplexTypeParam), bs)
Expand Down Expand Up @@ -105,7 +130,13 @@ class KindRewriter(plugin: Plugin, val global: Global)
val (args, subtree) = parseLambda(a, as, Nil)
val innerTypes = args.map {
case Ident(name) =>
makeTypeParam(name)
makeTypeParamFromName(name)

case AppliedTypeTree(Ident(Plus), Ident(name) :: Nil) =>
makeTypeParamCo(name)

case AppliedTypeTree(Ident(Minus), Ident(name) :: Nil) =>
makeTypeParamContra(name)

case AppliedTypeTree(Ident(name), ps) =>
val tparams = ps.map(makeComplexTypeParam)
Expand All @@ -127,16 +158,22 @@ class KindRewriter(plugin: Plugin, val global: Global)
def handlePlaceholders(t: Tree, as: List[Tree]) = {
// create a new type argument list, catching placeholders and create
// individual identifiers for them.
val args = as.zipWithIndex.map {
case (Ident(`placeholder`), i) => Ident(newTypeName("X_kp%d" format i))
case (a, i) => super.transform(a)
val xyz = as.zipWithIndex.map {
case (Ident(Placeholder), i) => (Ident(newTypeName("X_kp%d" format i)), Some(Placeholder))
case (Ident(CoPlaceholder), i) => (Ident(newTypeName("X_kp%d" format i)), Some(CoPlaceholder))
case (Ident(ContraPlaceholder), i) => (Ident(newTypeName("X_kp%d" format i)), Some(ContraPlaceholder))
case (a, i) => (super.transform(a), None)
}

// for each placeholder, create a type parameter
val innerTypes = args.collect {
case Ident(name) if name.startsWith("X_kp") => makeTypeParam(name)
val innerTypes = xyz.collect {
case (Ident(name), Some(Placeholder)) => makeTypeParam(name)
case (Ident(name), Some(CoPlaceholder)) => makeTypeParamCo(name)
case (Ident(name), Some(ContraPlaceholder)) => makeTypeParamContra(name)
}

val args = xyz.map(_._1)

// if we didn't have any placeholders use the normal transformation.
// otherwise build a type projection.
if (innerTypes.isEmpty) super.transform(tree)
Expand All @@ -145,11 +182,11 @@ class KindRewriter(plugin: Plugin, val global: Global)

tree match {
// Lambda[A => Either[A, Int]] case.
case AppliedTypeTree(Ident(`tlambda1`), AppliedTypeTree(_, a :: as) :: Nil) =>
case AppliedTypeTree(Ident(TypeLambda1), AppliedTypeTree(_, a :: as) :: Nil) =>
handleLambda(a, as)

// λ[A => Either[A, Int]] case.
case AppliedTypeTree(Ident(`tlambda2`), AppliedTypeTree(_, a :: as) :: Nil) =>
case AppliedTypeTree(Ident(TypeLambda2), AppliedTypeTree(_, a :: as) :: Nil) =>
handleLambda(a, as)

// Either[?, Int] case (if no ? present this is a noop)
Expand Down
14 changes: 14 additions & 0 deletions src/test/scala/test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,18 @@ object Test {
def hex[T[_[_[_[_]]]]] = ()
hex[({type L[A[_[_[_]]]] = Unit})#L]
hex[Lambda[A[_[_[_]]] => Unit]]

// covariant
def mux[T[+_]] = ()
mux[({type L[+A] = Either[A, Int]})#L]
mux[Either[+?, Int]]
mux[Lambda[`+A` => Either[A, Int]]]
mux[Lambda[+[A] => Either[A, Int]]]

// contravariant
def bux[T[-_, +_]] = ()
bux[({type L[-A, +B] = Function2[A, Int, B]})#L]
bux[Function2[-?, Int, +?]]
bux[Lambda[(`-A`, `+B`) => Function2[A, Int, B]]]
bux[Lambda[(-[A], +[B]) => Function2[A, Int, B]]]
}

0 comments on commit c5539f6

Please sign in to comment.