In a discussion with Aly on discord, the following bit of code came up and compiled:
class A[C^](c1: C)
I find it very hard to understand what this code means - in fact, it feels like this shouldn’t compile, because we’re using a capset variable in place of a concrete type.
Can anyone explain how to understand what type c1 is?
I must admit I have no real understanding of capture sets, but in Scala a “decoration” on a type without the type usually means that Any is being decorated
For example {val x: Int} =:= Any{val x: Int}
The C^ in unbound type param position I guess means that you require c1 to be a capability. Reading from the doc pages here
“The type of a capability must be a capturing type with a non-empty capture set”
The first expression does not compile because c1 does not have a non-empty capture set. The second expresion compiles becasue it is of the “fallback” type CapSet
The type of a capability must be a capturing type with a non-empty capture set
This is where the vocabulary gets a little confusing (for me), by the way - C is not a capability in the sense of inputs to context functions, and I get confused about which kind of capability we’re talking about - the ones from capture sets, or the ones for actual capabilities?
This has led to people asking me surely A^{Rand} is the same as Rand ?-> A…
But to your answer: so C^ here is basically C <: CapSet. This I had gathered through @Aly 's experiences (and the fact that it’s stated explicitly in the doc once I figured out how to read it). But I would, perhaps naively, expect [C^] to mean that C only appears as a capture set, and to fail to compile if it’s used as a type, and [C <: CapSet] to express the subtyping constraint. Simply for clarity for users.
So, in my head:
class A[C^](c: C) // does NOT compile
class A[C <: CapSet](c: Foo^C) // does NOT compile
class A[C <: CapSet](c: C) // does compile
class A[C^](c: Foo^C) // does compile
I’m not trying to imply that it’s how it should be, merely how it was in my mental model and how I end up getting confused, again.
I guess you may be confusing the type and term level. A capture set contains terms that must be capabilties (so the are of a type that is a sub-type of CapSet). So C is not a capabilty (it is a type with a non-empty capture set), but the term c1 must be a capability as you said it is of type C^.
(I hope I’m right… not 100% sure)
Mmm… this is how I understand it, but I’m very open to the idea of being wrong:
[C^] is a capture set, which is conceptually not a type, but something you use to decorate types with (although yes, internally, it is a subtype of CapSet).
a capture set is a set of terms, which are called capabilities, and may or may not be actual capabilities in the sense of SharedCapability and its siblings. It’s really a set of tracked terms.
C, in our example (that is, [C^]), is not, I think, a type with a non-empty capture set, but an actual capture set - what the documentation refers to as capability polymorphism. And my original question is about how confused I am that it can also be used as a type.
Now your explanation makes sense: if I understand correctly, it’s desugared to C <: CapSet, which is a type. This explains why the compiler accepts it where a type is expected. It’s just really confusing (for me, obviously, but I expect also for people not intimately familiar with how this all works).
I’m not too surprised by this: you’re merely saying that B is parametric on C, and C is tracked. Unless I’m missing something obvious again, this is only tangentially related to what we’re discussing.
The difference between B and A is that with class A[C^](c1: C) you require that what is bound to C when applied is a type that has a non-empty capture set (i.e a “capability”).
But I would think that the C^ in param position would require the same…
Really?! that is not at all what I understood from the documentation. I honestly, genuinely thought A[C^] meant that C is a capture set, so that you can parameterise over the capture set of your inputs - a little like @Odersky explained here.
With my (incorrect?) mental model, it makes perfect sense that B(new Impure) compiles. B’s constructor takes any tracking type. But since you can pass non-tracking types where tracking types are expected, you really can pass any type to the constructor.
Perhaps the difference is between “must capture” and “can capture”. So C^ in unbound type param position perhaps means “must capture” and C^ in bound value type position means “can capture”.
When you write f[C^] this is treated as a type parameter with CapSet as upper and lower bound
Yeah I ended up working that out in the end, between the documentation and the help of @Sporarum and @bjornregnell
From a user’s perspective, this is (quite clearly) a little confusing - it feels like mixing apples and oranges. I assumed it was done this way because it simplified the underlying implementation.
Perhaps the difference is between “must capture” and “can capture”. So C^ in unbound type param position perhaps means “must capture” and C^ in bound value type position means “can capture”.
I genuinely don’t think so, unless something drastic (other than the name of cap, wink wink) changed recently:
C^ as a type parameter means Cis a capture set.
C^ as a type means C is tracking - has a capture set.
I will admit that I’m not fully certain what you mean by unbound type param position and bound value position here and am assuming the former is what I mean with as a type parameter and the latter as a type, but if not, then I have not at all understood what you meant.