-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add match type #4964
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
Add match type #4964
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, and thank you for opening this PR! 🎉
All contributors have signed the CLA, thank you! ❤️
Commit Messages
We want to keep history, but for that to actually be useful we have
some rules on how to format our commit messages (relevant xkcd).
Please stick to these guidelines for commit messages:
- Separate subject from body with a blank line
- When fixing an issue, start your commit message with
Fix #<ISSUE-NBR>:
- Limit the subject line to 72 characters
- Capitalize the subject line
- Do not end the subject line with a period
- Use the imperative mood in the subject line ("Add" instead of "Added")
- Wrap the body at 80 characters
- Use the body to explain what and why vs. how
adapted from https://chris.beams.io/posts/git-commit
Have an awesome day! ☀️
04ee39b
to
e1a2e61
Compare
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/4964/ to see the changes. Benchmarks is based on merging with master (1d24eaa) |
So, this looks good from an expressiveness and performance perspective. There are for me two open questions, which are interlinked: Do we have the right criterion for discarding a case in a match type? Right now this is implemented by comparing scrutinee with all abstract types and type parameters mapped to wildcards with pattern type. If that match fails, the case can be discarded and reduction proceeds to the following cases. The test seems a bit ad hoc, and there are conditions which it does not cover. For instance,
Here, we know that for all instances of How can match expressions get match types? We would like a typing rule stating when a match expression has a match type (to keep things simple, let's assume the match type will be given explicitly). What's the right rule? Again, the problem of negative information is the hard one to crack. How do we know that a type reduction clause cannot be fired for a scrutinee? EDIT: I think it would be good to put forward and discuss concrete type-systematic rules for both questions. I would be very interested in everyone's input on this. |
bb21dba
to
12dde86
Compare
12dde86
to
3f3a05c
Compare
Reduction of such types is not included in this commit
This way, it is always decidable whether a typed tree represents a type or a term. Previously, such literals could be used in type position, but were classified as terms.
Need to implement printing of match types from tasty first
This is necessary since we would otherwise get direct type recursion. We have to special case these abstract types later for subtyping. The commit means that ad-hoc fixes in cyclic checking and variance checking can be reverted (done also as part of this commit).
This is largely the result of a discussion we had on Friday with Guillaume, Sandro, Georg and Olivier. The rules for typing match expressions have evolved a bit from the state we discussed there.
369008d
to
9897d22
Compare
9897d22 contains a new informal spec for match types, which arose from our discussion at LAMP yesterday. The current implementation does not yet cover this new spec. |
- Don't reduce eagerly when forming matchtypes, as this could generate illegal cycles. - Add section to match-types.md how termination is handled.
d64717d
to
194d85d
Compare
This used to give an index-out-of-bounds error.
We cannot normalize match types and S types on creation since that ignores constraints that might change. So we normalize instead as part of `simplify`. The new scheme causes deeper subtype recursions than the old, so the Tuple23 part in tuples1 had to me moved to pos-deep-subtype.
... as mandated by new spec. This required some changes to Tuple.scala to avoid blow-up of repeated matches. Also, scrutinees of bottom types are now specialized to always yield NoType. This makes sense since corresponding match expressions would not match anything, either. It also prevents deep subtype recursions with Nothing as scrutinee.
@OlivierBlanvillain @gsps @AleksanderBG @smarter Checking that a match term has a match type is harder than first thought. 22e08f8 contains a revised description. |
Current status:
|
Unfortunately, things are not as easy as first hoped for...
22e08f8
to
5a8ac63
Compare
However, need non-overlapping patterns in order to discard a pattern.
There are still a lot of things to do, but many of those steps are independent from this PR. Since on the other hand this PR is by now the reference implementation for generic tuples I propose to work on merging it, so that we have something to base next steps on. Otherwise, we'd be stuck with the rewrite method based version of tuples, which is harder to fix, and there is less motivation to fix anything because rewrite methods are likely to be dropped. To be clear: The version of tuples in this PR still uses rewrite methods, but does not rely on the fact that rewrite methods can change types. So it should continue to work in a more standard inlining setting. |
@odersky Yesterday you mentioned bumping into cases that require type aliases and would be cumbersome with dependent method. Are those example part of this PR? As far as I can see all the type functions in If we try to keep type-level parts of sealed trait Tuple extends Any {
rewrite def ++(that: Tuple): Concat[this.type, that.type] = ...
rewrite def size: Size[this.type] = ...
}
object Tuple {
type Concat[+X <: Tuple, +Y <: Tuple] <: Tuple = X match {
case Unit => Y
case x1 *: xs1 => x1 *: Concat[xs1, Y]
}
type Size[+X] <: Int = X match {
case Unit => 0
case x *: xs => S[Size[xs]]
}
type Elem[+X <: Tuple, +N] = X match {
case x *: xs =>
N match {
case 0 => x
case S[n1] => Elem[xs, n1]
}
}
}
abstract sealed class NonEmptyTuple extends Tuple {
rewrite def apply(n: Int): Elem[this.type, n.type] = ...
} The same could be expressed with dependent methods: sealed trait Tuple extends Any {
rewrite def ++(that: Tuple): { concat(this, that) } = ...
rewrite def size: { size(this) } = ...
}
object Tuple {
dependent def concat(x: Tuple, y: Tuple): Tuple = x match {
case () => y
case x1 *: xs1 => x1 *: concat(xs1, y)
}
dependent def size(x: Tuple): Int = x match {
case () => 0
case x *: xs => size(xs) + 1
}
dependent def elem(x: Tuple, n: Int): Any = x match {
case x *: xs =>
n match {
case 0 => x
case n => elem(xs, n - 1)
}
}
}
abstract sealed class NonEmptyTuple extends Tuple {
rewrite def apply(n: Int): { elem(this, n) } = ...
} I think one clear advantage of the |
@OlivierBlanvillain I also think there are many similarities between the two approaches, and that there are advantages to both. But let me stress that match types are much more conservative than typeof. In fact we could probably do without match types altogether by adding the right type member structure to type Tuple
type Append[Ys]
}
class *: [+X, +Xs] extends Tuple {
type Append[Ys] = X *: Append[Xs, Ys]
}
class Unit extends Tuple {
type Append[Ys] = Ys
} I believe we can appeal to an encoding like this to argue soundness of match types. That's what the corresponding Haskell paper does. By contrast, typeof seems to need a lot more machinery. I still need to see a convincing argument why this would not roll in all the complexity of dependent types. If typeof is reduced in scope to be essentially the same as match types, then I would argue that match types are still clearer. typeof has the somewhat strange mixing of terms and types. It just feels less clear to say "the type of applying this function" than to state the type directly. And since |
Given that Append is defined as a type member, I guess this should be:
But then we have a projection on an abstract type, so how do we justify soundness ? |
@odersky I think there is some confusion about what are dependent types. In my view, dependent types are types that dependent on a value, and that's all there is to it. Other things like:
are orthogonal concerns to dependent types, and clearly out of scope of Scala. Having these things on top of dependent types is useful for theorem proving, but it's also perfectly fine to be conservative and stick the essence of dependent types: types that depend on value. So, according to this definition, match types clearly qualify as dependent types: type M[X] = X match {
case A => Int
case B => String
}
def m(x: Any): M[x.type] = ... Here the return type of
Can you give an example where TypeOf bring more complexity than match types? Things are reversed for me, I would like to see a convincing argument why match types are any simpler than TypeOf. I feel than increasing the gap between types and values makes things trickier, not simpler:
|
@OlivierBlanvillain would you consider TypeScript to be dependently typed? Because your example can be done in TypeScript: function m<T extends "A"|"B">(x: T): T extends "A" ? number : T extends "B" ? string : never {
return m(x)
}
let x: number = m("A") // ok
let y: number = m("B") // Type 'string' is not assignable to type 'number'.
let z: string = m("A") // Type 'number' is not assignable to type 'string'.
let w: string = m("B") // ok Sure, the TypeScript version uses a type parameter, but I really don't think this is an essential difference. Ultimately, what you match on in both the Scala and TypeScript versions is the type of what's passed into the function, not the value itself. |
@LPTK Yes, I would call that some form of dependent typing. I don't know much about the advanced type features of typescript, is it possible to do structural induction on types? We can also do all that in today in Scala thought implicits. Whether or not that enough to call TypeScript and Scala 2.x dependently typed languages is up for debate, but in the case of Scala I would certainly argue that implicits are enough to write dependently typed programs.
I agree, and this is also what's happening in the TypeOf version. Given the following definition: dependent def size(x: Tuple): Int = x match {
case () => 0
case x *: xs => size(xs) + 1
} When using |
@OlivierBlanvillain I like the TypeOf proposal; it seems very natural to me. In fact, it already exists (in a more primitive form) in other languages, such as C++ with the It seems to me that while TypeOf is similar to match types for type-level programming, it would additionally be useful in its own right (sans As we've talked about before, it'd allow to distinguish |
TypeBounds(readType(), readType()) | ||
val lo = readType() | ||
val hi = readType() | ||
if (lo.isMatch && (lo `eq` hi)) MatchAlias(lo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this check be performed in TypeBounds
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have a constructor in TypeBounds. Maybe we need a refactoring, I am not sure. Anyway at the moment MatchType as whole needs more work but this will be in future PRs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation of the match types LGTM
Add matching on types as a full-blown type.