The case
expression in Ruby works like this (see section 11.5.2.2.4 The case
expression of the ISO/IEC 30170:2012 Information technology — Programming languages — Ruby specification for details):
case object_of_interest
when category1
some_expression
when category2
some_other_expression
when category3
another_expression
else
an_entirely_different_expression
end
is (roughly) equivalent to
temp = object_of_interest
if category1 === temp
some_expression
elsif category2 === temp
some_other_expression
elsif category3 === temp
another_expression
else
an_entirely_different_expression
end
The ===
operator (I call it the case subsumption operator, but that is something I made up, not an official term) checks whether the right-hand operand can be subsumed under the receiver. What do I mean by that? Imagine, you had a drawer labelled with the receiver, would it make sense to put the right-hand operand into it?
So, if you have
foo === bar
this asks "if I have a drawer labelled foo
, would it make sense to put bar
into it?", or "if I interpret foo
as describing a set, would bar
be a member of that set?"
The default implementation of Object#===
is more or less just simple equality, it looks a bit like this (see also section 15.3.1.3.2 Kernel#===
of the ISO/IEC 30170:2012 Ruby Language Specification):
class Object
def ===(other)
equals?(other) || self == other
end
end
This is not exactly helpful, because it does not show what I mean by case subsumption. For that, we have to look at some of the overrides. For example, Module#===
(see also section 15.2.2.4.5 Module#===
of the ISO/IEC 30170:2012 Ruby Language Specification):
mod === obj
→ true
or false
Case Equality—Returns true
if obj is an instance of mod or an instance of one of mod's descendants. Of limited use for modules, but can be used in case statements to classify objects by class.
So, it is equivalent to
class Module
def ===(other)
other.kind_of?(self)
end
end
So, it implements the question "If I had a drawer named mod
, would it make sense to put other
in it" as "Is other
an instance of mod
", and we can see that it makes sense:
Integer === 3 #=> true
Integer === 'Hello' #=> false
String === 3 #=> false
String === 'Hello' #=> true
If I have a drawer labelled Integer
, does it make sense to put 3
in it? Yes, it does. What about 'Hello'
? No, it does not.
Another example is Range#===
(see also section 15.2.14.4.2 Range#===
of the ISO/IEC 30170:2012 Ruby Language Specification):
rng === obj
→ true
or false
Returns true
if obj is between begin and end of range, false
otherwise (same as cover?
).
So, it implements the question "If I had a drawer named rng
, would it make sense to put other
in it" as "Does the range rng
cover other
", and we can see that it makes sense:
0..2 === 1 #=> true
0..2 === 3 #=> false
If I have a drawer labelled 0..2
, does it make sense to put 1
in it? Yes, it does. What about 3
? No, it does not.
The last example is Regexp#===
(see also section 15.2.15.7.4 Regexp#===
of the ISO/IEC 30170:2012 Ruby Language Specification). Imagine a Regexp describing an infinite set of languages that match that Regexp:
/el+/ === 'Hello' #=> true
/el+/ === 'World' #=> false
If I have a drawer labelled /el+/
, does it make sense to put 'Hello'
in it? Yes, it does. What about 'World'
? No, it does not.
So, your case
expression:
case p1...p2
when p1 == 'rock' && p2 == 'scissors' || p1 == 'scissors' && p2 == 'paper' || p1 == 'paper' && p2 == 'rock'
return 'Player 1 won!'
when p1 == 'scissors' && p2 == 'rock' || p1 == 'paper' && p2 == 'scissors' || p1 == 'rock' && p2 == 'paper'
else
p1 == p2
return 'Draw!'
end
Note that else
doesn't have a conditional. It literally means "in every other case", so there is no need for a conditional. So, the p1 == p2
is actually the first expression inside the else
block. However, since it has no side-effects, and its value is completely ignored, it doesn't actually do anything at all.
Let's simplify that to:
case p1...p2
when something_that_is_either_true_or_false
return 'Player 1 won!'
when something_that_is_either_true_or_false
else
return 'Draw!'
end
This is equivalent to
temp = p1...p2
if something_that_is_either_true_or_false === temp
return 'Player 1 won!'
elsif something_that_is_either_true_or_false === temp
else
return 'Draw!'
end
Now, depending on how exactly you call the rps
method, those conditional expressions may be either true
or false
, but we don't know exactly which. What we do know, however, is that neither TrueClass#===
nor FalseClass#===
override Object#===
, so they still have the same semantics: they simply test for equality. So, this is actually equivalent to
if something_that_is_either_true_or_false == p1...p2
return 'Player 1 won!'
elsif something_that_is_either_true_or_false == p1...p2
else
return 'Draw!'
end
Here's the thing: a boolean will never be equal to a range. They aren't even the same type! What you are effectively asking in your case
expression is
"If I have a drawer labelled true
, does it make sense to put p1...p2
into it?" or "If I have a drawer labelled false
, does it make sense to put p1...p2
into it?" It doesn't matter which of the two situations you have, because the answer is "No" in both cases.
So, there is, in fact, no possible way in which any of the when
clauses can ever be true
. Therefore, your case
expression will always evaluate the else
clause, or put another way, your entire method is completely equivalent to
def rps(_, _) # ignore all arguments
'Draw!'
end
There is a second form of the case
expression: when you leave out the object_of_interest
, then it becomes equivalent to a series of conditionals, like this:
case
when conditional1
some_expression
when conditional2
some_other_expression
when conditional3
another_expression
else
an_entirely_different_expression
end
is (roughly) equivalent to
if conditional1
some_expression
elsif conditional2
some_other_expression
elsif conditional3
another_expression
else
an_entirely_different_expression
end
So, one way to fix your method, is to simply delete the p1...p2
:
case # that is all we need to change to make it work
when p1 == 'rock' && p2 == 'scissors' || p1 == 'scissors' && p2 == 'paper' || p1 == 'paper' && p2 == 'rock'
return 'Player 1 won!'
when p1 == 'scissors' && p2 == 'rock' || p1 == 'paper' && p2 == 'scissors' || p1 == 'rock' && p2 == 'paper'
else
p1 == p2
return 'Draw!'
end
That is literally all we need to change to make it work. Just delete that single expression. We can simplify it a bit further: first, as discussed above, we can remove the p1 == p2
, because it doesn't do anything. Secondly, the case
expression is an expression. It is not a statement. (In fact, there are no statements in Ruby. Everything is an expression.)
Ergo, the case
expression evaluates to a value, in particular, it evaluates to the value of the branch that was taken. So, whenever you see something like
case foo
when bar
return baz
when qux
return frobz
end
that is equivalent to
return case foo
when bar
baz
when qux
frobz
end
Also, the last expression evaluated in a method body (or block body, lambda body, class definition body, or module definition body) is the value of the whole method (block, lambda, class definition, module definition), so in your case, the return
is redundant there, too.
Lastly, your second when
has no body, so it isn't doing anything, which means we can just remove it.
Which means the entire method becomes
case
when p1 == 'rock' && p2 == 'scissors' || p1 == 'scissors' && p2 == 'paper' || p1 == 'paper' && p2 == 'rock'
'Player 1 won!'
else
'Draw!'
end
Since there is only one condition, it doesn't really make sense for it to be a case
expression at all, so we make it a conditional expression instead:
if p1 == 'rock' && p2 == 'scissors' || p1 == 'scissors' && p2 == 'paper' || p1 == 'paper' && p2 == 'rock'
'Player 1 won!'
else
'Draw!'
end
An alternative way of solving this with a case
expression would be to actually use the fact that, when not specifically overridden, ===
just means equality:
case [p1, p2]
when ['rock', 'scissors'], ['scissors', 'paper'], ['paper', 'rock']
'Player 1 won!'
when ['scissors', 'rock'], ['paper', 'scissors'], ['rock', 'paper']
'Player 2 won!'
else
'Draw!'
end