Skip to content
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

Syntax: allow multiple expressions in a case condition #2239

Closed
wants to merge 1 commit into from

Conversation

asterite
Copy link
Member

This PR adds this syntax:

case exp1, exp2, ..., expN
when cond1, cond2, ..., condN
  body
end

This is just a simple syntax sugar of this:

if cond1 === exp1 && cond2 === exp2 && .. && condN === expN
  body
end

However, as with the regular case, you can use a type and, in the case of a variable, it will be restricted. For example:

a = rand < 0.5 ? 1 : "hello"
b = rand < 0.5 ? 1 : "hello"

case a, b
when Int32, Int32
  puts "Both ints: #{a + 1}, #{b + 1}"
when String, String
  puts "Both strings: #{a.upcase}, #{b.upcase}"
else
  puts "Other: #{a}, #{b}"
end

Or the "implicit obj" notation:

a = rand(1..10)
b = rand(1..10)

pp a, b
case a, b
when .even?, .even?
  puts "Both even"
when .odd?, .odd?
  puts "Both odd"
else
  puts "Meh"
end

An underscore is also allowed as an expression, and the comparison is just "true".

Finally, the compiler checks that the number of "when" expressions matches the number of "case" expressions, in case there is more than one expression.

@kostya
Copy link
Contributor

kostya commented Feb 27, 2016

why not a tuple?

case {exp1, exp2, ..., expN}
when {cond1, cond2, ..., condN}
  body
end

@asterite
Copy link
Member Author

@kostya You can already do some cases with a tuple, for example:

a = rand(1..10)
b = rand(1..10)

pp a, b
case {a, b}
when {1, 1}
  puts "Both one"
else
  puts "Other"
end

However, you can't do the .even?, .even? case, and you can't make the compiler restrict the types that way.

We could hardcode the compiler to understand the tuple case... but I think I prefer the syntax proposed in this PR, much less noise (no need to use curly braces everywhere), plus it's much simpler to implement.

@asterite
Copy link
Member Author

I forgot to mention that I want to add this because right now you can "pattern match" against a single type with a case or with a method:

def foo(x : Int32)
end

def foo(x : String)
end

# Option 1
foo(some_int_or_string)

# Option 2
case some_int_or_string
when Int32
when String
end

With more than one variable the only way you have is using a method. With this PR you could now also do it with a case.

@ozra
Copy link
Contributor

ozra commented Feb 27, 2016

This is a cool step towards fuller matching :-)

@bcardiff
Copy link
Member

Overall I like it.

  1. The compiler restricts that all the when clauses match in length, right? I bet that's the reason for _. But I would say that as long as the when conditions are not more than the case argument (and types make sense) we are fine. So we can think that trailing _ can be added automatically.
  2. Since you are willing to add _. I will say that it should be supported as method argument name that can't be referred inside the method body. Hence, used multiple times in the args. ;-)

@ozra
Copy link
Contributor

ozra commented Feb 28, 2016

I'd say trailing _ should be required too - it's an unnecessarily lazy risk of introducing bugs in your code to not state the full pattern.

@asterite
Copy link
Member Author

I agree with @ozra, it's better to be explicit about the number of when expressions. This is confusing:

case x, y
when Int32
  # Both Int32? Or just one of them? Maybe you forgot to add the second expression?
  # If you know the rules you know the answer, if not you might be confused
end

vs.

case x, y
when Int32, _
  # No possible confusion here
end

@bcardiff I can add _ for method arguments names, don't know why you want them so badly though (if it's for consistency then it's ok ^_^)

@asterite asterite force-pushed the feature/case_multiple_expressions branch from 9fe9cfe to bafa0c9 Compare February 28, 2016 14:29
@bcardiff
Copy link
Member

Ok item 1. dismissed. It can be misleading.
As for 2. I see _ as a match all pattern. I like how it is used in Haskell.

We have them in a, _, c = expr, in when _. I will like to se them available in procs, blocks and methods. I understand that in the later we need to create variables and is not the same as in the other cases. But I do see them as a consistency issue. For sure it can wait.

So 👍 for this PR.

@ysbaddaden
Copy link
Contributor

My mind prefers the example with tuples. Maybe because it deals with a single object (not N), which is less confusing, since when Symbol, String already means "match if object is a Symbol or a String". Maybe because I'm used to Erlang's {x, y} = {1, 2}.

Practical example for the "black hole variable" in methods (hidding noise):

logger.formater = Logger::Formater.new do |_, _, _, message, io|
  io << message
end

@asterite
Copy link
Member Author

@ysbaddaden It's true that it can be confusing because when now gets two different behaviours. We can use the tuple syntax, though I would hardcode that in the compiler so the number of when expressions matches those of the case (basically, if a curly brace comes after case, you are matching against multiple expressions, and the brace is required in every when). So you can do:

case {x, y}
when {0, 0}
  # ...
end

But you can't do

foo = {1, 2}

case {x, y}
when foo # compiler error, expecting {exp1, exp2}
end

For the above you'd have to do:

tuple = {x, y}
case tuple
when foo
end

If we use the {...} syntax, it becomes possible to match against different sets, so that's one point in favor of it, and the behaviour feels much similar to the current case behaviour:

case {x, y}
when {0, 0}, {0, 1}, {0, 2}
  # ...
end

And of course, you will still be able to do:

case {x, y}
when {.even?, .odd?}
  # ...
end

@asterite
Copy link
Member Author

Hm, we could actually make this work just fine:

foo = {1, 2}

case {x, y}
when foo
  # ...
end

It's just:

if foo === {x, y}
end

That way we can also keep backwards compatibility, in case someone was using this already.

@ysbaddaden
Copy link
Contributor

You were too fast, I was writing the following:

The con you expose can lead to confusion too. I think it depends on what we consider tuples to be in Crystal. An Array-like struct that retains each item type, or a lightweight struct for passing multiple variables... and now for matching. Destructuring tuples a, b = foo doesn't need curly braces on the left side, so it may be an argument for not having them in case/when expressions.

it becomes possible to match against different sets

❤️

@ozra
Copy link
Contributor

ozra commented Feb 28, 2016

I'm in favour of the tuple destructuring syntax instead of dilluting the meaning of when 1, 2, 3.

Optimally it should just work like:

foo = {1, 2, 3}
bar = {1, 2, 3}

# Any of these would match (which makes it a strange example)
case foo
when {.odd?, .even?, _}
  p "Yep"
when {_, 2, _}
  p "Yep"
when bar
  p "Yep"
else
  p "Yep"
end

The position in destructuring is simply used for index -> if (foo[0]?.is_odd? && foo[1]?.is_even? && true && foo.size == 3) - what level of generality (checking size also, etc.) is a question though...

Am I missing something?

@asterite
Copy link
Member Author

@ozra At first I thought making it work like that could work. However, what happens if we do this?

x = 1
case x
when {.even?, .odd?}
end

If we translate it to:

if x.size == 2 && x[0]?.try(&.even?) && x[1].try?(&even?)
end

that wouldn't compile, because there are no size and []? methods on Int32. So the translation should maybe include a .is_a?(Tuple), or x.responds_to?(:size) && x.responds_to?(:[]?). But I don't know, the simplicity of the original purpose of this PR is kind of lost... and I'm not sure this functionality will be used a lot (I guess it can be more useful in more dynamic languages). The original intention was to pattern match multiple expressions against multiple cases "in parallel", not to add an exponential combinatory of match cases.

That said, I'm now more inclined to either 1. keep the syntax like proposed in this PR, or 2. use the tuple syntax, but make it mandatory in every when expression (and also allowing multiple when tuple expressions). More inclined to do the second alternative, since it's a bit more powerful and the syntax makes it clear that it's different than the regular case expression (basically what I said here).

@ozra
Copy link
Contributor

ozra commented Feb 29, 2016

Yes, you're right, it's a bit of a Pandoras box. The "simple suggestion" is without doubt very useful as proposed. The tuple-like syntax still has an upper hand in my view, allowing multiple alternatives, and clearly separating it from multiple free expression alternatives case.

@asterite
Copy link
Member Author

asterite commented Mar 2, 2016

Closed in favor of #2258

@asterite asterite closed this Mar 2, 2016
@asterite asterite deleted the feature/case_multiple_expressions branch March 2, 2016 23:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants