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

Language request: Add forAll quantifier / Make MethodTypes first-class #2500

Closed
japgolly opened this issue May 22, 2017 · 55 comments
Closed

Comments

@japgolly
Copy link
Contributor

We already have forSome, how about adding a forAll quantifier?
Haskell, for instance, has this.

@odersky
Copy link
Contributor

odersky commented May 24, 2017

forSome has been deprecated 😄

@odersky odersky closed this as completed May 24, 2017
@notxcain
Copy link

@odersky anyway, why not add some means of universal quantification?

@odersky
Copy link
Contributor

odersky commented May 26, 2017

@notxcain Somebody has to study the theory and make sure it does not add an unsoundness hole as forSome did.

@Blaisorblade
Copy link
Contributor

Blaisorblade commented May 26, 2017

@odersky That's generally good, but unsoundness shouldn't be a danger here, because we already know how to encode first-class polymorphic values into a sound DOT—if we keep introduction and elimination explicit (unlike forSome), sth. like [A] => t (this is just abstract syntax). And we should keep intro and elims explicit (at least at first) because we know inference is very hard.

You presented in WadlerFest DOT the encoding from System F_<: to System D_<: and proved it type-preserving. I don't see an embedding of F_{<:>} into D_{<:>}, or a proof of meaning preservation—is that the problem?

For reference, the interesting rules for the type encoding are

translate(X) = x.Type
translate(forall X >: L <: U. T)` gives to `(x: {type Type >: translate(L) <: translate(U)}) => translate(T)

Also, you also suggested this ticket yourself here:

#1560 (comment)

@Blaisorblade
Copy link
Contributor

For others: In non-PL speak, I'm just saying that, it seems, we can already translate forAll X. T[X] into Scala with a standard encoding, where forAll X >: L <: U. T[X] becomes Forall[L, U, T] , with Forall defined as

trait Forall[L, U >: L, T[X >: L <: U]] {
  def apply[X >: L <: U]: T[X]
}

and a value [X]. t become new Forall[T] {def apply[X]: T[X] = t}.

BTW, in Forall the compiler seems to require eager checking against bad bounds (which is why I needed to bind U >: L instead of just U). Is this motivated by theory? I understand details (including soundness of what's already in the compiler) are ongoing work.

Forgot to mention: I can see some details with unclear foundations, but they seem to be very limited.

@japgolly
Copy link
Contributor Author

Awesome @Blaisorblade, I was thinking the same thing!

I can see some details with unclear foundations, but they seem to be very limited.

Can you elaborate? I'm very curious.

I was thinking that forall should already be possible using just D<: calculus. If D<: has been proven sound then I don't see where the problem would be. In the DOT calculus paper as well, from memory there's a demonstration of ∀A. A→A and a typed List Cons ∀A.

@Blaisorblade
Copy link
Contributor

I can see some details with unclear foundations, but they seem to be very limited.

Can you elaborate? I'm very curious.

"I don't see where the problem would be" is sadly not a soundness proof. It seems nobody saw problems with lower bounds—at least, people added them to Scala even if its first foundations (from ECOOP 2003!) didn't have them. And it took ~a decade to realize they'd be a problem (that's from OOPSLA 2014). This translation should work, but insisting "I don't see a problem" doesn't help.

What we don't know

So let me retract. Forall uses type constructors ("higher kinds"), which are nowadays a DOT extension that hasn't been proven sound. And that's a significant extension.

http://conf.researchr.org/event/scala-2016/scala-2016-implementing-higher-kinded-types-in-dotty and the paper has some more info on them.

For instance, I asked whether the restriction U >: L is needed in Forall. Without a soundness proof, we don't really know. And we know this question is hard.

What we do know

Without higher kinds, you could instead translate forAll X >: L <: U. T[X] to a structural refinement type Any { def apply[X >: L <: U]: T[X] }. Structural types are in DOT, but as you probably know they're tricky to compile on the JVM.

@Blaisorblade
Copy link
Contributor

OK, Nada confirmed the embedding is not the problem:

Yes, we can embed F_{<:>} in D_{<:>} (similar to what is done in Wadlerfest) [...] https://twitter.com/nadamin/status/868505892236427265

HKTs are a bigger one :-)

@sstucki
Copy link
Contributor

sstucki commented May 27, 2017

@Blaisorblade, quick comment on the U >: L restriction. I think your intuition about the restriction being necessary to enforce good bounds is correct. That being said, an extension of DOT with higher-order types would probably not be subject to that restriction (just as DOT is not) because it doesn't need to be algorithmic.

Also, I'm not sure to what degree an intrinsic forAll quantifier (as opposed to one encoded as a type operator) would be subject to such restrictions. My guess would be not at all, but I don't know enough about compiler internals to confidently answer that.

@japgolly
Copy link
Contributor Author

@Blaisorblade Thanks for the reply. I'm not sure though, why you're responding as if I said "I don't see a problem" in a vacuum. I followed with my rationale. Like I said, it seems to me that we have all the ingredients we need in D<:. D<: is proven sound therefore expansion of a forAll qualifier into D<:/DOT is also going to be sound. IIRC, the D<: calculus (or at least DOT) has type tags, types have bounds, type tags can be terms, appear in lambdas and become path dependent types again. I'm not by any means an expert at PL theory, but that's what's why it seems to me that this shouldn't be a problem.

A forAll qualifier that desugared into a { def apply[X >: L <: U]: T[X] } like you suggested above, would be fine. People are already writing code like that, it's just the syntax is clunky and verbose. If the downside is a non-optimal JVM representation that we're hoping might improve should future research yield a more advanced calculus, then that's just implementation detail, right? It'd still be nice to have a more concise syntax now.

@Blaisorblade
Copy link
Contributor

Blaisorblade commented May 28, 2017

I have more questions than when I started. But the experts (Martin, Sandro and Nada) agreed this might be trickier than it seems. Having an issue for it won't help—a volunteer for the formal work might.

BTW, we need a correct desugaring from DOT + forAll to DOT, not an encoding of F<:>—though we can use the same ideas.

Also, I'm not sure to what degree an intrinsic forAll quantifier (as opposed to one encoded as a type operator) would be subject to such restrictions. My guess would be not at all, but I don't know enough about compiler internals to confidently answer that.

To internalize a desugaring and prove the result sound, "just" lift the desugaring throughout your semantics, typing derivation, evaluation derivations, and so on. That is, treat the desugaring as a degenerate compiler and prove it correct. Luckily, the semantics of the original program follows very closely the semantics of its desugaring. So if bad bounds are fine with a desugared forAll, they should be fine with an internalized forAll.

Still, this is still quite a bit of work, where lots can go wrong.

EDIT: if forall X. T is supposed to be different (in the source language) from its desugaring, things get even trickier.

@TomasMikula
Copy link
Contributor

A desired property of F[X] forAll { type X } would be that it is a subtype of F[A], for any A, e.g.

F[X] forAll { type X }   <:   F[Int]   //   <:   F[X] forSome { type X }

Desugaring to Any { def apply[X]: T[X] } would not have this property.

@Blaisorblade
Copy link
Contributor

@TomasMikula True, but such a construct goes even farther away from DOT, and in a similar direction as forSome—making introduction and/or elimination forms implicit. I've found a description of some issues with forSome—and with the subtyping rule you propose, it seems I can adapt at least one of the problematic examples to forAll.

In which case, Martin's comment applies—maybe that's what he had in mind:

Somebody has to study the theory and make sure it does not add an unsoundness hole as forSome did.

Issues with forSome are mentioned by "Implementing Higher-Kinded Types in Dotty", which points to "Taming Wildcards in Java’s Type System". Which explains that Java wildcards can easily require, for instance, cyclic subtyping proofs. I wonder if similar examples apply. It seems I can construct one analogous to the one in Figure 1, with certain assumptions—to be sure, one must dualize (flip) some subtyping arrows in the examples to adapt them.

Generalizing your rule to bounded quantification, we have that F[X] forAll { type X >: L <: U } <: F[A] if A satisfies constraints on X, that is L <: A <: U. OK?

Let me try to adapt the example in latter paper's Figure 1. Let's define

type C >: List[List[X] forAll {type X <: C}]

then it seems we can try to ask if List[Y] forAll {type Y <: C} is a subtype of C. A proof attempt would proceed as follows:

List[Y] forAll {type Y <: C} <:? C //depends on
List[Y] forAll {type Y <: C} <:? List[List[X] forAll {type X <: C}] //depends on
List[X] forAll {type X <: C} <:? C  //oops, back to the same question again (up to alpha-renaming)!

I hope I didn't mess up some arrow—but luckily, all arrows are flipped relative to Figure 1 in the paper. That's what I'd expect since universals are dual to existentials.

This isn't a soundness hole. But even losing decidable typechecking would be trouble enough.

@TomasMikula
Copy link
Contributor

@Blaisorblade Thanks for digging in and adapting the example. IMO, in this and similar cases it would be OK to just detect divergence and fail.

I would say that F-bounded polymorphism is a more exotic feature than forAll, so should take more of the blame for divergence in this example.

I'm curious about the unsoundness introduced by forSome. Does anyone have an example handy?

@Blaisorblade
Copy link
Contributor

Memo: @sstucki found this paper on System F + your rule + another sensible one for symmetry, showing undecidability without F-bounds. Posting without further comment:
http://www.sciencedirect.com/science/article/pii/S0890540101929505

@TomasMikula
Copy link
Contributor

TomasMikula commented Jul 15, 2017

Related: a PR at kind-projector (EDIT: published as a separate plugin) to add somewhat more concise syntax to Scala 2.x for creating things like

trait Forall[F[_]] { def apply[X]: F[X] }

(@Blaisorblade thanks for the link! I just haven't got around to reading it yet.)

@Blaisorblade
Copy link
Contributor

@TomasMikula FWIW, I've only skimmed that paper, not read it in full. It's there for the record in case anybody wonders again or wants to do a PhD about this :-)

I'm curious about the unsoundness introduced by forSome. Does anyone have an example handy?

There's some discussion (about wildcards which are the same) linked here, though I don't have a good example yet: https://www.scala-lang.org/blog/2016/11/17/splash.html

@TomasMikula
Copy link
Contributor

So I read through just the introduction of that paper. The rule I proposed is rule (4) in the paper, also called instantiation:

  • (4)    ∀α.σ ⊑ σ(ρ/α)

and the other rule is rule (5)

  • (5)    σ ⊑ ∀α.σ,    whenever α does not occur free in σ.

The paper further considers rule

  • (6)    ∀α.(σ → τ ) ⊑ σ → ∀α.τ, provided α is not free in σ.

The main result is that subtyping (⊑), and thus type-checking, is undecidable, but the authors also note that

If the instantiation is restricted to monomorphic types ρ, i.e., types without any quantifiers in them, and the distributivity axiom (6) is removed, then, as shown in [7], the system becomes decidable.

[7] Odersky, M, and, Läufer, K. 1995, Putting type annotations to work, presented at the Newton Institute Workshop Advances in Type Systems for Computing, August 1995.

I would say that even this restricted form of subtyping would be quite useful in practice.

I realize that the paper talks about System F with subtyping, and that the decidability claim does not automatically apply to Scala. But note that this restriction already rules out your previous example, since you were using instantiation to a polymorphic type.

@liufengyun
Copy link
Contributor

As a document, following example seems to show that adding polymorphic class types will be unsound in the presence of mutable fields:

class Box[T] {
  var x: T
}

def foo(val b: forall T.Box[T]) = {
  b[Int].x = 3
  val a: String = b[String].x   // exception!
}

@TomasMikula
Copy link
Contributor

forall T. Box[T] seems uninhabited. Then foo could never be invoked.

@TomasMikula
Copy link
Contributor

If you change that to

class Box[T] {
  var x: List[T]
}

forall T. Box[T] becomes inhabited. So yes, vars pose a problem.

@Blaisorblade
Copy link
Contributor

Great point @liufengyun ! But that's indeed if forall T.Box[T] is inhabited. If it's not, where is it distinguished from forall T. ROBox[T]? That's with definition

class ROBox[T] {
  val x: T
}

@TomasMikula also why does List help? What's your introduction rule?

That's a a variant standard problem in Hindley-Milner (rather, Damas-Milner) polymorphism (see e.g. Pierce's TAPL, Sec. 22.7). I wonder if one can translate the value restriction (or some other fix) to this context somehow. It appears one would have to make type forall T.Box[T] uninhabited, but no clue how.

Memo: the imperative object calculus of Abadi and Cardelli has both polymorphic types and mutable methods (hence, I guess, fields), but avoids this problem. Can't find where. Reference I have: "A Step-indexed Semantics of Imperative Objects". Better reference: FOb<:μ in "A theory of objects" (which I don't have, I only found slides from http://lucacardelli.name/Talks/1997-08-04..15%20A%20Theory%20of%20Objects%20(Sydney%20Minicourse).pdf). I should check out the "Unsoundness of Method Extraction" slide.

@TomasMikula
Copy link
Contributor

TomasMikula commented Sep 20, 2017

With List, I can create an instance of Box[T] by initializing x to Nil. This is universally for all T, so universal generalization applies.

@TomasMikula
Copy link
Contributor

TomasMikula commented Sep 20, 2017

I think distinguishing immutable data types at the language level would help, and would be useful for other purposes, too.

@liufengyun
Copy link
Contributor

If we are going to introduce first-class polymorphic values, then I think forall X. Box[X] is inhabited:

def foo[X]: Box[X] = new Box[X]       // foo is one such inhabitant 

A semantic argument: given any type X, it's possible to create a value of the type Box[X].

But if we make foo an inhabitant, then my original example will be safe, as every type application will create a new object (think type abstraction in System F). If this is theoretically how it can be sound, then it will be a little surprising to Scala programmers -- as they may expect b[Int] and b[String] point to the same object.

@TomasMikula
Copy link
Contributor

The definition

class Box[T] {
  var x: T
}

does not compile.

error: class Box needs to be abstract, since variable x is not defined

If you make it abstract, you won't be able to call new Box[X].

@sstucki
Copy link
Contributor

sstucki commented Sep 21, 2017

I'll make an attempt to organize (and summarize) this thread a bit. There are a number of separate but related questions being discussed here.

  1. Should there be built-in support for universal quantification in Scala in the form of a dedicated forall syntax e.g. forall X. T?
  2. What should the semantics of such a forall construct be, in particular, is it just a shorthand/sugar similar to =>, and if yes, what for?
  3. Should there be dedicated introduction/elimination forms, and if yes, should these be shorthands/sugar as well?
  4. Should the (sub)typing rules be extended to allow special treatment of universally quantified types, e.g. adding (forall X. F[X] <: F[T]) for any A? (This seems to partly answer 2 negatively: forall is more than sugar, and affect 3 as well: elimination is no longer necessary)
  5. Should additional typing rules (as per 4) be restricted to certain classes of types, e.g. covariant types or "pure" types?

It seems to me that a satisfactory answer to point 2 is a must in oder to consider the introduction of built-in foralls (1).

Points 2-4 are intimately connected. In particular, adding a bit of syntactic sugar for universal quantification seems relatively benign to me, while extending the (sub)typing rules is quite a different story. For one, the generalization rule (forall X.F[X]) <: F[T] seems quite strange in a "Church-style" system like DOT (or System F) where instantiation of polymorphic terms t: forall X.T is done explicitly through type application t[U]: T[U]. With this new rule, t would already have type t: T[U]. This is a fundamental departure from the existing semantics of DOT, and it's unclear to me how something like this is best reconciled with the existing meta theory. There are multiple options, e.g. one could try to extend DOT with additional (sub)typing rules and develop a new meta theory. Or one could retain DOT as a core calculus into which a richer calculus with such rules would be elaborated. Again, this would likely have important consequences for semantics, e.g. whether t should be considered an unapplied "thunk" or a concrete instance of T[U] for arbitrary types U.

IMHO, we're getting a bit ahead of ourselves discussing issues like 5, at this point. Not least because we don't have a proper notion of "purity" of Scala types, let alone a way of enforcing it.

@TomasMikula
Copy link
Contributor

  1. What should the semantics of such a forall construct be, in particular, is it just a shorthand/sugar similar to =>, and if yes, what for?

I can see two possibilities of what forall X. T could be sugar for, if we want to be able to quantify over type constructors, not just types:

  1. Structural type { def apply[X]: T }.
  2. Given a kind-polymorphic type Forall (hopefully my pseudo-syntax is readable):
    trait Forall[κ: Kind, F: κ] { // κ is a kind variable, F is of kind κ
      def apply[A...: argKindsOf[κ]]: F[A...] // e.g. for κ = (* -> *) -> * -> *,
                                              // "A..." would be "a[_], b"
    }
    forall X. T would be a shorthand for Forall[* -> *, [X] => T].
    Obviously, I'm assuming kind-polymorphism here, which would be useful independently of forall.

But I would like to emphasize that there's a practical value to having forall X.T and T[U] be the same value (regardless of whether the rule (forall X.F[X]) <: F[T] is introduced for the purposes of type-checking/inference): being able to upcast List[forall X.F[X]] to List[F[Int]] at zero cost (even if such upcast is not done implicitly). This, however, seems incompatible with the sugar approach.

@Blaisorblade
Copy link
Contributor

First, can you expand on why we shouldn't reduce under type variable abstractions? Does it introduce new problems, that are not otherwise present?

Well, reducing under binders is dangerous whenever some variables have possibly-empty types. In Scala (or Agda), you better not reduce code that assumes, say, Int = String. Because of the "bad bounds" problems in the DOT literature, you can't rule out false assumptions (TL;DR. that one is clearly false, but with abstract types you can hide the falsity).
Same with forall [X >: Int <: String] or exists [X >: Int <: String]. Since terms did reduce under the binder for exists (well, forSome) because there is no binder in the term syntax (as mentioned by the Scala'16 paper), this is probably a cause of unsoundness for forSome.

I don't quite see the distinction. Is ((x: X) => x) anything different than new Function1[X, X] { def apply(x: X): X = x }?

I was looking at a calculus where functions are built-in, but the latter should work as well because the created object is still pure. I'd still prefer to look at the calculus side first—maybe we can extend the calculus but have to change the encodings.

Overall, wouldn't it be fine to restrict universal quantification to only immutable data types?
For a mutable Box[T], one couldn't have forall X. Box[X], but one would have to explicitly work with a thunk forall X. () => Box[X].

I'm fine with that restriction, also by the analogy with ML modules.
But it'd be nice to know what's the exact restriction needed for soundness — why is () => Box[X] fine but Box[X] OK?

If upcasting may require instantiation, [...]

Agreed, not sure how to balance things nicely. I agree that forcing the restriction to forall X. () => Box[X] avoids the box and the need for application. This means also allowing reduction under type-variable binders, with the serious problem above.

In Haskell, instead, universals have instead System F semantics (with abstraction erased where the semantics allow). It seems that scenario (with no subtyping rule) should be sufficient for Scala FP libraries—I mean, that's close to what people encode anyway.

@TomasMikula
Copy link
Contributor

@Blaisorblade Thanks for pointing out the problem with bad bounds, I didn't think about that (I didn't really consider bounded quantification at all). Though I believe that restrictions could be introduced that would prevent bad bounds at runtime (such as forall X >: L <: U. T would not be allowed in general, but class Foo[A >: L <: U] { ... forall X >: L <: U. T ... } would be OK; or even start with allowing just one-sided bounds). I wonder if there are other problems with reduction under type variable binders.

@Blaisorblade
Copy link
Contributor

(such as forall X >: L <: U. T would not be allowed in general, but class Foo[A >: L <: U] { ... forall X >: L <: U. T ... } would be OK; or even start with allowing just one-sided bounds)

Uh, that class example is pretty interesting: in a context that only runs when L <: U, you want to assume that L <: U. There can still be subtle issues though if more assumptions are added.

Also, the problem isn't just about type variable binders — binding L <:< U can be as dangerous.
What I know on reduction under binders, in extreme generality (written as a response to the OOPSLA'14 DOT paper):
https://github.com/Blaisorblade/Agda-playground/blob/master/SoundnessOpenTermsXiRule.agda

In that context, the evidence of assumptions is reified through (proof) terms, and combinators to manipulate it (say, make deductions from assumptions). You can use implicits to hide those terms, but the terms are still there in the internal syntax.

Back to your example where forall X >: L <: U. T is forbidden but class Foo[A >: L <: U] { ... forall X >: L <: U. T ... }: This can be understood in terms of reified evidence. The compiler does looks up implicit evidence for L <: U, in forall X >: L <: U. T this fails, but in class Foo[A >: L <: U] { ... forall X >: L <: U. T ... } this succeeds.

This isn't necessarily useful directly, but it's useful to understand things.

@TomasMikula
Copy link
Contributor

Implicit evidence is a nice tool to think about it, thanks! I think you are implying that type-variable binders can be reduced to value-level variable binders, at least conceptually.

If any evidence ev1: L <: U is as good as any other ev2: L <: U for all purposes, then even though reduction under binders is dangerous in general, we can safely reduce under binder of ev2 if there is ev1 in the outer scope, by substituting ev1 for all occurrences of ev2.

@sstucki
Copy link
Contributor

sstucki commented Sep 22, 2017

I wonder if there are other problems with reduction under type variable binders.

There's a discussion about related issues in #2887.

Also, the problem isn't just about type variable binders — binding L <:< U can be as dangerous.

Really? In what sense is this dangerous? Do you have an example @Blaisorblade?

It seems to me that working with explicit evidence of L <:< U is generally safe precisely because you cannot reflect such evidence back into the subtyping relation. The type L <:< U isn't magic: the type system has no idea it's supposed to be subtyping evidence. Instead you have to apply the evidence to an instance of L to get an instance of U (or from 2.13 on you could use the more powerful substitute method instead).

def cast1(x: Int)(         y: Int <:< String): String = x      // does not typecheck
def cast2(x: Int)(         y: Int <:< String): String = y(x)   // safe: y is abstract   
def cast3(x: Int)(implicit y: Int <:< String): String = x      // safe: equivalent to cast1   

The use of abstract evidence in the form of a variable y: L <:< U is delayed at runtime until y has been instantiated to a concrete, closed proof term, which will never happen if the bounds of L <:< U are absurd. In the above example, we couldn't reduce y(x) even if we wanted to because y is just a variable.

Isn't this precisely what you propose in your Agda snippet? By using terms t: L <:< U you achieve exactly that "evidence of assumptions is reified through (proof) terms, and combinators to manipulate it". What am I missing?

BTW. I feel like we're really digressing from the issue here. Maybe discussions about bad bounds should go into a separate issue (e.g. #2887).

@Blaisorblade
Copy link
Contributor

If any evidence ev1: L <: U is as good as any other ev2: L <: U for all purposes, then even though reduction under binders is dangerous in general, we can safely reduce under binder of ev2 if there is ev1 in the outer scope, by substituting ev1 for all occurrences of ev2.

@TomasMikula I agree. Depending on the exact syntax and surrounding rules, though, you risk trouble (example below at the end). Current rules for <:< are certainly fine.

Maybe discussions about bad bounds should go into a separate issue (e.g. #2887).

Maybe — this was about bad bounds as related to forall—and discussing a closed issue in an open one isn't ideal. I also still agree with Odersky that this extension is tricky enough to need a proper proof.

Really? In what sense is this dangerous? Do you have an example @Blaisorblade?

I'm not implying actual bugs, just warning about dangers in future rules for forall. Variants of Scala.js-style rewritings can be unsound in (unobvious?) ways.

@TomasMikula wrote "reduction under type variable binders", and I wanted to point out this is just about reduction under any binders. The Agda snippet is already an example, since it involves reducing cast under value binder λ (prf : ℕ ≡ Bool) →.
And you can adapt the example to Scala with <:<. Arguably, the rules needed for unsound results are less natural in Scala, but some of those are variants of rules in #2887.

  1. In particular, If L <: X <: U0 is safe, and allows you proving L <:< X, but you add assumptions on X (say, X <:< U for some arbitrary U), you can produce proof terms for empty types, say with combinator transitive[L, X, U](a: L <:< X)(b: X <:< U): L <:< U.

  2. Regarding Scala.js-style rewritings as in IndexOutOfBoundsException when attempting to reduce ill-kinded types #2887: Take (x: L <:< U) => (y: L) => x.apply(y): L => U: rewriting x.apply(y) as y.asInstanceOf[U] is still safe per se, but it isn't safe to lift y.asInstanceOf[U] outside of the binding for x (as GHC might do, if this function occurs inside another).

@sstucki
Copy link
Contributor

sstucki commented Sep 25, 2017

@TomasMikula wrote "reduction under type variable binders", and I wanted to point out this is just about reduction under any binders.

I think there's some risk of confusion here, so let me try to clarify this.

  1. Reduction under either term or type variable binders is problematic in general because type members can have "bad bounds". However, scalac/Dotty do not (AFAIK) reduce terms or types under term variable binders during compile time, nor does the JVM perform such reductions at runtime, nor do the operational semantics of DOT permit such reductions in theory.
  2. Reduction under type variable binders may happen during compile time, which can cause problems (as discussed in IndexOutOfBoundsException when attempting to reduce ill-kinded types #2887).

However, reductions under binders of the form x: L <:< U should never be problematic: while you can construct an instance of L <:< U whenever L <: U, you cannot -- to the best of my knowledge -- do the converse, i.e. the compiler will never assume L <: U just because it has a term of type L <:< U in scope. In other words, binders of the form x: L <:< U do not change the subtyping relation, you have to eliminate them explicitly using apply. Hence, such binders will neither cause run-time exceptions (we don't reduce under binders at run time anyway) nor compile-time exceptions (because they don't change the subtyping relation). Again, this is all to the best of my knowledge, but if you have a counter example, I'd be intrigued!

The Agda snippet is already an example, since it involves reducing cast under value binder λ (prf : ℕ ≡ Bool) →.

As you point out yourself (and as pointed out in the paper you cite) one cannot reduce cast under that binder, because prf is abstract (i.e. a variable), yet cast is defined by pattern matching and needs a concrete instance refl: ℕ ≡ Bool to be reduced. Such an instance does not exist, so the example is safe. More generally, proofs of type A ≡ B need to be explicitly eliminated (using pattern matching) in Agda, and similarly proofs of A <:< B need to be explicitly eliminated in Scala.

And you can adapt the example to Scala with <:<.

I'd love to see an actual example.

Again, constructing instances of L <:< U with bogus bounds (e.g. from L <: X <: U is not problematic per se because they can never be eliminated. The "Scala.js-style rewritings" you mention are not about <:< types, as far as I understand. I believe @sjrd referred to the method <:< defined in the compiler API, which is used to call the sub-typing algorithm of the compiler. @sjrd could you please clarify this?

@sjrd
Copy link
Member

sjrd commented Sep 25, 2017

@sstucki Yes you are right that I refer more to the compiler method <:< than Predef.<:<. However, to be even more precise, I am referring to the method isSubtype of the Scala.js linker. Essentially, it plays the same role as the Scala compiler method <:< but at link-time.

@Blaisorblade
Copy link
Contributor

However, scalac/Dotty do not (AFAIK) reduce terms or types under term variable binders during compile time

@sstucki again, we're in vociferous ("violent") agreement. I'm explaining why the rules must be like they are.
I hope that, after this discussion, the potential dangers for new reduction rules are clear.

@LukaJCB
Copy link

LukaJCB commented Jun 14, 2018

I think this feature would be an absolutely amazing complement to implicit function types. It would allow us to fully have first class polymorphic functions.

Right now, when you want to abstract over anything that requires a type class for some operation, you're forced to box all the things.
This gets a bit better with implicit function types, but you'll still need to specialize.
For example, consider this simple map definition:

def map[F[_]: Functor, A, B](fa: F[A])(f: A => B): F[B]

With implicit function types I can now do this:

def map[F[_], A, B](fa: F[A])(f: A => B): (implicit Functor[F]) => F[B]

This is better, but I'm still stuck with def. This means, that when I want to pass this to a function, we'll have to specialize all the type parameters and won't be able to call the passed function with different types. Now with Rank-N-Types we could do something like this:

val map: forAll { F[_], A, B } (implicit Functor[F]) => F[A] => (A => B) => F[B]

And then we've avoided the problem easily.

Now I realize this is a rather contrived example, but I have a bunch of other situations where I desperately needed this and I believe this feature would really go a long great with implicit function types.
I think @smarter told me that he thought about working on something like this? Is something like this still being considered for Dotty?
Would love to hear an update :)

@LPTK
Copy link
Contributor

LPTK commented Jun 14, 2018

@LukaJCB here's a trick to do it today in Dotty:

val map: implicit (A:Type,B:Type,F:Type1) => (F.T[A.T]) => (A.T => B.T) =>
         implicit (Functor[F.T])          => F.T[B.T]
  = x => f => implicit fun => fun.map(x)(f)

where:

trait Type { type T }
object Type { implicit def Type[S]: Type { type T = S } = new Type { type T = S } }

trait Type1 { type T[_] }
object Type1 { implicit def Type1[S[_]]: Type1 { type T[A] = S[A] } = new Type1 { type T[A] = S[A] } }

trait Functor[F[_]] { def map[A,B](x: F[A])(f: A => B): F[B] }
object Functor { implicit object listFun extends Functor[List] { def map[A,B](ls: List[A])(f: A => B) = ls.map(f) } }

You use it like so:

val ls = List(1,2,3)
println(map.apply.apply(ls)(_.toString))

The ugly .apply.apply is due to #1260.

However, last I tried, when compiling the usage above I got error:

-- Error: /tmp/scastie6141179723275680546/src/main/scala/main.scala:38:43 ------
   |    println(map.apply.apply(ls)(_.toString))
   |                                           ^
   |no implicit argument of type Functor[List] was found for parameter of implicit Functor[List] => List[String]

When calling map.apply.apply(ls)(_.toString)(listFun:Functor[List]) I get the error found: Functor[List]; required: Functor[List] so I assume it's a current Dotty bug. Unless someone points me to a mistake I've made or to an existing issue, I'm going to open one for it.

@LPTK
Copy link
Contributor

LPTK commented Jun 14, 2018

Oops! Two tentative reorderings of the implicit parameters gave two different compiler crashes:

  val map: implicit (A:Type,B:Type,F:Type1) => implicit (Functor[F.T]) => (F.T[A.T]) => (A.T => B.T) => F.T[B.T] =
  	implicit fun => x => f => fun.map(x)(f)
java.lang.IndexOutOfBoundsException: 2
  val map: implicit (A:Type,B:Type,F:Type1,fun:Functor[F.T]) => (F.T[A.T]) => (A.T => B.T) => F.T[B.T] =
  	implicit (A:Type,B:Type,F:Type1,fun:Functor[F.T]) => x => f => fun.map(x)(f)
java.lang.Error: internal error: cannot turn method type (implicit A: Type, B: Type, F: Type1, fun: Functor[F.T]): 
  <error missing parameter type
The argument types of an anonymous function must be fully known. (SLS 8.5)
Expected type: ?
Missing type for parameter x>
 => Any => Any => F.T[Any] into closure
because it has internal parameter dependencies,
position = <1409..1409>, raw type = MethodType(List(A, B, F, fun), ...

@milessabin
Copy link
Contributor

@LPTK there are tricks to do it in Scala 2 too ... for Scala 3 we should be aiming to do away with the tricks and make this idiom first class.

What I'd like to see is a direct correspondence between method types (polymorphic, implicit, dependent) and function types ... we're tantalizingly close now, with this being the last piece of the picture.

I don't particularly like the proposed syntax however. We already have universal quantification for method types, so why not simply reuse it for function types? Taking @LukaJCB's example, that would allow us to write,

val map: [F[_], A, B](implicit Functor[F]) => F[A] => (A => B) => F[B] = ...

@LPTK
Copy link
Contributor

LPTK commented Jun 14, 2018

@milessabin do you know why it's not already supported in Scala? After all, the compiler does have an internal representation of method types, they're just not truly first class (not denotable). On the bytecode side, couldn't these just erase to normal FunctionN classes?

@milessabin
Copy link
Contributor

@LPTK the drag of a mature codebase and because noone's had time to do it?

Yes, I believe that erasure is our friend here.

@Blaisorblade
Copy link
Contributor

@LukaJCB @LPTK Yes, making MethodTypes first-class as @milessabin proposes would be lovely. But time is not the only issue — I think making MethodTypes first-class as @milessabin proposes seems potentially easier to design, but what @TomasMikula proposed seems to involve substantial novel research on both soundness and implementation. Based on the experience with SI-2712, settling on the simpler choice might simplify things.

Why are they different?

  1. With MethodTypes, [X]F[X] becomes internally Function0[F[_]] and then after erasure Function0 (and this is hidden from the typechecker). That is, method types are just functions, like in Haskell. This is just the standard and well-understood universal quantification in System F, and also should work fine with effects. It'd still be good to formalize this carefully.
  2. @TomasMikula instead proposes polymorphic values: inhabitants of forall X. F[X] would also inhabit F[T] for arbitrary T, instead of meaning Function0[F[_]]. This is less usual, and not even Haskell does it. And as discussed above this is unsound if F[T] contains mutable state, so requires a (yet nonexistent) effect system. More open questions are covered above.
  3. Other variants are probably considered above.

What's the advantage of 2? Quoting @TomasMikula:

But I would like to emphasize that there's a practical value to having forall X.T and T[U] be the same value (regardless of whether the rule (forall X.F[X]) <: F[T] is introduced for the purposes of type-checking/inference): being able to upcast List[forall X.F[X]] to List[F[Int]] at zero cost (even if such upcast is not done implicitly). This, however, seems incompatible with the sugar approach.

Even if we pick MethodTypes, formal work might be required, and Martin in #1560 wrote "In theory this commit paves the way to allow polymorphic types as value types [...] Enabling this functionality should be a SIP and separate PR." And then the bottleneck would still be time/people — right now only Martin and @smarter are proficient on Dotty's typer.

@LPTK
Copy link
Contributor

LPTK commented Jun 14, 2018

@Blaisorblade

With MethodTypes, [X]F[X] becomes internally Function0[F[_]]

Isn't the latter equivalent to Function0[F[X] forSome { type X }], so very different from the former? But I think I see where you're getting at, and I agree.

Indeed, the forall type approach is widely different (and seems much more troublesome) than merely making method types first class. And forall doesn't even seem that useful, as there are other ways of doing the kind of zero-cost List[F[X]] conversions mentioned above (as we discussed before).

@Blaisorblade
Copy link
Contributor

Nitpick:

And forall doesn't even seem that useful, as there are other ways of doing the kind of zero-cost List[F[X]] conversions mentioned above (as we discussed before).

We discussed a different side: here List[forall X.F[X]] is never a List[F[Int]], so you can ask for List[forall X.F[X]] <:< List[F[Int]], but nobody can provide it soundly.

@Blaisorblade
Copy link
Contributor

Reopening for the MethodType proposal.

@Blaisorblade Blaisorblade reopened this Jun 15, 2018
@Blaisorblade Blaisorblade changed the title Language request: Add forAll quantifier Language request: Add forAll quantifier / Make MethodTypes first-class Jun 15, 2018
@LPTK
Copy link
Contributor

LPTK commented Jun 15, 2018

@Blaisorblade I haven't followed the whole discussion (so apologies if this was already mentioned earlier in the thread), but what I was getting at is that if List[forall X.F[X]] can be up-casted at no cost into List[F[Int]] (as @TomasMikula proposes in the message you quoted), then it seems to me that it could also be expressed/encoded without loss of generality as a List[FX] for some existential FX along with a method to Liskov-substitute FX with any F[T].

In other words, encode List[forall X.F[X]] as:

{ type FX; val self: List[FX]; def ev[T]: FX <:< F[T]  }

BTW, shouldn't a different issue be created for making method types first class, with a better name and more on-point discussion?

@Blaisorblade
Copy link
Contributor

Blaisorblade commented Jun 15, 2018

@LPTK Oh, thanks for explaining the connection. Yes, that's nice.

BTW, shouldn't a different issue be created for making method types first class, with a better name and more on-point discussion?

Created #4670, closing this one.
(EDIT: that issue is now lampepfl/dotty-feature-requests#1).

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

No branches or pull requests