Conversation
00cf0ac to
767d042
Compare
|
I just remembered more of the operator overloads need to be generated |
547dd3c to
2412122
Compare
2412122 to
735bbc7
Compare
|
I understand that this is a massive pull request, but sumtypes/matching is a big new feature, and one we need to keep up with even stodgy old conservative languages like C#. If we can pile into this and get multiple people to review it should be manageable. |
|
For reference, this design reduces to lowering to structs (literals + declarations), and ternary expressions (match). Overall I'm very impressed with the code quality MiMo V2.5 came up with, I couldn't have done it better (overall). |
|
I don't see what the point is. The syntax isn't an improvement over the library solution, and I don't see any new functionality being offered. Is all this just to save an import? |
|
Match expression support guard expressions and there are both named and unnamed variants. |
The latter can be supported in the library solution. The former just looks awkward and inconsistent with the rest of the language. Why does this need a fancy new syntax that we can't use anywhere else? |
|
If you want to extend match expressions to other types that is fine. But I am not working on that right now, and the scope is sufficiently large. Just because something can be done in library does not mean that it is the correct place to put it. Sumtypes are a primitive of data representation, same as tuples. Much more so than a map or a dynamic array. |
|
Oh and another thing, match functions can't actually be safe if the argument is by-ref. The match expressions, due to not using function calls can be analyzed with a borrow checker, and therefore the variables can be by-ref. Which is a pretty massive upgrade. |
It doesn't mean that it isn't the correct place to put it, either. And bitfields are far better than the C garbage that's supposedly replacing them. I used to think D's greatest strength was its metaprogramming, but it's doomed to be deficient, isn't it? |
Which can be fixed with a simple linting rule, which I implemented but wasn't accepted.
Yes, but not because of the design or implementation of metaprogramming stuff. The core problem is overall compiler architecture wasn't designed to solve cyclicity in analysis, which prevents effects analysis, and the behaviour isn't correct. On the flip side, you get fast compile times, so it isn't all bad. |
735bbc7 to
01e98a9
Compare
DMD perf check
Breakdown — compile hello.d
Breakdown — compile Phobos+38.4 M instructions: frontend +39.3 M (+1.08%), codegen -0.9 M (-0.06%)
All measurements
a8f423c vs merge-base d4c5f7a · about these metrics |
3753bec to
2edcc4e
Compare
C# and Swift don't support sumtypes so they are not relevant. I don't see a way to make the existing switch statements be turned into expressions. They operate on multiple fundamentally different principles. While they appear similar, they are not similar enough to make one do the other's job. Statement vs expression. Constant vs type with optional runtime guard. |
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/enumerations Swift enums are sumtypes and use |
Why? No other language with sum types and pattern matching does this. They all require that the arms unify to a single type. This will lead to bugs like: // User intended both arms to return an integer ID:
auto id = val.match!(
case User u => u.id, // int
case Admin a => a.uuid // string
);
// Instead of a compile error, 'id' silently becomes `__sumtype(int | string)`.
// The error is only discovered 50 lines later when trying to do `id + 1`.It doesn't seem worth the fluent method chaining. And what happens in this case? auto traverse(Node n) {
return n.match!(
case Leaf l => l.value, // int
case Branch b => traverse(b.left) // recurse
);
}Naively, it seems to me that this match will synthesize an infinitely recursive sum type. Some bikeshedding:Why invent new keywords like __sumtype and .match (the latter of which will be the first time that D has introduced a postfix keyword)? Can't we reuse union and/or enum, and introduce switch expressions, as @xoxorwr suggested? enum union Option(T)
{
case Some(T),
case None,
}
Option!int maybeInt;
auto n = switch (maybeInt) {
case Some(int n) => n, // int
case None => throw new Exception(...), // noreturn
}
// noreturn <: int therefore the expression unifies to int
|
Have you seen this wonderful library feature that D has, called input ranges?
EDIT: I put it into the wrong file, it does not compile.
As for why not what you came up with syntax wise, that is a much newer syntax. |
What do those have to do with sum types? Do you have a concrete example?
Contextual or non contextual or whatever, D has never before had a control flow construct that is spelled
I'm not talking about the primitive; I agree it's useful. I'm talking about the __sumtype(t1 | t2)/sumtype(t1 | t2) syntax. Why introduce a new keyword and specialized syntax when we can already easily add something like enum union that closely matches what exists in most other modern languages today? |
It allows you to chain both input ranges and match expressions together. Both are data processing transfer functions.
Right, it's not valid syntax which is why I prefer it over Not supporting it as an expression, and trying to force it into the role of a statement does mean that we lose to ability to chain them, like it is possible to do in library code. However I do want to emphasize, its not a switch statement in its current form. They do not overlap in their features even though it appears that they may.
Oh that particular bit of syntax is likely to be removed in favor of the other one. I introduced it originally for mixing with tuples, but because it's going to explode in symbols if people use it, I want to remove it. Waiting on borrow checker PR to be merged before I finish off this one. |
|
Over chaining, it is possible to have polymorphic expressions (aka. lazy unification). No invisible type instantiations as per D's conventional style. Also makes optimization easier. val.match {
(User u) => u.id, // int
(Admin a) => a.uuid // string
} // int or string, but not automatically __sumtype
// Error: can't unify two arms
auto id = val.match {
(User u) => u.id,
(Admin a) => a.uuid
};
// Generate both arms automatically
class Role
{
this(int) { ...; }
this(string) { ...; }
}
auto role = new Role(val.match {
(User u) => u.id,
(Admin a) => a.uuid
});
// Fine, IdType has both constructors
alias IdType = __sumtype(int | string);
IdType id = val.match {
(User u) => u.id,
(Admin a) => a.uuid
};
// Sure, with 200% template bloat
string id = to!string(val.match {
(User u) => u.id,
(Admin a) => a.uuid
});This might be more manageable than unconditional chaining. If there is a chance to make |
|
I'm not tieing matching into variable declarations, I want to get away from the entire idea of it acting as a statement. A few features that may not be known:
Hmm it does look that evaluating out to a sumtype isn't supported atm, I know I did do it so it must've got lost at some point. Thats another thing that needs fixing. |
myRangeOfS1s.map!(s => s.match { ... }).filter!(...)...Unless you've got a more specific example/use case in mind. |
That's certainly getting closer to what I have in mind. But think of match expressions as being a form of narrowing. int result = source.match {
(string) => X,
(int) => Y,
(bool) => Y
}.match {
(int) => Z,
(string) => U
};Instead of ending there, the result type may instead be something compatible with input ranges like arrays. source.match {
(string[]) => X,
(int[]) => Y,
(bool[]) => Y
}.match {
(int[]) => Z,
(string[]) => U
}.filter!().map!().each!(
(v) => v.match {
}
); |
I get it, but my question is why? No other language with pattern matching that I'm aware of does this, and I fail to see the value - especially when it'll likely be confusing for most programmers and has the potential to cause issues, and if you want pipelining you can very easily put your match inside a map() or a .filter(), as I showed. It seems to pointlessly introduce complexity for no real tangible benefit. |
|
Speaking in terms of range transformation, lazy unification is more capable because it also widens: class Authenticator
{
this(int);
this(string);
this(Certificate);
}
auto auth = new Authenticator(val.match {
(User u) => u.cred.match {
(int pinCode) => pinCode,
(string password) => password
},
(Admin a) => a.certificate
});It also interoperates with existing library solutions easily, just do |
I don't think your example will work. The result of the match expression will be |
I was proposing a structural typing alternative like Crystal, where sumtypes are automatically flattened. Rikki's proposal would give unflattened |
Regardless, I'm pretty sure my point still stands. |
|
I took a stab at my own (LLM-driven, very rough) implementation: #23744 |
2d40c84 to
b707d4e
Compare
b707d4e to
a8f423c
Compare
| assert(rt.tag == 1); | ||
| rt.match | ||
| { | ||
| (int x) => 0, (ref y) => (y = false, 0) // catch-all ref on bool variant: y is ref bool |
There was a problem hiding this comment.
Why does (y = false, 0) compile? I thought comma expression was an error.
There was a problem hiding this comment.
There was a problem hiding this comment.
It may parse but it should cause a semantic error:
comma.d(6): Error: using the result of a comma expression is not allowed
auto x = (y = false, 0);
So why doesn't this line?
There was a problem hiding this comment.
Because it isn't in a match expression.
There was a problem hiding this comment.
I don't know. It wasn't something I chose to support.
I expect it is some case that isn't disallowed.
There was a problem hiding this comment.
@Herringway I figured it out - using the result of a comma expression is an error, but the match expression result is not used. So actually the compiler should give "Error: 0 has no effect".
Will update later.