Skip to content

Add dependent function types #3464

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

Merged
merged 13 commits into from
Nov 27, 2017
Merged

Add dependent function types #3464

merged 13 commits into from
Nov 27, 2017

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Nov 12, 2017

We now support dependent function types as long as there are no inter-parameter dependencies.
Assuming

class C { type T }

a dependent function type is written like this:

 (x: C) => x.T

It gets represented as a normal function type that approximates the result type together with a refinement that adds an apply method with the precise type. E.g. the type above would be represented as

Function1[C, C#T] { def apply(x: C): x.T }

For the moment, dependent function types cannot be implicit. Maybe we can lift that restriction in the future.

Dependent function types are important, because they are already in DOT, and they are probably a key feature to enable polymorphic effect systems.

@nicolasstucki
Copy link
Contributor

We should have some neg tests for implicit dependent function types.

@LPTK
Copy link
Contributor

LPTK commented Nov 14, 2017

This is great. Now we can almost have a nice first-class polymorphic lambda syntax.

> def baz(f: (t: C) => List[t.T] => List[t.T]): Int = 
    f(new C{type T = Int})(List(1,2,3)).head
> baz(t => (ls:List[t.T]) => ls.reverse)
res0: Int = 3

Unfortunately, we currently need to explicitly state the type of ls, and also the whole thing stops working when t is made implicit.

Ideally, is it reasonable to expect the following program to work?

def baz(f: implicit (t: C) => List[t.T] => List[t.T]): Int = f(new C{type T = Int})(List(1,2,3)).head
baz(ls => ls.reverse)

*/
def instantiateDependent(tp: Type, tparams: List[Symbol], vparamss: List[List[Symbol]])(implicit ctx: Context): Unit = {
val dependentVars = new TypeAccumulator[Set[TypeVar]] {
lazy val params = (vparamss :\ tparams)( _ ::: _)
Copy link
Contributor

Choose a reason for hiding this comment

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

It this equivalent to vparamss.flatten ::: tparams? I guess as order doesn't matter it would be (tparams :: vparamss).flatten

Copy link
Contributor

Choose a reason for hiding this comment

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

Both will not conserve the last param list in vparamss, this might be useful in all non type polymorphic methods with a single argument list ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(tparams :: vparamss).flatten is indeed much clearer.

ParamRefNameString(tp) ~ ".type"
case tp: TypeParamRef =>
ParamRefNameString(tp) ~ lambdaHash(tp.binder)
case tp: SingletonType =>
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 something is wrong with the way dependent function types are printed:

scala> val depfun1: DF = (x: C) => x.m
val depfun1: (C => ){apply: (x: C): } = <function1>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should be fixed now.

// Reproduced here because the one from DottyPredef is lacking a Tasty tree and
// therefore can't be inlined when testing non-bootstrapped.
// But inlining `implicitly` is vital to make the definition of `ifun` below work.
inline final def implicitly[T](implicit ev: T): T = ev
Copy link
Contributor

Choose a reason for hiding this comment

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

Would changing implicitly to return ev.type instead of T help here?

Copy link
Member

Choose a reason for hiding this comment

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

The fact that adding inline changes how a method is typechecked doesn't seem like a great thing. I think @OlivierBlanvillain has some examples where it breaks working code.

Copy link
Contributor Author

@odersky odersky Nov 15, 2017

Choose a reason for hiding this comment

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

I am a bit nervous about changing it to ev.type. The inferencer has to do major acrobatics to deal with implicit method types. It might interfere with something else. The current type of implicitly is simpler and clearer.

@@ -1686,7 +1714,7 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
}

protected def makeImplicitFunction(tree: untpd.Tree, pt: Type)(implicit ctx: Context): Tree = {
val defn.FunctionOf(formals, resType, true) = pt.dealias
val defn.FunctionOf(formals, _, true) = pt.dealias.dropDependentRefinement
Copy link
Contributor

Choose a reason for hiding this comment

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

Since .dropDependentRefinement is always used after .dealias I guess the dealias could be pushed inside


args match {
case ValDef(_, _, _) :: _ =>
typedDependent(args.asInstanceOf[List[ValDef]])(
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a bit confused by this cast, args matches ValDef() and is casted to a List[ValDef]??

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I didn't see the ::

val (protoFormals, protoResult) = decomposeProtoFunction(pt, params.length)
def typedFunctionValue(tree: untpd.Function, pt: Type)(implicit ctx: Context) = {
val untpd.Function(args, body) = tree
val params = args.asInstanceOf[List[untpd.ValDef]]
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe type annotate args in the pattern above?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea.

trait C { type M; val m: M }

type DF = (x: C) => x.M
val depfun1: DF = (x: C) => x.m
Copy link
Contributor

Choose a reason for hiding this comment

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

I like than both the type and the value have the same syntax :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes. This is something that I really like about Scala in general.

}
val funCls = defn.FunctionClass(args.length, isImplicit)

def typedDependent(params: List[ValDef])(implicit ctx: Context) = {
Copy link
Member

Choose a reason for hiding this comment

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

I would add a comment explaining to quickly describe what this function returns.

@odersky odersky force-pushed the add-depfuns branch 2 times, most recently from 93bf1f8 to 07ab554 Compare November 15, 2017 17:32
Tests indicate it's not needed anymore.

Also: fix neg test error position.
For the moment we want to rule this out. There are quite a lot of
bits we have to think through before we can allow this.
If the result type of a dependent function contains a type variable whose bounds
refer to the function's parameters, that type variable has to be instantiated before
the method type is formed. Otherwise, if the type variable is instantiated later
the method result type will refer to the original parameter symbols instead of
the method types ParamRefs.
`isFunctionType` nor returns true for dependent as well as non-dependent
function types. `isNonDepFunctionType` returns true only for the latter.
The change that TermParamRefs are Singletons caused a change how they are printed,
which this commit reverts.

We had before

    def f(x: T): x.type
    def f(x: T): x.type # M

Once TermParamRefs were singletons we got:

    def f(x: T): T (x)
    def f(x: T): x.M

With this commit we now get:

    def f(x: T): x.type
    def f(x: T): x.M
The missing case was discovered after inlining `implicitly`. It produced
a type mismatch in Ycheck after lambda lift.
Given a dependently typed function value like this one:

    def f: (x: C) => D => x.T => E

we did not propagate information about the subsequent types `D` and `x.T` to
the result type of the closure with parameter `(x: C)`. Doing so is a bit
tricky because of the dependency. But it's necessary to infer the
types of subsequent parameters.

Test case: eff-dependent.scala
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