Open lrhn opened 4 months ago
Greetings, recently watching the video that you have shared, I have been thinking if I should give my opinion on this, personally I think this is a good idea to simplify very complex algorithms using Pattern Matching, I think the syntax could improve.
Personally, I have investigated how the dynamics of Pattern Matching works in different programming languages to have a clear idea. Here I will leave the sources of information.
As a promoter of functional programming, I have decided to make a comparison of how to use Pattern Marching with this paradigm. I have been taking Haskell as a reference.
In Haskell
fibonacci :: Int -> Int
fibonacci 0 = 0
fibonacci 1 = 1
fibonacci n = fibonacci (n-1) + fibonacci (n-2)
In Kotlin
fun fibonacci(n: Int): Int {
return when (n) {
0 -> 0
1 -> 1
else -> fibonacci(n - 1) + fibonacci(n - 2)
};
}
In Swift
func fibonacci(n: Int) -> Int {
switch n {
case 0, 1:
return n
case let n where n > 1:
return fibonacci(n: n - 1) + fibonacci(n: n - 2)
default:
fatalError("Error: Input must be a non-negative integer")
}
}
In Dart
int fibonacci(int n) {
return switch (n) {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
};
}
As you can see, it is possible to create a simple Fibonacci algorithm using pattern matching and functional programming. In this way, the behavior of Haskell can be emulated, this is good news because it could have all the benefits of functional programming in the Dart language.
In Haskell
fizzBuzz :: Int -> String
fizzBuzz n
| n `mod` 5 == 0 && n `mod` 3 == 0 = "FizzBuzz"
| n `mod` 5 == 0 = "Buzz"
| n `mod` 3 == 0 = "Fizz"
| otherwise = show n
In Kotlin
fun fizzbuzz(n: Int): String {
return when {
n % 3 == 0 && n % 5 == 0 -> "FizzBuzz"
n % 3 == 0 -> "Fizz"
n % 5 == 0 -> "Buzz"
else -> "$n"
}
}
In Swift
func fizzbuzz(n: Int) -> String {
switch n {
case let n where n % 3 == 0 && n % 5 == 0:
return "FizzBuzz";
case let n where n % 3 == 0:
return "Fizz";
case let n where n % 5 == 0:
return "Buzz";
default:
return "\(n)";
}
In Dart
String fizzbuzz(int n) {
if (n % 3 == 0 && n % 5 == 0) {
return 'FizzBuzz';
} else if (n % 3 == 0) {
return 'Fizz';
} else if (n % 5 == 0) {
return 'Buzz';
} else {
return '$n';
}
}
As you can see, this more complex syntax for defining the FizzBuzz algorithm is a bit difficult for the Dart language. On the other hand, for Swift and Kotlin it is possible to use a slightly advanced Petter Matching because these languages have functional programming by default, later I will propose a possible solution
This comparison aims to demonstrate the level of maturity of the Pattern Matching syntax for the Dart language, taking into account a case of using a real algorithm, with functional programming. The Dart language has made important advances and this is a clear example
Why compare Dart language with Swift, Kotlin?
This is important because Dart is an industry-accepted language for creating desktop and mobile applications, and it is only fair to compare it with languages that are used for the same purposes.
Why do we compare with Haskell?
To demonstrate how Dart language adopts the functional programming paradigm, languages like Kotlin and Swift use this paradigm by default, it is created to always use this paradigm
I think that the Dart language could optimize its Pattern Matching syntax if it took into account the type of data. This allows more control over the data to be compared because it is possible to determine its type if for some reason the variable to be compared is a dynamic
, Another important point is to have an alias x
to be able to make more complex comparisons within a when
to create a more advanced Pattern Matching, all these changes can be seen in this way
String fizzbuzz(int n) {
return switch(n) {
when (int x && x % 3 == 0 && x % 5 == 0 ) => "FizzBuzz" //it is assumed (True && True && True == True)
when (int x && x % 3 == 0) => "Fizz" //it is assumed (True && True == True)
when (int x && x % 5 == 0) => "Buzz" //it is assumed (True && True == True)
_ => "$n" //it is assumed (False == False)
}
}
In this way it is possible to have more complex patterns with an understandable syntax, another example of how to verify types would be
bool isNumber(dynamic n) {
return switch(n) {
when (int x) => True //it is assumed (True == True)
when (num x) => True //it is assumed (True == True)
when (double x) => True //it is assumed (True == True)
_ => False //it is assumed (False == False)
}
}
This is just my vision of how to solve this problem, you can use information in the most convenient way for you.
You have the power to choose and make the best possible implementation according to your capabilities and long-term vision.
@Ing-Brayan-Martinez I think your post is off-topic in this issue, which is about a very specific detail of type inference in patterns. You can consider moving it to a separate issue. However, what I think you're asking for already exists:
String fizzbuzz(int n) =>
switch(n) {
int x when x % 3 == 0 && x % 5 == 0 => "FizzBuzz",
int x when x % 3 == 0 => "Fizz",
int x when x % 5 == 0 => "Buzz",
_ => "$n"
};
Personally I'd write it as
String fizzbuzz(int n) =>
switch ((n % 3, n % 5)) {
(0, 0) => "FizzBuzz",
(0, _) => "Fizz",
(_, 0) => "Buzz",
_ => "$n",
};
I'm impressed, I was really looking for information on how to create the FizzBuzz algorithm, I reviewed the official documentation and I couldn't find an example that would tell me how to do it. I also asked Gemini, Copilot, You.com, Perplexity and I didn't find any answer either.
Regarding the relationship between my opinion and the main issue, of course it is related and now I can explain my point of view, I could be wrong, I ask for your patience.
Now we can talk about the concept of Pattern Matching in Tuples and Records
As I have read, a tuple is a list of elements of different types that can be accessed through an index, from my point of view a tuple should be defined in this way:
void main() {
[int, String] tuple = [2, "a"];
if (tuple is List<dynamic>)
print('yes....'); // print: yes....
}
print(tuple[0]); // print: 2
print(tuple[1]); // print: a
}
From my point of view I think that the Dart language should not allow writing var [int a, String b] = [2, "a"]
which is intended to be done with this syntax, I could be wrong, but that should be a Syntax error, we cannot define that code as a Tuple, what concept are you trying to express? I don't know
void main() {
var [int a, String b] = [2, "a"] as dynamic; // sintax error
print("$a: $b");
}
A record is an anonymous immutable object that groups other variables that can be objects dynamically and is defined in this way
void main() {
(int, String) record = (2, "a");
//if (record is Record)
// print('yes....'); // print: yes....
//}
print(record.$1); // print: 2
print(record.$2); // print: a
}
As you can see, I have had to comment out some lines to avoid syntax errors. I don't understand why this happens. It would be important to review that at another time because it is outside the scope of this issue.
I believe that for type inference to work it is necessary to clearly define the concepts that represent the base types of the language. That's why I explain that it is a tuple and a record so that we all understand the same idea.
Since the concepts are already defined, let's see how we can apply Pattern Matching in Tuples and Records
Pattern Matching in Tuples
bool isMytuple(List<dynamic> n) {
return switch(n) {
[int, String] x when x[0] == 22 && x[1] == 'Maria' => True,
_ => False
};
}
void main() {
[int, String] tuple1 = [22, 'Maria']
[int, String] tuple2 = [16, 'Yoel']
print(isMytuple(tuple1)); //print: True
print(isMytuple(tuple2)); //print: False
}
Pattern Matching in Record
bool isMyRecord(dynamic n) {
return switch(n) {
(int, String) x when x.$1 == 22 && x.$2 == 'Maria' => True,
_ => False
};
}
void main() {
(int, String) record1 = (22, 'Maria')
(int, String) record2 = (16, 'Yoel')
print(isMyRecord(record1)); //print: True
print(isMyRecord(record2)); //print: False
}
In this last comment I have managed to unify all the concepts, this allows me to express my vision on how this problem would be resolved.
Along the way I may be wrong, or make a mistake. I just want to leave useful information that can serve as a reference to resolve how type inference interprets all these concepts.
I'm going to limit myself to answering one last comment to avoid an endless debate that generates a waste of time.
I think I should become a Google Developer Expert, I think I'm ready, what do you think, do you recommend I follow this path? I personally don't have a job at the moment, now I have time to study
AIs are awesome for some things, but they are not a source of truth. And they don't know everything. They cannot replace actually studying a thing yourself.
Dart does not have a concept of "tuple" based on lists. Heterogenously typed collections of values should generally be represented by records. Calling the list pattern a tuple is a red herring.
However, Dart does have list patterns, and implicit downcast from dynamic
so doing:
dynamic value = ...;
if (value case <dynamic>[int v1, String v2]) ...
is valid pattern matching. It's basically equivalent to
if (value is List<dynamic> && value. length == 2) {
var v1 = value[0];
if (v1 is int) {
var v2 = value[1];
if (v2 is String) ...
}
}
Because of the implicit downcast from dynamic, you're also allowed to do
var [int v1, String v2] = value as List<dynamic>;
You may not like it, but it is currently allowed and it works.
This particular issue is about the type inference that happens for:
var [int v1, String v2] = value as dynamic;
Nothing more, nothing else. Your comments may be relevant to a larger discussion about which patterns should be allowed, but this pattern is allowed today, and it has an impractical behavior in a specific corner case. This issue is about that corner case, not whether the pattern should be allowed in general.
That particular use case where I encountered the issue was cross-isolate group communication, where records cannot be used.
Apparently I made a mistake in interpretation, I thought you wanted to represent a tuple, but we are talking about a different concept, I think we should define it better to avoid confusion
var [int v1, String v2] = value as dynamic;
If you asked me what this syntax is, I would tell you that it is a "variable group" that would be my interpretation.
A variable group represents an anonymous set of variables, mutable or immutable, allocated in a nearby memory space, where the variables can be accessed directly.
The stored values are conceptually related according to the business logic that is being implemented, that is, they have a purpose to be grouped to represent abstract business concepts, for example
var [String key, int value] = value as dynamic;
As you can see I have defined a variable group that represents the abstract concept of the values of a Map<String, int>
, if you observe each value is stored in its own variable with a different memory address accessible directly, so we can extract the values of a Map<String, int>
and store them in groups of variables
In order to define what a group of variables is, let's understand that it is not a group of variables.
Definitely, a variable group is a mutable or immutable and anonymous space in memory that groups variables in a given period of time.
One way to use a variable group is to represent the key and value of a Map<String, int>
when we try to iterate with for in
it would look like this
void main() {
Map<String, String> gifts = {
'first': 'partridge',
'second': 'turtledoves',
'fifth': 'golden rings'
};
for (final [String key, String value] in gifts) {
print('key: $key, value: $value');
}
}
It is not possible to use Pattern Matching directly to a variable group but it is possible to use its grouped variables. This is because each variable can be accessed individually directly.
bool isAdult(dynamic n) {
return switch(n) {
int x when x >= 18 => True,
_ => False
};
}
void main() {
Map<String, int> persons = {
'Maria': 22,
'Yoel': 16,
'Britany': 24
};
for (final [String key, int value] in persons) {
if (isAdult(value)) {
print('key: $key, value: $value');
}
}
}
The concept it tries to represent could be defined this way, I think the correct way to define it should be
var [String key, int value] = value as dynamic;
The other way to define it would be a syntax error because this is not a list
var [int v1, String v2] = value as List<dynamic>; // sintax error
In all this we have touched on many important points. If you think it is appropriate, you could open another separate problem to be able to discuss it. Some relevant topics would be
Personally, I am limiting myself in opening new issues because most have been rejected, I must adapt and understand what is the correct way to raise a new issue because it may happen that there are errors of interpretation
@Ing-Brayan-Martinez : I'm not sure whether your satire is intentional or not, but I think it is 😁. If so, you might be interested in learning from the masters of the genre. E.g. https://americanliterature.com/author/mark-twain/short-story/how-i-edited-an-agricultural-paper/
Greetings, it's good that you are having fun with our comments, I'm sorry to disappoint you but we are talking about serious topics, because the example presented in this thread is very abstract.
Imagine that a programmer with relative experience has had difficulties understanding the proposed syntax, imagine someone who is starting at university, someone who is learning the basic concepts of programming, we must think about usability
and I think that admitting errors is a positive activity because it is an indicator that there are things to improve, try to give a definition to the syntax mentioned to have a basic concept that is easy to remember and use
Precisely because of these comments I am limiting myself in proposing new ideas to avoid errors of interpretation, at this point trivial topics are already being touched on that do not add any value so I am thinking of abandoning the thread I have more important things to do
If you are looking for some reading, I can recommend the following:
If you are looking for some type of training, I invite you to listen to this
If you are looking for some kind of discussion on programming topics, I invite you to join my discord
I'm definitely going to leave the thread because I already made the comments on the main topic, right now I have nothing more to contribute. Have a great day.
Example code:
The inference (or the implementation of it at least) infers a type argument for the list pattern of
<Never>
. That is, the line becomes:and this type check fails.
If I write it as
then the inference becomes
and it works.
List<dynamic>
is OK becausedynamic
is assignable to each sub-pattern. The top-leveldynamic
is accepted statically becausedynamic
is assignable to any list pattern, but the inferrence of<Never>
is always going to get in the way.Suggestion: If the matched value type of a list pattern (and probably map pattern) with no explicit type arguments, is
dynamic
, then infer<dynamic>
as the type argument type. Otherwise, if the matched value type is notdynamic
, do what we do today.This only occurs at the top-level of the pattern. If I change it to:
the match succeeds, even though the inner
[int c, String b]
list pattern should also have a matched value type ofdynamic
. Maybe it's just a bug. (If it's just a bug in the implementation, we should fix it.)I have checked more. I believe it is a bug, with implementations not matching the specified behavior. It's likely due to an implicit downcast being done too soon. (Someone please verify).
In (step three of) type inference of patterns, for a
List
pattern p with matched value typeM
, it infers an element type for the list pattern as:Number 3 means that
var [int a, String b] = [1, "a"] as dynamic;
should infer a list element type ofdynamic
.Instead it uses
Never
. (And/or it casts<Object>[1, "a"]
toList<Never>
before getting there.)That
Never
is likely not taken directly from the context type schema of the pattern, computed in step one to beList<Never>
whereNever
is Down ofint
andString
, because that schema is only used for inferring the static type of the matched value.More likely there is an implicit downcast from
dynamic
to the context typeList<Never>
, which means that the RHS gets a static matched value type ofList<Never>
instead of retaining thedynamic
, which then goes back and makes the pattern infer<Never>
as its required element type (M implements List<T> for some
T`), and
Never` is (trivially) a valid element type for all its sub-patterns, so type checking succeeds.That's somewhat consistent with what we do for other assignments, inserting the implicit downcast during inference of the RHS, but it's incorrect for pattern declarations, where the downcast is specified to happen later, after we have found the required type of the pattern.
The spec stats, after step 3 has inferred a required type,
T
, from the patternp
and the matched value typeM
, that:A pattern declaration is an irrefutable context, so the downcast from
dynamic
must be done to the required type from step 3, not the context type schema from step 1, and the type checking in step 3 should use the un-coerced type of the initializer expression (dynamic
) here.So, inference for:
should proceed as:
List<Never>
, withNever
being the lower bound ofint
andString
.as
ignores context type schema, so RHS becomes<Object>[1, "a"] as dynamic
with static typedynamic
.E
isdynamic
because matched value type isdynamic
, sovar <dynamic>[int a, String b] = ...
.Then insert required downcast from
dynamic
, so the result after inference is:The current implementation probably inserts the
as List<Never>
during inference of the RHS, which it shouldn't. (So definitely @stereotype441!)More generally, we could assume that all declaration assignments has a third step which infers the required type of the variable, which happens before coercions:
int x = ...
the required type is the declared type (and the same as the context type), so when we downcast to the required type, we downcast is to (the same type as) the context type.var x = ...
the required type is the inferred declared type, which is the type of the RHS (unlessNull
), so no downcast is ever required.var pattern = ...
, the required type inferred from the pattern and matched value type may differ from both the context type and the static matched value type, and that is what we downcast to.(One could then hope that a raw types could one day be inferred that way.
List x = [1];
would introduce a context type ofList<_>
, the RHS would have a static type ofList<int>
, and the third step to infer the declared/required type asList<int>
. Which you can effectively do today asvar ([..._] && x) = [1];
, which requires aList
, but infers its element type. Shouldn'tvar (List() && x)
= [1];also work? It seems object patterns use I2B instead of
_` for the schema, should we change that? "If the type the object name resolves to is generic, and no type arguments are specified, then instantiate to bounds is used to fill in provisional type arguments for the purpose of determining the context type schema.")