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

[WIP] Context Bounds for Polymorphic Functions #21643

Draft
wants to merge 10 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) => Ord[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.


Open questions:

  • Should [X: Ord] => Cmp[X] be accepted as a correct poly function, since it eventually will have term parameters?

Currently missing:

  • Support for following type aliases when expanding context bounds

@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
@@ -1717,7 +1717,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
typedFunctionType(desugar.makeFunctionWithValDefs(tree, pt), pt)
else
val funSym = defn.FunctionSymbol(numArgs, isContextual, isImpure)
val result = typed(cpy.AppliedTypeTree(tree)(untpd.TypeTree(funSym.typeRef), args :+ body), pt)
val args1 = args.mapConserve {
case cb: untpd.ContextBoundTypeTree => typed(cb)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that won't do anything since args1 gets typed below as a list of untyped type trees. So typing it here beforehand should be redundant.

@@ -1926,8 +1930,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer

def typedPolyFunction(tree: untpd.PolyFunction, pt: Type)(using Context): Tree =
val tree1 = desugar.normalizePolyFunction(tree)
if (ctx.mode is Mode.Type) typed(desugar.makePolyFunctionType(tree1), pt)
else typedPolyFunctionValue(tree1, pt)
val tree2 = if Feature.enabled(Feature.modularity) then desugar.expandPolyFunctionContextBounds(tree1)
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe roll expandPolyFunctionContextBounds into normalizePolyFunction?


val less8 = [X: {Ord, Show as show}] => (x: X, y: X) => summon[Ord[X]].compare(x, y) < 0

val less9 = [X: {Ord as ord, Show as show}] => (x: X, y: X) => ord.compare(x, y) < 0
Copy link
Contributor

Choose a reason for hiding this comment

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

We should have tests that compare these polyfunction values with their expected types. E.g.

val _: [X: {Ord as ord, Show}] => (x: X, y: X) => Boolean = less7

Also we should have use aliases in some parts of these types and see whether it still works.

/** Desugar [T_1 : B_1, ..., T_N : B_N] => (P_1, ..., P_M) => R
* Into [T_1, ..., T_N] => (P_1, ..., P_M) => (B_1, ..., B_N) ?=> R
*/
def expandPolyFunctionContextBounds(tree: PolyFunction)(using Context): PolyFunction =
Copy link
Contributor

Choose a reason for hiding this comment

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

I was wondering whether we could re-use some of the code that desugars context bounds in methods. But maybe not. In any case, need to check that all applicable cases there are also present here.

@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

@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.

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.

2 participants