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

Context Bounds for Polymorphic Functions #21643

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

KacperFKorban
Copy link
Member

@KacperFKorban KacperFKorban commented Sep 25, 2024

Implement the #6 point form SIP-64 i.e.


6. Context Bounds for Polymorphic Functions

Currently, context bounds can be used in methods, but not in function types or function literals. It would be nice propose to drop this irregularity and allow context bounds also in these places. Example:

type Comparer = [X: Ord] => (x: X, y: X) => Boolean
val less: Comparer = [X: Ord as ord] => (x: X, y: X) =>
  ord.compare(x, y) < 0

The expansion of such context bounds is analogous to the expansion in method types, except that instead of adding a using clause in a method, we insert a context function type.

For instance, the type and val definitions above would expand to

type Comparer = [X] => (x: X, y: X) => Ord[X] ?=> Boolean
val less: Comparer = [X] => (x: X, y: X) => (ord: Ord[X]) ?=>
  ord.compare(x, y) < 0

The expansion of using clauses does look inside alias types. For instance,
here is a variation of the previous example that uses a parameterized type alias:

type Cmp[X] = (x: X, y: X) => Boolean
type Comparer2 = [X: Ord] => Cmp[X]

The expansion of the right hand side of Comparer2 expands the Cmp[X] alias
and then inserts the context function at the same place as what's done for Comparer.


Currently missing:

  • Support for poly functions that only have context bound term parameters

@KacperFKorban
Copy link
Member Author

@odersky Could you take a look and see if there is something fundamentally wrong with this approach?

On a slightly different note:
What is the motivation for following aliases? I'm not sure how exactly it should work and how to fit it nicely into the current design, which does most of the transformations in desugaring. Should it only expand aliases and not other type parameters or match types?
Should we for example be able to represent the following declaration in TASTY?
type Comparer3[F[_]] = [X: Ord] => (x: X, y: X) => F[Boolean]

compiler/src/dotty/tools/dotc/typer/Typer.scala Outdated Show resolved Hide resolved
compiler/src/dotty/tools/dotc/typer/Typer.scala Outdated Show resolved Hide resolved
compiler/src/dotty/tools/dotc/typer/Typer.scala Outdated Show resolved Hide resolved
compiler/src/dotty/tools/dotc/ast/Desugar.scala Outdated Show resolved Hide resolved
@odersky
Copy link
Contributor

odersky commented Sep 30, 2024

About aliases:

type Cmp[X] = (x: X, y: X) => Ord[X] ?=> Boolean
type Comparer2 = [X: Ord] => Cmp[X]

The tricky part is, say you have

val f =  [X; Ord] => (x: X, y: X) => Ord[X] ?=> true

Does f: Comparer2 hold?

@odersky
Copy link
Contributor

odersky commented Sep 30, 2024

Also, we don't need to put this modularity anymore since SIP 64 got accepted.

@KacperFKorban
Copy link
Member Author

KacperFKorban commented Oct 1, 2024

@odersky I'm not entirely sure that I understand your aliases example.
Do you mean that we need some normalization mechanism to eliminate redundant context functions? Or not generate them when they are unnecessary?

e.g.

[X: Ord] => (x: X, y: X) => Ord[X] ?=> Boolean
===>(desugar)
[X] => (x: X, y: X) => Ord[X] ?=> Ord[X] ?=> Boolean
===>(normalize)
[X] => (x: X, y: X) => Ord[X] ?=> Boolean

@odersky
Copy link
Contributor

odersky commented Oct 1, 2024

Well, does f: Comparer2 hold or not? I guess it depends how Comparer2 is expanded. I believe it should hold.

@KacperFKorban
Copy link
Member Author

It doesn't hold now. I'll try moving the expansion later, to be able to use some type info then.

@KacperFKorban
Copy link
Member Author

@odersky I rewrote the whole implementation to first mark apply methods in PolyFunctions and insert the context parameters after that when typing.
The missing part is the support for PolyFunctions that only have context bounds with an alias result e.g. [X: Ord] => SomeAlias[X]. This is mainly because the compiler assumes in a few places, that a PolyFunction has exactly one type param clause and exactly one term param clause. Do you have any advice for me strictly regarding the implementation?

@KacperFKorban KacperFKorban changed the title [WIP] Context Bounds for Polymorphic Functions Context Bounds for Polymorphic Functions Oct 18, 2024
@bishabosha bishabosha added the needs-minor-release This PR cannot be merged until the next minor release label Oct 19, 2024
@bishabosha
Copy link
Member

I guess this should be merged in 3.7 now? @KacperFKorban @WojciechMazur

@Gedochao
Copy link
Contributor

Gedochao commented Oct 21, 2024

I guess this should be merged in 3.7 now? @KacperFKorban @WojciechMazur

+1, it's too late for this to be included in 3.6 (unless it's behind an experimental flag)

@KacperFKorban KacperFKorban marked this pull request as ready for review October 23, 2024 07:40
@Gedochao
Copy link
Contributor

@bishabosha @KacperFKorban Actually, it seems we will be backporting this change to 3.6.2 (which, despite appearances, is technically considered the first stable version in the 3.6.x line).

@soronpo
Copy link
Contributor

soronpo commented Oct 30, 2024

@bishabosha @KacperFKorban Actually, it seems we will be backporting this change to 3.6.2 (which, despite appearances, is technically considered the first stable version in the 3.6.x line).

I don't know if it was considered, but you could skip to 3.7.0 and declare all 3.6.x as invalid.

@Gedochao
Copy link
Contributor

@bishabosha @KacperFKorban Actually, it seems we will be backporting this change to 3.6.2 (which, despite appearances, is technically considered the first stable version in the 3.6.x line).

I don't know if it was considered, but you could skip to 3.7.0 and declare all 3.6.x as invalid.

It was considered, but we decided to keep 3.6.x instead.

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

Code is good, with scope for some better comments. We need to refine the logic where evidence parameters are added.

compiler/src/dotty/tools/dotc/ast/Desugar.scala Outdated Show resolved Hide resolved
@@ -508,7 +514,24 @@ object desugar {
case Nil =>
params :: Nil

cpy.DefDef(meth)(paramss = recur(meth.paramss))
def pushDownEvidenceParams(tree: Tree): Tree = tree match
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be good to add a doc comment explaining what it does.

/** Push down the deferred evidence parameters up until the result type is not
* a method type, poly type or a function type
*/
private def pushDownDeferredEvidenceParams(tpe: Type, params: List[untpd.ValDef], span: Span)(using Context): Type = tpe.dealias match {
Copy link
Contributor

@odersky odersky Nov 4, 2024

Choose a reason for hiding this comment

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

I believe this is too coarse. If the evidence parameter is named, a parameter could already refer in its type to it. So the clause has to go just in front of this parameter, as is explained in Section 3 of SIP 64 and implemented in Desugar.addEvidenceParams. We should see how we can re-use this logic.

@odersky odersky assigned noti0na1 and KacperFKorban and unassigned odersky and noti0na1 Nov 4, 2024
@noti0na1
Copy link
Member

noti0na1 commented Nov 5, 2024

Will nested polymorphic functions work? like [X: A] => X => [Y: B] => Y => Z

@WojciechMazur
Copy link
Contributor

Do we have any estimation of when this PR could be finalized?
We concluded that we want to have it in 3.6, so it's becoming a blocker for a stable release. The cutoff for 3.6.3 (current development cycle) is on 19th November, so next week. We'd prefer to have at least 3.6.2-RC1 by this date.

@KacperFKorban
Copy link
Member Author

I'll try to address the review comments today/tomorrow.

@WojciechMazur
Copy link
Contributor

ping

@KacperFKorban KacperFKorban force-pushed the context-bounds-for-poly-functions branch from 72d4d35 to 24e3fa0 Compare November 14, 2024 13:19
Copy link
Member

@noti0na1 noti0na1 left a comment

Choose a reason for hiding this comment

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

Some small questions. I think we should have at least a simple run test to ensure the context parameters are working. Otherwise, LGTM.

compiler/src/dotty/tools/dotc/typer/Typer.scala Outdated Show resolved Hide resolved
compiler/src/dotty/tools/dotc/typer/Typer.scala Outdated Show resolved Hide resolved
@KacperFKorban
Copy link
Member Author

@odersky I changed the implementation to reuse the logic from addEvidenceParams. This makes the desugaring consistent with the way we insert evidence parameters into methods.
For a poly function [X: {TC as tc}] => Params => Res, the TC evidence parameter will:

  • replace Params, if any of Params references tc, and Params will be moved as parameters to a result function -- [X] => (tc: TC) ?=> Params => Res
  • be inserted into Params, if Params are given/implicit parameters -- [X] => (Params ++ (tc: TC)) ?=> Res
  • otherwise, tc will be added as contextual parameters to the resulting function -- [X] => Params => (tc: TC) ?=> Res

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-minor-release This PR cannot be merged until the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants