-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Exhaustive case #8424
Exhaustive case #8424
Conversation
Also thinking a bit more about this, I asked myself why make |
How does this handle something like a = 1 || "foo" || :bar
case a
when Int32
puts a*2
when "foo"
puts "yay"
end ? |
@jhass the compiler only checks types and enum members, never other values. So in that case you'll have to add an |
And that's why I feel having a distinct construct that can only have types xor enum members in its " |
Isn't that a little too broad? Enforcing exhaustiveness when it can be formally verified is great, maybe boring, but good nonetheless since it can avoid else raise unreachable branches and help catch bugs down the road (adding type to union, value to enum) but maybe it could be skipped for unverifiable cases, because it replaces raising branches for empty else branches for all mixed cases, which kinda defeats some of the purpose. I'm not advocating for another keyword or construct. I may even be worrying about something barely existent. |
I am because to me |
e057f69
to
01260f3
Compare
Well, if you all look at my original proposal in #8001 I said I prefer a new keyword or a new construct. But then:
So so far me, @straight-shoota, @RX14 and @bcardiff agree with this change, and that's a big part of the core team. That said, we have a slightly different option, inspired by the new pattern matching introduced in Ruby 2.7. The trick is to avoid introducing a new keyword (thus introducing a breaking change) by using something other than So, the idea is: # the case we all use and love
case exp
when ...
when ...
end but: # the exhaustive case
case exp
of T1
of T2
end This new However, I'm not entirely sold on this. My main concern is that sometimes you want to mix types, values and so on, and still avoid having to remember to put an Another concern is that it's not a new keyword that can conflict with others, but you do have to learn something new. The way it's proposed in this PR is very simple:
Also think about this not as a change to the language but as if it were like that from the beginning. That is, let's think about 1.0 as something final and ready to be used, and not as a cumulative changes between 0.x and 1.0. |
Oh, I forgot to include the pun I had in my head for like half an hour until I got to the computer: I think this conversation is pretty much exhausted. |
Can it be # the exhaustive case
case exp
with T1
with T2
end I like |
More data. I tried it in:
So my conclusion is that:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is amazing! Thank you so much!
# we must cover. | ||
# Note: a user could override the meaning of such methods. | ||
# In the future it would be wise to mark these as non-redefinable | ||
# so this checks are sounds. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enum constructors should reject invalid values by default too, of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oooh... I see. Yes, probably.
spec/std/ecr/ecr_lexer_spec.cr
Outdated
@@ -6,47 +6,47 @@ describe "ECR::Lexer" do | |||
lexer = ECR::Lexer.new("hello") | |||
|
|||
token = lexer.next_token | |||
token.type.should eq(:STRING) | |||
token.type.should eq(ECR::Lexer::Token::Type::String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weird idea, perhaps introduce .should be(&.string?)
to the DSL?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'd like something like that but we need to think how to implement it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just be(& : T -> Bool)
??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I don't understand how would that work. Also with &
it can't work, it'll need to capture the block, but there's no way to figure out what the T
is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ohhh, yeah, you're right...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up doing a trick used by @bcardiff in one of his PRs: use a file-private method to convert a symbol to enum. I think it's a good workaround, and usually you only need this in a spec file.
@@ -207,12 +209,16 @@ module Crystal | |||
|
|||
def source_lines(filename) | |||
case filename | |||
when Nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when nil
should be the same as when Nil
to the exhaustiveness checker.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, done!
Let me know if you'd like me to move forward with this. /cc @bcardiff @straight-shoota @waj |
(whoops, thought that was the button to add my review) |
I do prefer an explicit construct for exhaustive checks. Having a separate syntax could:
My opinion on #8001 (comment) was to avoid different semantics on |
Sounds good. What do you think if we use the same syntax as in Ruby? case type
in Foo
in Bar
end But I don't know how we'll make it work for enums. Maybe we can allow this: case value
in :foo
in :bar
end and translate that to: if value.is?(:foo)
elsif value.is?(:bar)
end and implement But for now we can focus on the syntax. |
I don't follow the issue with enums. Whatever syntax is supported to express the expected value, I would not expect that to affect much. What am I missing? |
@bcardiff How do you propose expanding So if we are going to go with the |
Why not |
@asterite In the implementation aspect: I would not do an expansion. I would leave the case until the codegen phase. I think that if later we introduce more a pattern matching way this will be more comfortable than rewriting to Regarding the case cond # cond : T for any T
in expr_1
...
in expr_2, expr_3
...
end Note: All a. types for (a), if If (a) and (b) are mixed it is a compiler error (note that this may be detected far after parsing, since If in (a) a hierarchy of types is used it would be ideal to check using the more specific one first, but otherwise, we can use the explicit order. So some cases might not be reachable. WDYT? I think that covers the main use cases. |
Sounds good to me, but comments follow:
Okay, but this sounds like making the compiler's code even more complex... but if we go this route, I'm not going to implement soon (no time these days for such a big task, sorry).
Oh, I thought we were going the symbol route, so you could do: enum Color
Red, Green, Blue
end
color = ...
case color
in :red
# ...
in :green
# ...
in :blue
# ...
end but I guess using
I don't think we should check exhaustivity in hierarchy types because external code can add new types and that would mean it breaks existing code: class Foo
end
class Bar < Foo
end
foo = FooFactory.build
case foo
when Foo
# ...
when Bar
# ...
end
# Oops, someone adds a new type, breaks the above code:
class Baz < Foo
end What about Lines 142 to 149 in 7ffc459
Then @RX14 mentioned Also, maybe we should discuss all of this in a separate issue? Not sure. |
Yes please. |
I like that! case cond
is Foo
# code
is :bar
# code
is false
# code
end Feeld really nice to read and understand imo |
We always agree stuff then someone implements it and then someone comes in at the last second and says "wait no throw that all away". Can we not derail this feature again? I've been guilty of it too, but, it shouldn't happen. |
I agree. Many of the new modern languages have a form of exhaustive |
I have a failing case in Minitest:
|
For permalink, replace "master" with "b52e34584a68248e489aad9dfafd6235d4f3fce4" above. |
@ysbaddaden It's working fine. When you do That's not to say that it's a nice situation. But it's not a bug in the exhaustive check. Essentially, you can replace |
Wouldn't this show up if you printed the types? |
The error already tells you that Exception is not covered. |
Urgh, really? I didn't care much about this change (near zero benefit) but now it's totally falling into the annoying, whiny, compiler (negative benefit). Whenever I write a case I'll probably have to add an This is almost as annoying as being forced to specify the return type when implementing abstract methods. This is so boring and sometimes unacceptable, that I usually remove the return type annotation of abstract method —IMO abstract methods should just be informational, never enforced. Overall, warnings are just annoying. I much preferred the "no warnings" stance. I'm not fond of where this is all going. The nice & fun of Crystal is slowly eroding away 😞 |
@ysbaddaden I have an idea for not merging the types into the base type when there's a type annotation in instance or class variables, and that will at least don't force a useless Where there many places where you needed to add an |
@asterite I'm checking some other projects, but so far, yes, everywhere I use a I thought I could benefit from exhaustiveness checks in a few places, replacing Node with specific unions (if Node+ merging was to be corrected for exhaustive cases), but looking closer, I don't think so. I admit I abuse if (value = node.value).is_a?(Something) # bad: too many nested actions on single line
end
case value = node.value # much better (to me)
when Something
end I also have lots of: case something
when .foo?, .foo2?
when .bar?
end Where I must now specify en empty if something.foo? || something.foo2?
elsif something.bar?
end So far, the change is |
Yeah, I found fixing these warnings a bit annoying, too. But to put that into perspective, it actually wasn't too frequent, or less than I would have expected. Obviously, we're currently seeing lot's of this because of the initial adaptation. This is going to ease down quickly. |
I just fixed a SDL project. Crystal wants to check the exhaustiveness of the keyboard symbols enum (236 symbols) and of the event types (23 classes): loop do
case event = SDL::Event.wait
when Event::Quit
exit
when Event::Keyboard
case event.sym
when .q? then exit
when .s? then save_screenshot
else # skip 234 symbols
end
else
# skip 21 event types
end
end So far, my impression is confirmed: unless you specifically program for exhaustive checks, you will be annoyed by the warnings, and forced to write an empty |
Well, if you take a look at my original proposal in #8001, I proposed using a different keyword, I still think having two constructs is better, and we have time to change this... if everyone else agrees. I personally think |
Also I know |
It would be a feature to looks weird, like |
Sorry for the rant. I'll see whether it's boring to use compared to annoying to fix. In Rome, for example, it's not that bad. There are a bunch of I got another false positive, thought, this time while iterating an The SDL example was expected to make the feature look awkward: a human wouldn't consider so many missing classes and enum symbols, but how should a computer program deal with ithis? It would be awkward and confusing to use arbitrary ratios (ratios for my example are 8.3% and 0.85%). I still stand by my original comment: it's too broad. The compiler shouldn't whine when it can't prove exhaustiveness. That's a job for a linter tool, which is meant to complain. Otherwise it's annoying. |
That tuple syntax shouldn't work. I'm surprised it works at all. The special tuple syntax only works if you specify a tuple literal in the case expression. In your case, you can use Tuple(Symbol, Symbol) and it should work fine and the compiler shouldn't complain. |
For big enums we could always have a way to mark them as "don't check for exhaustiveness". |
https://travis-ci.org/github/oprypin/crsfml/jobs/677030266#L1597 as an example of enums where exhaustive case is never useful. |
@oprypin I'm not sure what we can do with that. I guess you can add Any other idea in mind? |
Your idea!
|
The thing about "implicit |
Oh, my idea was to always require an else for open enums like http status. After thinking about it a bit more, I can't see how not requiring exhaustiveness in some cases will work because then you have two modes of operations. For now that redudnant else will have to stay there.. |
Part of: #8001
Related: #4837, #4870
This PR is the initial step towards having
case
be exhaustive.What that means is that the compiler must be able to prove that all
when
cover all cases of the expression given tocase
, or otherwise anelse
must be given.As an initial step, if the compiler can't prove that all
when
are covered a warning will be issues. This will help the transition of shards authors. The warning will eventually become an error.The compiler will be able to prove that all types in a union or single type are covered (not including inheritance because inheritance is open). It can also prove that all enum members of a given enum are covered.
Some things missing:
Flags
enums in the checkcase
over a tuple literalbut I didn't want to spend time and effort on those things until we all agree that this is the direction we want to take.
An example of how it works:
In the example above the compiler will issue a warning saying that the case is not exhaustive and
Char
is missing to be covered.Note that right now if you cover all cases the compiler won't issue a warning but there will still be an implicit
else nil
, but eventually that will change toelse unreachable
of some sort.Then this PR makes all
case
be exhaustive by adding anelse
clause when needed. While doing that I felt:else
were just "yeah, I know I didn't want to cover this case" (but what about someone other than me? would they know?)else
made me stop and think "hm, why was this left out?"else
I knew I ignored in the past, but this time around I couldn't remember why I did that and I left a "TODO" to check itelse
were missing a few types or enum cases and I added them for clarityenum
so the compiler could check I covered all casescase
toif
when I considered it was more readable/clearIt was a bit tedious to add all the
else
in one go, but doing it as you code is probably not tedious and it can actually be helpful. I find that it's a bit tedious that the compiler will ask you to write anelse unreachable
for things that it knows can't be reached, so the state before this PR also has some bad things to it.There were around 250 warnings in this entire repo. Considering that cloc says there are 218466 lines of pure Crystal code (excluding comments), it's 1 warning per 874 lines of code, which I think isn't that bad. Also 30 of those warnings were in the lexer when scanning identifiers, so the distribution might actually be like 1 warning per 1000 lines of code (well, it depends on how often you use
case
... let's say it depends on the case ;-)).I also wanted to see what warnings I needed to fix for a couple of shards, so I chose
shards
,crystal-db
andcrystal-pg
. To my surprise, no warnings were issued, which is a good sign.But please, if you can, try the compiler in this PR against your project and see if the warnings make sense and are generally good to have.