Skip to content

Could a constant map that has a non-infinite type as key know that it is exhaustively defined? #1717

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

Closed
mateusfccp opened this issue Jun 30, 2021 · 7 comments
Labels
enhanced-const Requests or proposals about enhanced constant expressions feature Proposed language feature that solves one or more problems

Comments

@mateusfccp
Copy link
Contributor

mateusfccp commented Jun 30, 2021

Consider the following:

enum Letter { alpha, beta, delta }

const letterNames = {
  Letter.alpha: 'α',
  Letter.beta: 'β',
  Letter.delta: 'Δ',
};

String letterToString(Letter letter) => letterNames[letter];

The code above throws, because letterNames[letter] is String? instead of String.

I know I can simply use bang (!) in this case, but it's not costless, and I feel that the compiler could be able to infer that this case is exhaustive and promote the expression to String.

Is this something doable/viable?


Edit:

I changed the title because it could be expanded to any type whose value is not virtually infinite, like bool, or even to subtype relations, something like in the following:

abstract class A {}
class B implements A {}
class C implements A {}

const letterNames = <T extends A, String>{
  B: 'B',
  C: 'C',
};

String letterToString(Letter letter) => letterNames[letter];
@mateusfccp mateusfccp added the feature Proposed language feature that solves one or more problems label Jun 30, 2021
@enzo-santos
Copy link

I had a similar idea a couple of minutes ago and I was going to open a similar issue when I found yours. My idea was to extend it to all constant values. For example, in the snippet below, I think that the compiler, given that romans is constant, should be able to infer that romans[1] is not null:

const Map<int, String> romans = {1: "I", 5: "V", 10: "X", 50: "L"};
print(romans[1].toLowerCase());
// Raises Error: Method 'toLowerCase' cannot be called on 'String?' because it is potentially null.

Maybe this makes more sense with enums, but it's just my two cents.

@mateusfccp
Copy link
Contributor Author

mateusfccp commented Jun 30, 2021

@enzo-santos In your case, however, you are not exhausting int (and it would be virtually impossible). This would be possible only if we would have something like dependent types, where we would be able to make a type depend on it's value and, thus, limit its extent so it could be exhaustively checked.

Even so, in your specific case, as you are using a constant in [], the compiler could be able to infer that it has been declared.

@mateusfccp mateusfccp changed the title Could a constant map that has an enum as key know that it is exhaustively defined? Could a constant map that has a non-infinite as key know that it is exhaustively defined? Jun 30, 2021
@mateusfccp mateusfccp changed the title Could a constant map that has a non-infinite as key know that it is exhaustively defined? Could a constant map that has a non-infinite type as key know that it is exhaustively defined? Jun 30, 2021
@Levi-Lesches
Copy link

Levi-Lesches commented Jul 1, 2021

enum Letter { alpha, beta, delta }

const letterNames = {
  Letter.alpha: 'α',
  Letter.beta: 'β',
  Letter.delta: 'Δ',
};

String letterToString(Letter letter) => letterNames[letter];

I use this pattern all the time, but I wanted exhaustiveness checking, so I switched to switch:

enum Letter { alpha, beta, delta }

String letterToString(Letter letter) {
  switch (letter) {
    case Letter.alpha: return 'α';
    case Letter.beta: return 'β';
    case Letter.delta: return 'Δ';
  }
}

But that's a little verbose, especially if you repeat this pattern. So I'm looking forward to switch expressions as part of pattern matching, which would make the following possible:

enum Letter { alpha, beta, delta }

String letterToString(Letter letter) => switch (letter) {
    case Letter.alpha => 'α';
    case Letter.beta => 'β';
    case Letter.delta => 'Δ';
  }
}

Seeing as switch statements offer exhaustiveness-checking (to a point), would this be helpful?


I changed the title because it could be expanded to ... subtype relations

For that, Dart would have to have guarantees in place about who can subclass a type. Consider these two files in separate packages, by different authors.

// shapes.dart
abstract class Shape { } 
class Square extends Shape { }
class Circle extends Shape { }

// Using pattern-matching syntax
String getShapeName(Shape shape) => switch (shape) {  
  case Square => "Square";
  case Circle => "Circle";
}
// my_project.dart
import "package:shapes/shapes.dart";

class Rhombus extends Shape { }

void main() {
  print(getShapeName(Rhombus()));
}

What gets printed? Should getShapeName throw an error? But it does consider all Shapes in the package! Stuff like this means we need to wait for #1696, which has keywords that tell Dart who can extend, implement, instantiate, and mix-in a type. In this example, Dart can know for sure that Shape is meant to be self-contained and Rhombus should not be allowed, or that Shape can be extended by anyone and so getShapeName cannot be exhaustive.

Hope that helps!

@lrhn
Copy link
Member

lrhn commented Jul 1, 2021

I would also recommend using a switch statement instead, because switch statements is the only place where Dart does something special for enums. In all other settings, an enum is just a type like any other, and "completeness" isn't considered.

For the map lookup, the compile could potentially recognize that the Map object is a constant platform Map, and that it has entries for all the enum values, and those values are non-nullable, so the lookup will definitely give something non null.
It's not something we'd put into the language specification, though. That's far too specific an analysis, so at most, it would allow the compiler to optimize away the ! that the language requires you to add to make the types check out.
(But, most likely, that's not an optimization that would happen often enough that it's worth the compiler's limited time to check for it).

@mateusfccp
Copy link
Contributor Author

mateusfccp commented Jul 1, 2021

Thanks, @lrhn and @Levi-Lesches.

I already make use of switch in this specific case, it was only an example to bring up the topic and see if this makes sense and is somehow viable.

For that, Dart would have to have guarantees in place about who can subclass a type. Consider these two files in separate packages, by different authors.

I am almost sure that I read something related in some issue/proposal about patterns, so I was considering that they will have to solve these kind of issues anyway...

@Levi-Lesches
Copy link

Yep, the issue I linked (#1696) cites pattern matching as one of the motivating reasons for limiting who can extend a class. That, and the optimized switch statement make me think this is probably a duplicate of pattern matching. I've only looked at the proposal doc I linked, but I see that #546 is sort of an umbrella issue that you might be interested in.

@mateusfccp
Copy link
Contributor Author

This does not falls strictly in pattern matching, so it is not a duplicate, although I agree that it could be labeled as part of the patterns feature. However, considering @lrhn comment, I think this is not something that the team is interested in or will spend any resource in, so I am going to close.

@eernstg eernstg added the enhanced-const Requests or proposals about enhanced constant expressions label Dec 1, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhanced-const Requests or proposals about enhanced constant expressions feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

5 participants