-
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
Reusable Pattern Matching #4057
Comments
This is my first issue here, so I'd really appreciate any feedback to help me improve it ❤️! Here's another example to illustrate this proposal: patterndef ValidUser(String name, int age) = {
'name': var name,
'age': var age,
};
const userMap = {
'name': 'John Doe',
'id': '123',
'age': 30,
};
validateUser() {
// same as:
// if (userMap case {'name': String name, 'age': int age}) {}
if (userMap case ValidUser(:name, :age)) {
print('Valid user: $name, $age');
} else {
print('Invalid user: $userMap');
}
// same as:
// final {'name': name, 'age': age} = userMap;
final ValidUser(:name, :age) = userMap;
final userFromMap = switch (userMap) {
ValidUser(:name, :age) => User(name, age),
_ => throw ArgumentError('Invalid user MAP: $userMap'),
};
switch (userMap) {
case ValidUser(:name, :age):
if (age < 18) {
print('User $name is underage');
} else {
print('User $name is an adult');
}
break;
default:
print('Invalid user: $userMap');
}
}
class User {
User(this.name, this.age);
final String name;
final int age;
} |
While i love the idea, the problem that i think needs to be addressed is variable declaration patterns. Since declaration patterns define new variables, how should compiler act when it encounters a variable redeclaration? Should it automatically redefine the variable, or throw error for trying to redeclare the variable? This is not problem with normal patterns, since the developper can see what the pattern does, but since this pattern is reusable, the developper can't know what it declares. This kind of ambiguity can make it hard to work with and is very error prone. Do you propose that So the your code could look like this: patterndef ValidUser(String name, int age) = {
'name': var name,
'age': var age,
};
...
switch (userMap) {
case ValidUser(:validName, :validAge):
if (validAge < 18) {
print('User $validName is underage');
} else {
print('User $validName is an adult');
}
break;
default:
print('Invalid user: $userMap');
} Since we also don't want to overcomplicate the things, i also propose syntax for redeclaring the variables, instead of making new ones: case var ValidUser(:name, :age): //will act the same way as your proposal, but without ambiguity Every time you use a patterndef and redefine a variable, you have to explicitly use |
After thinking about it some more, i'm completely sold on this idea and would like to propose an addition it. ProposalIf Dart were to have type User = {"username": string, "nickname": string, "id": number, "isMember": boolean};
type MemberFields = {"rank": number, "joined": string};
type MemberUser = User & MemberFiels; Here are the patterns for patterndef User(String username, String nick, int id, bool isMember) = {
"username": var username,
"nickname": var nick,
"id": var id,
"isMember": var isMember
};
patterndef MemberFields(int rank, String joined) = {
"rank": var rank,
"joined": var joined,
}; Now imagine you are getting some json, and you want to validate if it's user and check if he is privilaged. I propose typescript-like composition of patterns, where instead of validating if json is a User and if he is a Member separately, having multiple conditions, why not just compose the patterns and check once? The composition would only work with Map patterns. and there would be two different kinds of composition - Strict, and Optional. Strict compositionStrict composition pattern would pass only if both subpatterns pass. The composite pattern would capture variables from both subpatterns and pass them to the user. Here is my proposed syntax: patterndef MemberUser(/*args*/) = User(/*usrAgrs*/) & MemberFields(/*memberArgs*/);
// equivalent to
patterndef MemberUser(/*args*/){
"username": var username,
"nickname": var nick,
"id": var id,
"isMember": var isMember,
"rank": var rank,
"joined": var joined,
}; Optional compositionOptional composition pattern would pass if the left pattern passes, regardless of if the right side pattern fails. The composite pattern would capture variables from left patterns, and try to capture as many variables from right pattern. If some variable in right pattern is not found, it would instead assign null to the variable. patterndef UserPossiblyMember(/*args*/) = User(/*usrAgrs*/) + MemberFields(/*memberArgs*/);
// equivalent to
patterndef UserPossiblyMember(/*args*/){
"username": var username,
"nickname": var nick,
"id": var id,
"isMember": var isMember,
// would be optional fields, if not found assign null
"rank"?: var rank,
"joined"?: var joined,
}; UsageHere is example validating a user // before
if(user case User(...)){
if(user case Member(...)){
doSomething();
}
}
// after
if(user case var User(usename, nickname, id, isMember) & Member(rank, joined)){
print("$username $nickname $id $isMember $rank $joined");
} EditI've just realised a problem with the proposed ValidateUser(:name, :age) // user defined pattern
ValidateUser(:name, :age) // class ValidateUser with properies name and age As such, new syntax is needed for the |
This is a number of features, of growing complexity. The fundamental level is named patterns, which allows giving a pattern a name, and reusing it by referencing that name. // Pattern on `Object` values.
patterndef Object UrlPattern = (Url() || String()) && var url; A pattern has an associated matched value type, which the definition should include. It affects how the pattern is interpreted, you can't just have a pattern without a matched value type. (But you can probably write nothing and get The next step is parameterized patterns. First type parameterized patterns: // Pattern on `Object?` values, parameterized by a type `R`.
patterndef Object? Opt<R extends Object> = (R _ || null) && var value; Then there are first order patterns, which take patterns as arguments. // Pattern on Object? which takes type `R` and *pattern on `R`* as parameter.
patterndef Object? Chk<R extends Object>([#Pattern<R> extraCheck = _]) = (R value && extraCheck);
void main() {
if (someValue() case Chk<int>(>= 0)) {
print(value.toRadixString(16));
}
} This gets hard to parse because the grammar cannot see that Actually, before that there could be parameterization of patterndef Object GoodInt(int min) = int v && >= value;
patterndef Object GoodInt2(int min) = int v when v >= value; Both of these are complicated by patterns needing to be constant, but then that just means that all the arguments to these patterndefs need to be constant values or constant patterns. Then there is the holy grail: First class patterns, higher order pattern definitions, where you can take pattern definitions (pattern functions, really) as argument to non pattern functions. But we could potentially take pattern definitions as arguments to pattern definitions, since everything must be constant. Imagine: patterndef Object => {int number} IntValue(Object => {int number} fallback) =
int value || fallback;
void main() {
if (someValue() case IntValue(String(length: var value) || List(length: var value))) {
print(value.toRadixString(16));
}
} The Again, the syntax is horribly close to existing pattern syntax, and may need special punctuation. It's also unlikely that we'll want to this far. Or anywhere in the direction of abstracting over patterns. Use functions for abstraction, don't abstract over patterns. Functions are the most general functionality, they can contain pattern matches and any other code too. The more restricted the grammar you're abstracting over, the more special-casing you may end up needing. I'd just write: ({String name, int age})? validUser(Object input) =>
switch (input) {
{'user': [String name, int age]}) => (name: name, age: age),
_ => null;
};
// ...
switch (validUser(input)) {
case (: var name, : var age)):
if (age< 18) {
print('User $name is underage');
} else {
print('User $name is an adult');
}
break;
default:
print('Invalid user: $input');
} |
^
That's not really a reusable pattern. It's a function. IMO the more realistic approach is to use extension methods: extension on Color {
Color? get casePrimaryColor {
switch (this) {
case Colors.red:
case Colors.blue:
case Colors.green:
return this;
default:
return null;
}
} Then used as: switch (color) {
case Color(casePrimaryColor: final color?):
print('Is primary color: $color');
} |
I think we have been thinking about reusable pattern matching in a wrong direction. There is no real benefit to making a ProposalI propose allowing patterns to delegate the validation and matching to functions, which would allow more composable patterns, optional complex validation, automatic typing of the data and quick structure building. If the function throws or returns null, the pattern is wrong and is refuted. SyntaxHere is a proposed syntax of the pattern type: functionIdentifier [var_name]
// example
matchUser user // captures data to var user
matchUser // only validate the correctness of the pattern As far as i know, this kind of pattern doesn't overlap with any current pattern grammar, since the compiler knows what is a class and what is a function. The compiler would be able to infer the type of the variable from the return type of the function. ExampleLet's say we have a user, which itself has a field of sent messages (pretend "string" is actually type definition): {
"user_id": "string",
"user_info": {
"username": "string",
"nickname": "string",
"email": "string",
"avatar": "string"
},
"messages": [
{ "message_id": "string", "content": "string" },
{ "message_id": "string", "content": "string" },
{ "message_id": "string", "content": "string" },
]
} This is how we would validate it and load it up into a structure using my proposal: class User {
String id;
UserInfo info;
List<Message> messages;
User(this.id, this.info, this.messages);
static User? match(dynamic objToMatch) {
// quick match and map => match a list and map the elements to Message
// if its not a list, or the elements are not Message, refute it
List<Message>? matchMessages(dynamic obj) {
if (obj is List) return obj.map<Message>((e) => Message.match(e)!).toList();
return null;
}
if (objToMatch
case {
"user_id": String id,
"user_info": UserInfo.match userInfo,
"messages": matchMessages messages,
}) {
return User(id, userInfo, messages);
}
return null;
}
}
class UserInfo {
String username, nickname, email, avatar;
UserInfo(this.username, this.nickname, this.email, this.avatar);
static UserInfo? match(dynamic objToMatch) {
if (objToMatch
case {
"username": String username,
"nickname": String nickname,
"email": String email,
"avatar": String avatar
}) {
return UserInfo(username, nickname, email, avatar);
}
return null;
}
}
class Message {
String id, content;
Message(this.id, this.content);
static Message? match(dynamic objToMatch) {
if (objToMatch case {"message_id": String id, "content": String content}) {
return Message(id, content);
}
return null;
}
} And then we could use it directly in if(incomingObject case User.match){
print("its a User");
}
// with capturing data
if(incomingObject case User.match user){
print("${user.id} ${user.info} ${user.messages.length}");
} BenefitsIt would allow very quick validation and loading of the structures, where the data would closely resemble the structure they are coming from. The class would also be able to choose whether some values are valid (example adult >= 18) and refuse the pattern. In relation to With the data classes and macros, that are in the works, it would also be much easier to write, since you could use a macro (for example I also can't think of any detriments this feature would bring, though it needs to be studied some more. |
True. And the function is the unit of code-reuse and abstraction in the language, which is why it works. Using extension getters is also viable, getters is one way to get user code into a pattern, and it's the most flexible one. (Another one is overriding (I'd extend object pattern property naming to entire constant cascade-selector-chains, so you don't need to write extensions to do The thing here is that abstracting over a pattern really is abstracting over a test. A pattern is one way to do a test, but using a function means you can choose another implementation too. You can't embed that in the middle of a larger pattern, but you can combine multiple test functions in another test function. As Tennent said in "Principles of Programming Languages", any meaningful syntactic class can be abstracted. It's not completely unreasonable to be able to name a pattern, so it can be reused. We do it for types because it's useful. We don't need to, today you can write any type inline, but sometimes you want to give it a name. My worry is that pattern abstraction is a feature with a large risk of feature creep. Should it be able to include a From a principle in the same book, a language should have zero, one or an infinite amount of any feature. Dart has zero levels of pattern abstraction. First order patterns, aka. pattern definitions, pattern aliases which directly expand to a pattern, is an option. Can probably even take other patterns as arguments. I'm not sure it's worth its own complexity cost, but it's likely doable. So, for the heck of it, let's define a first-order pattern alias, with hygienic local variables, so it cannot abstract over bound variables: <pattern-alias> ::=
'patterndef' <pattern-type>? <identifier>
<pattern-type-parameters>?
<pattern-parameter-list>? '=' <pattern> ('when' <expression>)?
'[' <pattern-pattern-parameter-list>? ']'
-- Signature of a pattern. The matched value type, and the type that the pattern promotes that to
-- if it matches. If the promoted type is omitted, we can either infer it, or default it to not promoting.
-- If both are omitted, we can infer or default to `Object? -> Object?`
<pattern-type> ::= (<type> '->') <type>
-- Like a type parameter list, but allowing `const` in front of names.
-- Must be `const` if referenced outside of `when` clause.
<pattern-type-parameters> ::= '<' <pattern-type-parameter> (',' <pattern-type-parameter>)* '>'
<pattern-type-parameter> ::= 'const'? <identifier> ('extends' <type>)
-- Normal "value" parameters, like normal parameter list, but can be marked `const`.
-- Must be const if used outside of `when` clause.
<pattern-parameter-list> ::= '(' ... normal parameter list, but allowing `const` modifier on parameters ...')'
-- Like a parameter list, including optional and named parameters, but for pattern parameters which have a
-- pattern type. Non-empty.
<pattern-pattern-parameter-list> ::=
<pattern-pattern-parameters> (','?
| ',' '[' <pattern-pattern-parameters> (','? ']' | ']' ',' '{' <pattern-named-pattern-parameters> ','? '}'
| ',' '{' <pattern-named-pattern-parameters> ','? '}')
| '[' <pattern-pattern-parameters> (','? ']' | ']' ',' '{' <pattern-named-pattern-parameters> ','? '}'
| '{' <pattern-named-pattern-parameters> ','? '}')
<pattern-pattern-parameters> ::= <pattern-pattern-parameter> (',' <pattern-pattern-parameter>)* ']'
<pattern-pattern-parameter> ::= <pattern-type>? <identifier> ('=' <pattern>)?
<pattern-named pattern-parameters> ::=
'required'? <pattern-pattern-parameter> (',' 'required'? <pattern-pattern-parameter>)*
<pattern-pattern-parameter> ::= <pattern-type>? <identifier> ('=' <pattern>)?
<pattern> ::= ... | <pattern-alias-reference>
<pattern-alias-reference> ::=
<qualified-identifier> <argumentPart>? '[' (<argumentList> ','?)? ']' which you can use as: /// Matches [List<T>] that starts and ends with elements matched by [First] and [Last].
patterndef List<T> ListEnds<T>[T First, T Last] = [First && Last] || [First, ..., Last];
/// Matches a [List<T>] with [minLength] ≤ `length` ≤ [maxLength].
///
/// If [First] and/or [Last] are are provided and the list is non-empty,
/// the [List.first]/[List.last] elements must match.
patterndef Object?->List<T> LimitedList<T>[{T First = _, T Last = _}]({int minLength = 1, int? maxLength}) =
List<T>(length: >= minLength && var length) &&
([] || ListEnds<T>[First, Last]) when maxLength == null || length <= maxLength;
// ...
if (value case LimitedList<int>[> 0](minLength: 2, maxLength 4)) {
// `value` promoted to `List<int>`
// Known to have length 2..4 and first element >0.
..
} or
The pattern-type before the name has the form "matched-value-type -> promoted-type". The pattern assumes the matched value type is the former, and if it matches, it promotes to the latter. The The '(...) The The pattern parameters and value parameters can be referenced in the aliased pattern, the type parameters can be referenced everywhere. The aliased pattern may include a The pattern signature,
For the last point, we can define As for exfiltrating bindings ... that's a bigger can of worms. I'd probably suggest defining a "return type/value" for a pattern, which creates a record value out of some of the matched variables or other constants. <pattern-type> ::= (<type> '->')? <type> '!'? (':' <recordType>)? so you can write // Makes `T first` and `T last` available as "object pattern" properties of a match.
patterndef List<T>:(T first, T last) FirstLast<T> =
List<T>(isNotEmpty: true, : var first, :var last) return (first, last) and use it by putting // ...
if (someList case FirstLast<int>[]:(var fst && >= 0, var lst && >= 0) when fst < lst) { ... } If you write nothing, the return type is So, ... possible. Not entirely sure how desirable this is. There is much complexity because we are passing in three kinds of parameters: Types, values and other patterns, in order to create a single pattern, and we need a way to say how to extract values that is not returning them. Or rather, a template is like a combination match + return value. I would also think it'll be better to build on top of functions. They are the more general abstraction mechanism. if (value case Foo() && .(@toPoint(_): (int x, int y))) ... where |
If we were to compare As @lrhn pointed out, and as i have also failed to realise (hence bad proposals), patterns are not just some constructs, they are tests. The Dart compiler basically compiles the pattern into validation function with variable bindings. Thinking of them as their own separate construct, and building on it, will only lead to more complexity and insufficient solutions. Instead of thinking of patterns as their own thing, thinking of them as functions makes it easier to use and extend their functionality. Baseline functionalityBefore we continue with this discussion, it's good to understand what we want to achieve with the reusable patterns. Here are the most essential points:
And this are the features that would be very nice to have:
And so, if reusable pattern matching were to be added to Dart, "functions as patterns" makes a lot more sense than Improvement to syntaxThe improvement concerns the variable binding and destructuring syntax. I propose an "operator" function ["->" [declaration_pattern | object_destructure_pattern] ]
// no binding pattern
functionName
// variable binding
functionName -> var_name
// object destructuring syntax => function matches Point(int x, int y);
functionName -> (:x, :y) I would also make it that the pattern only accept functions with signature User? matchOnPrivilage(dynamic obj, int privilage){
...
if(user.privilage != privilage) return null;
// other matching logic;
}
User? matchOnAdmin(dynamic obj){
return matchOnPrivilage(obj, 10);
}
if(json case matchOnAdmin -> (:user_id, :email)) {
print("$user_id, $email");
} Also, i'd like to ask @HenriqueNas to look throught through the proposed solutions, so that we can better understand the requirements. |
For me, the idea of using Here's the refactored initial examples using this new proposed syntax: class User {
const User(this.name, this.age);
final String name;
final int age;
factory User.fromJson(dynamic user) {
if (user case { 'name': String name, 'age': int age }) {
return User(name, age);
}
throw 'Invalid argument';
}
}
class Vehicle {
const Vehicle(this.wheels);
factory Vehicle.fromJson(dynamic vehicle) => Vehicle(vehicle['wheels'] as int);
final int wheels;
}
class Car extends Vehicle {
const Car() : super(4);
}
class Bicycle extends Vehicle {
const Bicycle() : super(2);
}
// I might not get the ideia, but it would work like that, right ?
Vehicle getVehicle(dynamic vehicle) {
return switch (vehicle) {
Vehicle.fromJson -> (:wheels) when wheels == 4 => const Car(),
Vehicle.fromJson -> (:wheels) when wheels == 2 => const Bicycle(),
Vehicle.fromJson -> vehicle => vehicle,
_ => throw 'Invalid argument',
};
}
const json = {
// valid user
'name': 'John Doe',
'age': 30,
// valid vehicle
'wheels': 2,
};
void run() {
try {
if (json case User.fromJson -> (:name, :age)) {
print('user name: $name'); // John Doe
}
final Vehicle vehicle = getVehicle(json);
print('vehicle type: ${vehicle.runtimeType}'); // Bicycle
} catch (error) {
print(error);
}
}
Anything I missed ? |
@HenriqueNas You didn't miss anything. // I might not get the ideia, but it would work like that, right ?
Vehicle getVehicle(dynamic vehicle) {
return switch (vehicle) {
Vehicle.fromJson -> (:wheels) when wheels == 4 => const Car(),
Vehicle.fromJson -> (:wheels) when wheels == 2 => const Bicycle(),
Vehicle.fromJson -> vehicle => vehicle,
_ => throw 'Invalid argument',
};
} Yes, that's exactly how i want it to work. I want "function as patterns" over "patterndef", because it would also allow the Now we just have to wait for the feedback from Dart Language team, to see how feasable the proposal is, and what doesn't work. One thing that is needed to be addressed (at least i think so), is the constrains of the pattern matching process (which function can it take, is |
I'd allow a function with a subtype of Forcing it to be a record ensures that accessing properties had no side effect, and making it nullavle gives a way to reject. But it is a little special-cased. We could allow the function to have any return type, and return value, treating the function as a transformer, not a matcher, and then you call do case ... || transform -> (:var x, :var y) to only match if it is a non-null record, or you ca use any other pattern: ... nullOrEmpty -> false If you want to have a general transformer with the This behavior is why I suggested that the function call was part of the object pattern, as a way to do "user code" access on the object, just like calling an extension getter. Because that is a possible implementation today. The functionality is there, it's just inconvenient. So we need a way to not have to create the extension getter for an existing (constant?) function. With a shorter syntax. I suggested Then we just need a way to call a function with the "current value" as an argument. That's what the "pipe operator" is about in expressions.(There are multiple issues for that.) Assume we have a pipe operator case .(-> foo(): ...) Nothing new or specialized, but generalizing object patterns and making their syntax shorter, and using an existing (well, if we add it first) way to call a static function as a selector. |
Edit - this whole comment should be disregarded, see comment below Could you please provide a code example? I just can't really wrap my head around the syntax. Also i don't mind allowing record type as return value, i'm just of the opinion, that we should start it out sort of restricted to simple cases, and when we see that developpers want more, it would be easier to make the compiler more loose, than making it very loose from the start and than making it stricter if some kind of coding pattern causes problems I don't oppose making it Syntax suggestionIf the functions in
where user will be able to pass the required arguments, if he needs to (i'm still opposed to passing arguments), and the // example with piping and argument passing
// mapMessages(dynamic, bool shouldSkip) -> if shouldSkip is enabled, skip non matching element, and continue
// just a rudimentary showcase of passing arguments, with no real purpose
var msg = switch(json){
List() -> $.map[(e) => Message.match(e)] -> var messageList => messageList,
{
"messages": mapMessages[$, true] -> var messageList,
"user_id": var user_id
} => messageList,
_ => throw "Bad json"
};
// example with subsequent object destructuring
...
if(json case {
"user_info": UserInfo.match[$] -> $(username: var user, nickname: var nick)
}){
print("$user, $nick"); // good
print("${$.username}") // throws since the $ is no longer a variable
}
// matchPoint is a transformer function
// normal record destructuring => matchPoint() returns (int x, int y)
matchPoint[$] -> (int x, int y)
// or (since we know the data type)
matchPoint[$] -> (var x, var y)
// Object destructuring => matchPoint() returns Point(int x, int y);
matchPoint[$] -> $(:x, :y)
// or
matchPoint[$] -> $(x: var x, y: var y) This syntax would allow proper piping, with type safety. I believe this syntax should be used if we are to have a piping mechanism in patterns. |
Actually, i can't think of a reason to make piping possible in the pattern. Or at the very least genuine piping that you could chain. I also don't know about allowing returning I would allow only a matcher - function, that could return some value or That means that My suggested syntax of Object destructuring syntaxIf i understand it correctly, your proposed syntax for object destructuring is The syntax i propose is similar, with the only change being readibility. Here it is: matchSomeObject -> $(attr: var a, ...) I only changed the TLDR: Multiple chaining is probably a mistake and my previous comment should be disregarded. |
The not currently existing syntax that I suggest (at least as a strawman syntax) are:
With those features, there is no need to add extra functionality to patterns to allow reusable matching or value transformation, they just work from the existing features: case SomeNum(
toString()->int.tryParse: var integer?) There is no need to require specific return types for functions you call on the matched value, because that would prevent you from using existing functions. The (That all said, the pipe operator is itself a little too special cased. You can see I said to put the value first in the argument list. Why not last? The general feature is |
Going according to the requirements that i would like the new syntax to have:
I feel that all of those new syntaxes would have it's use cases in the normal flow, but i don't think piping, or at least genuine piping, is good to have in patterns. Many of the syntaxes you propose have great functionality, but also somewhat jarring syntax, that makes the pattern more difficult to understand. I think that if all the new syntax were to be added, it would indeed create the Regex problem, where with the more we try to do in the Regex, the difficulty of understanding of what it does grows exponentially. Meaning they would work wonderfully in small patterns, but medium sized patterns would be difficult to understand and the complex ones are basically foreign language I think this proposals could work nicely together:
The new syntax for
could probably work, but i feel that at least some of the functionality could be substituted by functions. One thing i definitely don't want to see in patterns is chained piping. As i've already stated, if you need to chain transformers multiple times in a pattern, which is essentially a testing function, you ARE doing something wrong, and should probably rethink what you are doing. Also piping is quite uncommon in programming languages, being mostly confined to functional programming. The syntax feels foreign and doesn't blend well with the current Dart syntax. Most developpers would have a hard time understanding what is actually happening in the pattern. While piping would work well in normal code, i don't feel like it would do great in patterns. That being said, most of my concerns are with making the new patternmatching syntax relatively easy to understand and follow. If such syntax is found, that supports the new proposals, while also mending well with the current pattern syntax, i would have no objections. New syntax needs to be easy to follow in order to be usable (at least according to me) PS. I still think that functions as patterns is a more elegant solutions to reusable patterns and some of the other issues, but that's just my opinion PS 2. I have also never written a programming language, so take it more as my personal opinion |
Sure, that's what code reviews are for. The moment you allow running arbitrary user code insider the pattern, then the doors are open. Users can run arbitrary code there. Then it's just a matter of making running reasonable code convenient. And we do allow almost arbitrary code, because we allow extension getters. The only thing we don't allow is parameterization, each individual operation needs its own getter, locked down at compile time (or have configurable getters, that you can configure before running the pattern - but it starts being more complicated than just writing |
I do understand that, but still think if we were to allow piping in patterns, and leave it at that, it would possibly kill the feature. It's true that what the developper does should be at his own discresion, and the issues he faces will be purely the fault of his own wrong planning and programming. That being said, piping in patterns in wrong circumstances adds a lot of complexity. So in order to alleviate at least some of the complexity, i propose adding new lint and formatting rules, that would strive to make the piping part more readable. This way, the user gets as much freedom as before, while also getting at least some kind of support for making it easier understand (formatting), some kind of mechanism to tell him that he is probably abusing the feature (linter) Here are my rules:
I think that's a good compromise between allowing piping in patterns, and trying to keep the readibility to the maximum. Making it suggestion, rather than banning the piping in pattern matching, ensures the user has the ability to do what he wants, while also supporting the user to not abuse the feature. This way it still aligns with the requirements i set out (readibility), while also aligning with your requirement to make the feature powerful. |
Problem
Dart currently offers powerful features with pattern matching like
if-case
,switch expressions
,destructuring
... which greatly enhance code readability and safety:However, I believe these feature could empower even more developers by introducing a mechanism to define reusable patterns, much like we define functions or classes:
This would be particularly useful in scenarios where the same pattern is used across multiple switch statements or within different parts of the application. Additionally, allowing the definition of pattern constants could help in enforcing consistency across the codebase and reducing redundancy.
Proposal
By adding the
patterndef
keyword in Dart that allows developers to define patterns once and reuse them multiple times. Here's an example of what this could look like:The text was updated successfully, but these errors were encountered: