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

Fix #7139: Implement kind-projector compatibility #7775

Merged
merged 1 commit into from
Jan 23, 2020

Conversation

travisbrown
Copy link
Contributor

@travisbrown travisbrown commented Dec 16, 2019

This change adds support for a subset of kind-projector's syntax behind the existing -Ykind-projector flag (closing #7139).

It supports the following kind-projector features:

  • * as a placeholder (Functor[Either[L, *]] is equivalent to Functor[[x] => Either[L, x]]).
  • * as a placeholder in tuple types (Functor[(A, *)] is equivalent to Functor[[x] => (A, x)]).
  • * as a placeholder in function types (both Functor[S => *] and Functor[* => T] work).
  • λ syntax for naming parameters ( Functor[λ[x => (x, x)]] for Functor[[x] => (x, x)]).

There are a few things kind-projector provides that this change doesn't support:

  • ? as a placeholder (since it collides with wildcards).
  • * as a placeholder in infix types.
  • Lambda as an alternative for λ.
  • λ arguments of a kind other than * (e.g. λ[f[_] => Functor[f]] isn't supported).
  • Variance annotations on either * or λ arguments.
  • Polymorphic lambda values (λ[Vector ~> List](_.toList)).

I've prioritized supporting the kind-projector features needed in Cats, and while we do use some of these features there, it's pretty easy to make small changes to avoid them without hurting readability (in most cases thse changes improve readability it, in my view—e.g. getting rid of λ[`-α` => ...]).

The changes here have no effect on parsing if -Ykind-projector isn't enabled. I enabled it generally in the test configuration since I didn't see a way to enable it for specific test files.

Copy link
Member

@dottybot dottybot left a comment

Choose a reason for hiding this comment

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

Hello, and thank you for opening this PR! 🎉

All contributors have signed the CLA, thank you! ❤️

Have an awesome day! ☀️

@smarter
Copy link
Member

smarter commented Dec 16, 2019

I enabled it generally in the test configuration since I didn't see a way to enable it for specific test files.

You'll need to add your test files in tests/pos-special and tests/neg-custom-args, then specify which flags to pass like this: https://github.com/lampepfl/dotty/blob/b8449ae956fbfcefaa50283291f7ba3b233303f4/compiler/test/dotty/tools/dotc/CompilationTests.scala#L58 https://github.com/lampepfl/dotty/blob/b8449ae956fbfcefaa50283291f7ba3b233303f4/compiler/test/dotty/tools/dotc/CompilationTests.scala#L127

@travisbrown
Copy link
Contributor Author

@smarter Thanks! Would it be better for me to do that? It seems like there's also value in confirming that the changes behind the flag don't break any existing tests.

@smarter
Copy link
Member

smarter commented Dec 16, 2019

It seems like there's also value in confirming that the changes behind the flag don't break any existing tests.

Yeah it's a trade-off, but I think it's better to not enable special language features by default in the tests, since that potentially decreases the coverage of the code path that most people will run.

@travisbrown
Copy link
Contributor Author

@smarter Sounds reasonable, thanks. I've just pushed the change.


val newParams = params.map {
case Ident(tpnme.raw.STAR) =>
val name = tpnme.syntheticTypeParamName(tparams.length)
Copy link
Member

Choose a reason for hiding this comment

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

To make sure there's no weird shadowing issues, it's probably better to use fresh names like we do for term parameters:

Suggested change
val name = tpnme.syntheticTypeParamName(tparams.length)
val name = WildcardParamName.fresh().toTypeName

private def replaceKindProjectorPlaceholders(params: List[Tree]): (List[Tree], List[TypeDef]) = {
val tparams = new ListBuffer[TypeDef]

val newParams = params.map {
Copy link
Member

Choose a reason for hiding this comment

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

To reuse the same list if possible:

Suggested change
val newParams = params.map {
val newParams = params.mapConserve {

Comment on lines 1330 to 1334
if (tparams.isEmpty) {
Function(params, t)
} else {
LambdaTypeTree(tparams, Function(newParams, newT))
}
Copy link
Member

Choose a reason for hiding this comment

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

I think this is equivalent to:

lambdaAbstract(tparams, Function(newParams, newT))

(lambdaAbstract could also be used in a couple of other places)

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 was using LambdaTypeTree directly to avoid using the new params, etc. when they weren't needed, and in the tuple case it lets us avoid instantiating a new Tuple value:

              if (tparams.isEmpty) {
                t
              } else {
                LambdaTypeTree(tparams, Tuple(newParams))
              }

I'm changing the other two now, and am happy to change the tuple one as well if you'd prefer.

val tparams = new ListBuffer[TypeDef]

val newParams = params.map {
case Ident(tpnme.raw.STAR) =>
Copy link
Member

Choose a reason for hiding this comment

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

Name the parameter to be used in another suggestion below:

Suggested change
case Ident(tpnme.raw.STAR) =>
case param @ Ident(tpnme.raw.STAR) =>

val newParams = params.map {
case Ident(tpnme.raw.STAR) =>
val name = tpnme.syntheticTypeParamName(tparams.length)
tparams += TypeDef(name, TypeBoundsTree(EmptyTree, EmptyTree))
Copy link
Member

Choose a reason for hiding this comment

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

  1. set the Param flag to make sure this is handled as a type parameter
  2. set the position ("span") of the constructed tree to match the position of the user-written *, for better error messages and IDE support.
Suggested change
tparams += TypeDef(name, TypeBoundsTree(EmptyTree, EmptyTree))
tparams += TypeDef(name, TypeBoundsTree(EmptyTree, EmptyTree))
.withFlags(Param).withSpan(param.span)

There's another use of TypeDef below which should do something similar (or even better, be refactored so there's only one point where TypeDef is used)

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 don't see a way to use the * span without ending up with a "child positions in wrong order" error:

java.lang.AssertionError: assertion failed: position error, child positions overlap or in wrong order
parent         = [_$5] =>> Either[Int, _$5]
1st child      = _$5
1st child span = [117..118]
2nd child      = Either[Int, _$5]
2nd child span = <105..115> while compiling tests/pos-special/kind-projector.scala
Fatal compiler crash when compiling: tests/pos-special/kind-projector.scala:
assertion failed: position error, child positions overlap or in wrong order
parent         = [_$5] =>> Either[Int, _$5]
1st child      = _$5
1st child span = [117..118]
2nd child      = Either[Int, _$5]
2nd child span = <105..115>
dotty.DottyPredef$.assertFail(DottyPredef.scala:17)
dotty.tools.dotc.ast.Positioned.check$1(Positioned.scala:183)

It seems like in this case it just might not be possible to point to the user-written *?

@travisbrown
Copy link
Contributor Author

@smarter Thanks for the comments! I've made the changes you suggest except for two things (also noted above):

  • I'm using LambdaTypeTree directly (instead of lambdaAbstract) for the tuple case where that lets us avoid instantiating a new Tuple.
  • I'm only setting the span on the TypeDef in the λ-argument case, since for * it ends up getting positions in the wrong order.

@travisbrown
Copy link
Contributor Author

@smarter Let me know if there's anything else I can do here, and thanks again!

@Katrix
Copy link
Contributor

Katrix commented Dec 18, 2019

Isn't Functor[Either[L, *]] being equivalent to [x] => Functor[Either[L, x]] the wrong behavior? Shouldn't it instead be equivalent to Functor[[x] => Either[L, x]]?

It's spelled out pretty explicitly as a gotcha on the docs too. https://github.com/typelevel/kind-projector/blob/master/README.md#type-lambda-gotchas

@travisbrown
Copy link
Contributor Author

@Katrix Sorry, that was me mistyping in the description. The actual behavior is as you describe. For example, if you compile this with -Ykind-projector -Xprint:parser:

trait Functor[F[_]]

object Functor {
  def eitherFunctor[L]: Functor[[x] =>> Either[L, x]] = ???
  def otherEitherFunctor[L]: Functor[Either[L, *]] = ???
}

You get this:

parsed:
package <empty> {
  trait Functor[F[_$1]] {}
  module object Functor {
    def eitherFunctor[L]: Functor[[x] =>> Either[L, x]] = ???
    def otherEitherFunctor[L]: Functor[[_$2] =>> Either[L, _$2]] = ???
  }
}

I've fixed the description. Thanks for catching that.

@travisbrown
Copy link
Contributor Author

@smarter I don't mean to sound impatient, but please let me know if it'd be useful for me to merge master or if there's anything else I can do here. Thanks!

@smarter
Copy link
Member

smarter commented Jan 16, 2020

Don't worry about merging with master, it's fine as is. Sorry for the wait, I keep getting distracted trying to fix the issues you're reporting :).

Copy link
Member

@smarter smarter left a comment

Choose a reason for hiding this comment

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

LGTM with one minor nit, sorry for the delay in review!

Longer term, I think we should try handling type placeholders in the same way we handle term placeholders currently (see the var placeholderParams that is used as a stack). This would avoid having to special case applied types, tuples, infix types, etc. This isn't a big deal for now since placeholder params are just a compatibility thing, but in the future we'd like to have them by default with one syntax or another: #5379

@@ -1413,6 +1420,29 @@ object Parsers {
}
}

private def makeKindProjectorTypeDef(name: TypeName, span: Option[Span]): TypeDef = {
Copy link
Member

Choose a reason for hiding this comment

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

I think we should drop the span parameter and just have call-sites that need to set a span do it themselves (makeKindProjectorTypeDef(...).withSpan(...)), it's not much longer and slightly clearer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed—I've just pushed the change.

@smarter
Copy link
Member

smarter commented Jan 22, 2020

Sorry, you'll need to rebase to pick up some recent CI changes fixing dependency resolution (while you're at it, please squash these commits together since they represent a single change, and maybe reuse the PR description as a commit message since it's more detailed).

This change adds support for a subset of kind-projector's syntax behind
the existing -Ykind-projector flag.

It supports the following kind-projector features:

- * placeholder (Functor[F[L, *]] instead of Functor[[x] => F[L, x]]).
- * in tuple types (Functor[(A, *)] instead of Functor[[x] => (A, x)]).
- * in function types (both Functor[S => *] and Functor[* => T] work).
- λ syntax (Functor[λ[x => (x, x)]] for Functor[[x] => (x, x)]).

There are a few things kind-projector provides that the flag doesn't:

- ? as a placeholder (since it collides with wildcards).
- * as a placeholder in infix types.
- Lambda as an alternative for λ.
- λ arguments of a kind other than * (no λ[f[_] => Functor[f]]).
- Variance annotations on either * or λ arguments.
- Polymorphic lambda values (λ[Vector ~> List](_.toList)).

The changes have no effect on parsing if -Ykind-projector isn't enabled.
@travisbrown
Copy link
Contributor Author

@smarter Done!

@odersky odersky merged commit 709a3c7 into scala:master Jan 23, 2020
xuwei-k added a commit to scalaz/scalaz that referenced this pull request May 16, 2020
prepare dotty.

dotty "-Ykind-projector" does not support higher kind

scala/scala3#7775

> λ arguments of a kind other than * (e.g. λ[f[_] => Functor[f]] isn't supported).
xuwei-k added a commit to scalaz/scalaz that referenced this pull request May 16, 2020
dotty `-Ykind-projector` does not support this patterns

scala/scala3#7775
@djspiewak
Copy link

For posterity…

There are a few cases which are remarkably annoying to port from kind-projector syntax to -Ykind-projector due to the omissions in this implementation. Specifically, something like this:

trait Bar[R[_[_], _], F[_], E]
trait Baz[F[_], E]

def foo[R[_[_], _], F[_], E]: Bar[λ[(f[_], a) => Baz[R[f, ?], a]], F, E] = ???

The only way I could find to meaningfully port this was to use an OG type lambda:

type BazLam[R[_[_], _]] = { type L[F[_], A] = Baz[R[F, *], A] }
def foo[R[_[_], _], F[_], E]: Bar[BazLam[R]#L, F, E] = ???

This could would be a lot easier to write if it were Scala 2 only, or Scala 3 only, but since it's cross-compiling it becomes a bit of a headache.

@smarter
Copy link
Member

smarter commented May 25, 2020

@djspiewak We welcome changes to -Ykind-projector to make it support more complicated cases of kind-projector type lambda syntax if it's useful for cross-compilation. though if these usages of kind-pjector are rare enough in your codebase, maybe defining some type aliases would be good enough?

@djspiewak
Copy link

though if these usages of kind-pjector are rare enough in your codebase, maybe defining some type aliases would be good enough?

I'm currently tracking the pain meter. :-) If it reaches a certain point, I'll bite the bullet and PR improvements to make it easier. Honestly the more painful thing is losing the value-level syntax, since that's basically everywhere in all of my codebases, but I'm under the impression that is somewhat more complicated to achieve.

@djspiewak
Copy link

(as an aside, https://gist.github.com/02019a4f1c83f7b7b3978964b9ed06ab almost works for shimming the value level syntax; the only real problem is that the call site inference for polyfunctions is rather limited)

@travisbrown
Copy link
Contributor Author

For what it’s worth I have a Scalafix rule that desugars the value-level syntax here.

@smarter
Copy link
Member

smarter commented May 26, 2020

call site inference for polyfunctions is rather limited

Yeah that's on my todo list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants