-
Notifications
You must be signed in to change notification settings - Fork 205
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
Patterns and related features #546
Comments
A great digest 👍 In the meanwhile, there is a young library that generates sum-types (-alike classes) for you: https://github.com/werediver/sum_types.dart (is this shameless plug an off-topic?) |
Thanks for mentioning this! This is a good reference for ensuring that whatever we add to the language covers the functionality that users want to see. |
If existing packages are interesting to see, here's a bunch of them: |
Since you're collecting information, I'd like to mention my "proposal" where I suggested how you could combine pattern matching, data classes, union types, and typedef into a coherent whole while still making each of those concepts useful individually. The result is easier to use/understand and more flexible than algebraic data types. This is what we want. So, pattern matching should IMHO focus on these orthogonal concepts of data classes (for destructuring) and union types (for exhaustiveness). Also, before I found this issue I already commented here on why algebraic data types result in a worse developer experience vs. union types. TL/DR: with algebraic data types, pattern matching forces you to check for cases at runtime (and raise exceptions) although they should be forbidden at compile-time! I suppose that's also relevant to this issue. |
@wkornewald The untagged unions concept sounds nice, but it doesn't sound like a replacement to the algebraic data types. They can totally coexist and serve different needs. (not clear which topic is more relevant, so put in both #546 and #83) |
I'd say it's more relevant to #83, so let's discuss over there. |
I haven't seen lenses mentioned anywhere before when data classes are being discussed. I strongly believe that they would greatly benefit from 'boilerplate free' lenses. |
@modulovalue That's a great suggestion. Lenses would be very helpful for safe and ergonomic state management (e.g. for my flutter_simple_state package or, if you prefer, something similar to immutable.js + Omniscient in TypeScript). Unfortunately, it's seemingly impossible to define a lens that can wrap arbitrary (including 3rd-party) data structures / objects. Well, at least without some compile-time code generation... Maybe lenses should be a separate issue, though? |
@modulovalue Whats the benefit of lenses over record copying semantics of F# over lenses. |
I’m not familiar with it F#, but what is shown on the linked page looks
very similar to `copyWith` method. Lenses are much more powerful. They
allow you to change values deep within a nested data structure. See the
example in the functional_data package.
…On Sat, 21 Sep 2019 at 06:08, Gary Becks ***@***.***> wrote:
@modulovalue <https://github.com/modulovalue> Whats the benefit of lenses
over record copying semantics of F# over lenses.
https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/copy-and-update-record-expressions
?
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#546?email_source=notifications&email_token=ABCADABNFAIEQD2WZVXNORTQKWM5TA5CNFSM4IPYOCZ2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD7IJ4IA#issuecomment-533765664>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABCADABIBLYRWKW5JK7AR4TQKWM5TANCNFSM4IPYOCZQ>
.
|
I'm also not too familiar with F# but as spkersten said, they are much more flexible by being a more general abstraction. Take a look at the following code snippet: class Foo {
final Bar bar;
}
class Bar {
final Baz baz;
}
class Baz {
final String content;
} A Lens of type Foo updateFoo(Lens<Foo, String> lens, Foo foo) {
return lens.update(foo, "New Content");
} Lenses also pave the way for many other abstractions like the But as I mentioned, creating such lenses is too verbose right now. I'd ideally want something like --
I don't see how the dart team could add support for lenses right now as doing that in the OOP context of Dart only makes sense for data classes. There's not a lot I can say other than asking politely to consider the strengths of this abstraction when implementing data classes. |
@spkersten Very cool package, I didn't even know that existed! I understand what lenses are, but I don't understand what the use case is at a language level vs baked in copyWith ? Even the most functional of languages don't have lens support at the language level. Haskell uses the lens library, Scala uses monocle, F# uses aether, Ocaml uses lens. Edit... Considering the OO nature of Dart, I do see why this would be more important for dart than it would be in a more functional language, I don't tend to use any inheritance when writing functional code and my records are always designed to be small enough to not need lenses and also not to negatively affect performance. That said, I agree with your point about the team not being able to implement such a feature, would be nice though. |
Can someone comment on the layers that have an effect on the Dart language as described in the grammar to the Dart language I experience as a developer? For instance the analyzer seems to add restrictions, such as not allowing Another question: Would patterns allow default type parameters like I looked into other grammars from ANTLR's catalog, and ones with patterns (scala, C#) didn't seem to do anything out of the ordinary in their grammar. Is there a certain way of connecting rules? Edit Sorry if this is rather open ended and asking for a volume of info, I hope I didn't derail the thread. |
I very much like the proposal to add a form of (ML style) algebraic data types - or even features that would let one encode algebraic data types in a convenient way. I'd like to point out some "hooks" in the language spec that would make it easy to add ML style algebraic data types.
The following example uses a syntax that is borrowed from Scala 3 (see http://dotty.epfl.ch/docs/reference/enums/adts.html ...) but swift (https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html) and Kotlin (https://kotlinlang.org/docs/reference/enum-classes.html) take a similar route.
An enum either defines each case using the "case" keyword, or none (which would mean, it's a traditional enum as before). We'd extend the enum translation to create one subclass per case, e.g. (only doing the second example)
Instead of the verbose inline "extends", one could use the syntax for super constructors and say
Again, the same class translation that is spec'ed for "enum" would be used (which somewhat justifies the use of the num keyword).
I will leave it at that for now - hope this is helpful. Let me close with the statement that enum corresponds closely to the sum type from type theory (or disjoint union from set theory), so in my mind it's not a coincidence that all these languages have extended "enum" to cover case distinction in data modeling. |
Thanks for the comment. I'm not sure if we literally want to build on Dart's existing enum syntax (though I'm not fundamentally opposed either). But, otherwise, we seem to be thinking in the same direction. |
Thought I’d share something I came across today while browsing through facebooks’ skiplang docs. This might be an interesting approach for adt’s. Of course , using the keyword “children” wouldn’t be an option because of flutter and what not, but maybe something else would work. If nothing else, I think it’s worth a look. |
Very interesting, thanks @g5becks! |
For some reason, extension methods helped quite a bit in that area. For example, we can write: extension UnionValue<A> on Union2<A, A> {
A get value => _value as A;
}
class Union2<A, B> {
Object _value;
} Which has interesting properties: Union2<String, int> example;
// String and int have no shared interface, so value is of type Object
Object value = example.value;
int impossible = example.value; // COMPILE ERROR, not type safe
Union2<int, double> example2;
// int and double have a common interface: num. Therefore value is of type num
num value2 = example2.value; // valid and type safe
int stillImpossible = example2.value; // COMPILE ERROR, value is a num not a int
Union2<String, String> example3;
// The type is always a String, so value is a String too
String value3 = example3.value; // still type safe See union for a package using that pattern. The only thing we're lacking for now is:
The first one is not too important, as we can easily make a method that transform The most important one being the second one (Union2 to Union3+) Maybe a simplification of the feature could be an "any"-like, so that we can do: class Union2<A, B> implements Union3<A, B, any> {} which would allow: Union2<String, int> union2;
Union3<String, int, double> union3 = union2; |
@rrousselGit not sure how that fills the need for adt’s. E.G. how can you create a Result type without all the boilerplate it would take wrt the class hierarchy? I think the extension methods help fill the need of pattern matching for the interim, but not with the creation of what you are matching against. Or at least not for the use cases I think they would be used for mostly. From the looks of it (I could be wrong) a Union<A,A> that has multiple constructors is an And type and not an Or type? Looks more like a Tuple type than a Choice type. |
@g5becks No, this is not a Tuple but an actual union. TL;DR Union2<String, int> myUnion;
myUnion.switchCase(
(String value) => print("got a string $value"),
(int value) => print("got an integer $value"),
); It also has an example of a result-like type https://github.com/rrousselGit/union#making-a-reusable-union |
@rrousselGit i should have read closer! That’s actually pretty cool. I think I’ll definitely be using this. |
Had another go at this recently. Which means we can use functions instead of classes to implement our temporary unions. Which also means we can write: Union2<String, int> union2;
Union3<String, int, double> union3 = union2; // upcast, no problem
Union4<String, int, double, Object> union4 = union2; // still works while still being unable to write: Union3<String, int, double> union3;
// downcast, does not work because the value can be a double too.
Union2<String, int> union2 = union3; It's sad that Dart doesn't support well typedefs though. It'd be almost perfect: https://github.com/rrousselGit/union#custom-unions |
True, nice work! @rrousselGit Also patiently waiting on the |
I also faced these issues and handled using the factory constructors mentioned by @rrousselGit in a StackOverflow post and was pretty satisfied with the results. To avoid the boilerplate, I built a code generator with my colleague which generates these classes by annotating Enums. @superEnum
enum _MoviesResponse {
@Data(fields: [DataField('movies', Movies)])
Success,
@object
Unauthorized,
@object
NoNetwork,
@Data(fields: [DataField('exception', Exception)])
UnexpectedException
} where:-
Then it can be used easily with the generated moviesResponse.when(
success: (data) => print('Total Movies: ${data.movies.totalPages}'),
unauthorized: (_) => print('Invalid ApiKey'),
noNetwork: (_) => print(
'No Internet, Please check your internet connection',
),
unexpectedException: (error) => print(error.exception),
); |
Even knowing that they'll come is so good. Without them codes are really dirty and and running build_runner takes so much time. |
I just wanted to add my 2c, but you guys really need to look at how C# has implemented pattern matching and destructuring. It's turned out to be so elegant at least in utility. if (something is { YouCan: true, Do: { Some: "crazy", Stuff: true}})
{
// ...
} Even if it looks verbose at first blush, the amount of verbosity it cuts down when performing checks of complicated object hierarchies is just priceless. |
We did. :)
Yes, the proposal supports arbitrary destructuring like that. There are some syntactic differences between Dart has map literals which naturally want to claim the curly braces, but the level of expressiveness is roughly the same. |
Good to see this in the "Being implemented" funnel 🎉 @munificent now that this is being implemented how the new "Data class" feature will look like? Or perhaps where it is being dicused/designed? |
No major updates on data classes. One relevant bit is that part of the design of views includes a primary constructor-like syntax and that led to discussing whether we can generalize that for classes (#2364). I believe we can and my hope is that we'll be able to design and ship that at some point after we ship records and patterns. My feeling is that primary constructors is a big piece of what users want when they ask for data classes. |
@munificent since the whole "Pattern and related feature" is being implemented, does this include all the feature mentioned in this PR? I suppose that Record will be the first feature, but also the others are work in progress? Specifically, what about ADTs? |
I can't promise it covers every single comment in this issue (there's a lot), but, yes, we're doing records and pattern matching and implementing both now. That includes support for algebraic datatype-style code. We want this to feel like a robust, complete set of features. |
I've played around with Dart 3 Alpha and it's such a major upgrade in DX. Great job everyone involved! 🥇 @munificent With sealed classes in place, is it there a plan in the future to implement narrowing with the ternary operator when working with sealed classes? Something like this:
That would be really valuable for handling conditional widget rendering inline in Flutter without the need to create a function with a switch statement. |
@blueturningred I may be wrong, but can't we just implement the Also, when reading What is Maybe I'm missing something here. If Dart 3 is able to infer which classes are "left" in the |
Isn't the whole purpose of sealed classes that the compiler would be able to find all the subclasses that extends the sealed class by preventing extension outside the package?. Also, you can't practically use switch inline.
I think it's pretty obvious what x's type would be on the else side. What seems to be the mystery here? You have two subclasses, you've already checked for one of them, what would be the subclass in the else side? Here's a working example in Typescript: Link |
Sorry if I'm bothering you, but what happens if you add a third one? Could you show that with Typescript, too? |
Not at all. If you add a third one which doesn't have
You can check the code here. However, if you add a third one that DOES have |
Thank you @blueturningred, I understand this now. It's even better to what |
Good question! The main issue to discuss this is #2179. We aren't currently planning to have type promotion take sealed types into account and at this point it's very unlikely it will make it into 3.0. I think that's probably the right call. Flow analysis and type promotion versus sealed types and pattern matching are sort of two opposing styles for solving the same problem (moving from a more general type to a more specific one). Most languages don't let you mix those styles at all. Rust, Swift, Haskell, and other functional languages do pattern matching but don't do any flow analysis. Kotlin and TypeScript do flow analysis but don't really have pattern matching. Dart is a little odd in doing both but it can get kind of weird when the two styles mix. I think it's probably easier for users to understand if we try to keep those styles mostly confined. I could be wrong. But for now, if you want to work exhaustively with sealed types, that's what switches are for. If you want to work with type tests and control flow, that's what type promotion is for. As to your example, I would write: sealed class User {}
class A extends User {
final String firstName;
A(this.firstName);
}
class B extends User {
final String lastName;
B(this.lastName);
}
String formatName(User x) =>
switch (x) { A a => a.firstName, B b => b.lastName }; |
Closing this because records and patterns are now enabled by default on the main branch of the Dart SDK! 🎉 |
@munificent you should move this into the "Done" column of the "Language Funnel" project |
I'm not sure exactly how the timing for stuff in the language funnel is managed. That might wait until it ships on stable. I'll let @mit-mit or @itsjustkevin handle that part. :) |
will I be able to use the record identifier instead of indexes? const (String name, int age) person = ('John doe', 32);
// this
print('${person.name}, ${person.age}');
// or this
print('${person.$name}, ${person.$age}');
// is more readable than this
print('${person.$1}, ${person.$2}'); |
@talski Yes, you can do as follows: const (String name, int age) person1 = ('John doe', 32);
print('${person1.$1}, ${person1.$2}');
const ({String name, int age}) person2 = (name: 'John doe', age: 32);
print('${person2.name}, ${person2.age}'); You can read more on Record type annotations:
|
This feature is getting more exciting version by version! Will this type of inference be possible in further iterations? Currently, the following code produces a sealed class Sealed<T> {}
class Case implements Sealed<int> {}
T consume<T>(Sealed<T> value) => switch (value) {
Case() => 1,
}; |
@purplenoodlesoop To my understanding, that's expected behavior; why would the compiler specialize There, I see Dart expecting a type |
@purplenoodlesoop It is indeed working as intended, as @lucavenir says. There is currently no subclasses of sealed class Sealed<T> {}
class Case implements Sealed<int> {}
class OtherCase implements Sealed<string> {}
T consume<T>(Sealed<T> value) => switch (value) {
Case() => 1,
OtherCase() => "wat",
}; Then we can't use a global analysis to decide that all actual objects implementing Even if the type inference was incredibly clever, and recognized that any value which (I'm also not entirely convinced this doesn't hinge on Still, currently, knowning that a So complaining that (If Dart gets variance annotations, it might also want to have lower bounds, like Java's |
We're starting to investigate a couple of related language changes around tuples, pattern matching, and destructuring. These features touch on other things like "data classes", so I wanted to kick things off by trying to sketch out my understanding of the space, how I think the various features relate to each other, and the relevant issues people have filed.
Draft feature specification:
https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/feature-specification.md
Implementation issue: dart-lang/sdk#50585
Concepts
As I understand it, the core concepts are:
Patterns
Languages with pattern matching revolve around patterns. "Expression" and "statement" are both syntactic categories in the grammar. Patterns form a third category. Most languages that have pattern matching have a variety of different kinds of patterns they support. The basic idea with a pattern is that it:
Languages with patterns use them in a variety of places. They can be how you declare variables and catch exceptions. Some languages use them for defining parameter lists or "overloads" of a function. Every language with patterns has some kind of explicit pattern matching expression or statement...
Pattern matching
Once you have patterns, it makes sense to introduce a control flow structure that uses them. A pattern matching statement takes a value that it matches against. Then it has a series of pairs of patterns and bodies. At runtime, the implementation tests each pattern against the value in turn. The first pattern that matches has its body executed. Any variables the pattern binds are only available in the corresponding body.
If you really want a functional style, an even more powerful form is a pattern matching expression that lets you do pattern matching in the middle of a larger expression and have it evaluate to a value. In order to do that in a sound way, though, you need the next concept...
Exhaustiveness
So you're executing a pattern matching construct, walking through all of the patterns to find a match. What happens if none of the cases match? If the pattern matching construct is an expression, this is a real problem because what value do you evaluate to in that case? Even when you are using pattern matching as a statement, users still likely want to know if it's possible for a value to not match any of the cases.
To help with that, many languages with pattern matching also do exhaustiveness checking. The static checker will examine all of the patterns and determine if it's possible for a value to sneak through and match none of them. If so, the compiler reports a warning or error to let the user know. Dart does a limited form of this now with switch statements on enum types. If you miss one of the enum cases, Dart gives you a warning to let you know. Exhaustiveness checking in pattern matching takes that concept and applies it to much larger, more complex patterns.
Destructuring
A list literal expression takes a number of smaller values, the list element expressions, and composes a new larger value out of them, the list. Patterns go in the other direction. For example, a list pattern contains a series of nested patterns for elements. When the list pattern is matched against a list, it destructures the list by walking the elements and matching them against the corresponding element patterns.
This gives you a really nice terse way to pull pieces of aggregate objects like lists, maps, and even instances of classes. Any time you have a pattern that contains nested subpatterns, you're usually doing some kind of destructuring.
Tuples
Lists are a great way to collect a series of values of the same type, but they work poorly when you want to, say return a number and a string from a function. Since lists only have a single element type, the type of each separate element is lost.
Tuples fix that. A tuple is sort of like a fixed-size list where each element has its own distinct type. The type system can see "into" the tuple. A tuple of an int followed by a String has a different static type than a tuple of two booleans. Syntactically, tuples are usually a comma-separated list of expressions surrounded by parentheses, like
(1, "string")
.Tuples are useful in a statically-typed language for things like multiple return values because they maintain precise types. Once you have a tuple, you eventually need to pull the elements back out of it. You can provide number-like getters, but that gets tedious. Languages with tuples almost always have some kind of tuple pattern so that you can destructure them.
Algebraic data types, sum types, unions with fields
Pattern matching comes to us from the ML language family. Another key feature of those languages that ties into pattern matching are algebraic data types, also known as sum types, discriminated unions, or tagged unions. To make matters more confusing, these are quite different from both union types and (untagged) unions in C and C++. Algebraic data types are sometimes abbreviated ADT, which is again distinct from abstract data types. Thanks, computer scientists.
Sum types are like superpowered unions. You have a type that contains a fixed, closed set of cases. Unlike Dart enums, each case may also have its own fields containing extra data. For example, you might have a Shape type, with cases for Rect and Circle. Rect would have four coordinates for its corners. Circle would have a center and radius.
In an object-oriented language like Dart, you'd model this using subclasses. You'd have an abstract Shape class and Rect and Circle subclasses. Subclasses and ADTs are sort of duals. Indeed, Scala's case classes are like ADTs but are subclasses under the hood.
These come into discussions of pattern matching because the typical way to define behavior on specific cases in a sum type is by using a pattern match on the different cases. Likewise, the way to extract each case's fields is by using a case pattern to destructure them. Something like:
It would be very strange to add algebraic data types to a language without a nice way to switch on and decompose them like this.
"Data classes" and "value types"
One of the most frequent feature requests for Dart is some kind of data classes or value types. The former is inspired by Kotlin's corresponding feature. The latter means different things to different people, but common attributes seem to be:
A lightweight way of defining a named type with some fields without having to explicitly declare a constructor that initializes each field.
An automatically provided implementation of
hashCode
andoperator ==()
so that the resulting object has "value semantics".Some kind of immutability. The fields can't be modified. Some ask for deep immutability—the values of all fields must themselves be immutable. This usually implies that the data class can't be extended either.
Often some easy way to clone or make a new copy of an object with some fields changed.
Data classes are involved with patterns and pattern matching because users also often request that a data class also implicitly provides destructuring support. (Kotlin data classes give you this.) That means some kind of pattern to let you pull apart the fields in an object.
Also, part of the motivation for both algebraic data types and data classes is a nicer notation for defining a new composite type without all of the boilerplate overhead of a full class declaration. In other words, a good ADT feature might subsume data classes or vice versa.
Type patterns
The last concept which has mostly come up internally but exposes a capability the language doesn't otherwise offer is some kind of way to expose the runtime type argument of an object. If you have patterns and pattern matching, a natural solution is a type pattern that matches an object of some generic type and binds variables to the object's type arguments.
Structure
We are very early in our investigation. This is a family of features that are very close to my heart. I wrote a doc in 2011 requesting that we add pattern matching to Dart. At the same time, as you can see, this is a big sprawling feature and we currently have our hands full with extension methods and non-nullable types (both very large features in their own right!).
That being said, here is how I am interested in approaching the space and what I hope we can do:
Define a set of pattern syntax and semantics. I think patterns are the most important core to all of these features. You obviously can't do pattern matching, destructuring, and exhaustiveness checking at all without them. You technically can do tuples, sum types, and data classes, but I think they are significantly hampered without them.
Also, while it may not be obvious, I believe there are some real tricky design challenges in this area. Patterns are a dense grammatical region where we want to be able to express a wide variety of behaviors in a small amount of syntax. We need to be able to destructure lists, maps, sets, and tuples. Do runtime type tests. Pull fields out of, at least, sum types or data classes. And, of course, test for literals and constant values for simple switch-like cases.
We are constrained by the expectations users bring from other languages, the desire for patterns to mirror their corresponding expression forms, and (hardest) Dart's existing notation for variable declarations, since those are already a limited form of "pattern" in that they introduce bindings.
I think getting this right is key.
Define a set of language constructs where patterns can be used. Some new kind of pattern matching statement is the obvious one. But also retrofitting them into variable declarations so that you can destructure in a declaration. Can we re-engineer catch clauses to be pattern based so that you can have more sophisticated catches? Should parameter lists use patterns? Do we want an expression-based pattern match? Some kind of if-let-like statement? This is the fun stuff. Once users have internalized how patterns work, the more places they can use that knowledge the better.
Add tuples. This can happen in parallel with the previous one. Much like adding sets, this is a whole feature in its own right with new syntax, object representation, runtime behavior, and type checking.
@lrhn has already put some work into defining this.
User-defined destructuring. My favorite language features are ones that add new expressive syntax whose semantics can be programmed by an API designer. The
for-in
loop syntax is baked into the language, but you get to decide what it means in your implementation of Iterable.I want the same thing for destructuring. I think a syntax something like
Name(subpattern1, subpattern2)
(an identifier followed by a parenthesized list of subpatterns) should desugar to calling some user-defined destructuring operation on the matched value. That operation can define whether the value matches or not and, if it does, what subvalues the subpatterns are matched against. There are obviously many details to work out here, but also prior are in Scala'sunapply()
method and Kotlin'scomponentN()
methods.Data classes. The least-defined (in my mind) goal is around improving the user experience for easily defining value-like types. This could mean some new explicit "data class" syntax, or a family of smaller features like allowing implicit constructors to take parameters.
If we allow these two be subclasses of a superclass, then that gets pretty close to sum types once you include the previously-mentioned ability to pattern match on types and destructure them. Likewise, once you have general user-defined destructuring, then any lightweight data class-like syntax can just be syntactic sugar for a class declaration that includes a declaration of its own destructuring support.
As I said, this is all still very early and tentative, but I wanted to collect what I think are the related issues into one place and then begin sketching out a map through this territory.
The text was updated successfully, but these errors were encountered: