dart-lang / language

Design of the Dart language
Other
2.67k stars 205 forks source link

Unusable inference in list pattern with `dynamic` value. #3956

Open lrhn opened 4 months ago

lrhn commented 4 months ago

Example code:

void main() {
  var [int a, String b] = [2, "a"] as dynamic;
  print("$a: $b");
}

The inference (or the implementation of it at least) infers a type argument for the list pattern of <Never>. That is, the line becomes:

  var <Never>[int a, String b] = <Object>[2, "a"] as dynamic;

and this type check fails.

If I write it as

  var [int a, String b] = [2, "a"] as List<dynamic>;

then the inference becomes

  var <dynamic>[int a, String b] = <Object>[2, "a"] as List<dynamic>;

and it works. List<dynamic> is OK because dynamic is assignable to each sub-pattern. The top-level dynamic is accepted statically because dynamic 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 not dynamic, do what we do today.

This only occurs at the top-level of the pattern. If I change it to:

var [int a, [int c, String b]] = [2, [2, "a"]] as List<dynamic>;

the match succeeds, even though the inner [int c, String b] list pattern should also have a matched value type of dynamic. 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 type M, it infers an element type for the list pattern as:

1.  Calculate the value's element type `E`:
       1.  If `p` has a type argument `T`, then `E` is the type `T`.
       2.  Else if `M` implements `List<T>` for some `T` then `E` is `T`.
       3.  Else if `M` is `dynamic` then `E` is `dynamic`.
       4.  Else `E` is `Object?`.

Number 3 means that var [int a, String b] = [1, "a"] as dynamic; should infer a list element type of dynamic.

Instead it uses Never. (And/or it casts <Object>[1, "a"] to List<Never> before getting there.)

That Never is likely not taken directly from the context type schema of the pattern, computed in step one to be List<Never> where Never is Down of int and String, 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 type List<Never>, which means that the RHS gets a static matched value type of List<Never> instead of retaining the dynamic, which then goes back and makes the pattern infer <Never> as its required element type (M implements List<T> for someT`), andNever` 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 pattern p and the matched value type M, that:

If p with required type T is in an irrefutable context:

  • If M is dynamic and T is not dynamic, then an implicit cast from dynamic to T is made before the pattern binds the value, tests the value's type, destructures the value, or invokes a function with the value as a target or argument. During destructuring, an implicit cast from dynamic is allowed, which may fail and throw an exception at runtime.

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:

  var [int a, String b] = [1, "a"] as dynamic;

should proceed as:

Then insert required downcast from dynamic, so the result after inference is:

  var <dynamic>[int a, String b] = (<Object>[1, "a"] as dynamic) as List<dynamic>;

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:

(One could then hope that a raw types could one day be inferred that way. List x = [1]; would introduce a context type of List<_>, the RHS would have a static type of List<int>, and the third step to infer the declared/required type as List<int>. Which you can effectively do today as var ([..._] && x) = [1];, which requires a List, but infers its element type. Shouldn't var (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.")

Ing-Brayan-Martinez commented 4 months ago

introduction

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.

The comparison

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.

Fibonacci Algorithm

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.

FizzBuzz Algorithm

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

Why this comparison is important

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

How my opinion can add value

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)
    }
}

Conclusions

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.

lrhn commented 4 months ago

@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",
    };
Ing-Brayan-Martinez commented 4 months ago

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

What is a tuple?

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");
}

What is a Record?

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.

About type inference

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.

Applying Pattern Matching

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
}

Conclusions

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

lrhn commented 4 months ago

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.

Ing-Brayan-Martinez commented 4 months ago

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

So what is this syntax?

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.

So what is a Group of Variables?

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

So what is not a Variable Group?

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.

How can we use groups of variables to create an algorithm?

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');
  }
}

How to use Pattern Matching on groups of variables

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');
     }
  }
}

conclusions

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

tatumizer commented 4 months ago

@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/

Ing-Brayan-Martinez commented 4 months ago

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.