-3

If I write this:

case v
when String
  puts :String
else
  puts :other
end

If I set v to "some string" I get 'String'.

If I set v to String, I get 'other'.

How am I supposed to 'switch' on a variable containing one of several class objects?

How does this honour the well-established computing principle of 'least surprise'?

Please don't tell me to monkey-patch the 'Class' class.

Machavity
  • 30,841
  • 27
  • 92
  • 100
android.weasel
  • 3,343
  • 1
  • 30
  • 41

4 Answers4

3

I agree that === (the test used by case) can be confusing.

It's often called "case equality operator", but === is neither reflexive nor symmetric nor transitive, so it doesn't behave like an equality at all :

String === "test"
# true
"test" === String
# false
String === String
# false
String === Class
# false
Class === String
# true
Class === Class
# true

The best description I've read for a === b is from @JörgWMittag's answer :

"If I have a drawer labelled a would it make sense to put b in that drawer?"

For your problem, you could write :

v = String
case v
when Class
  puts "#{v} is a Class, let's investigate some more"
  # another case, if statements or hash lookup...
else
  puts :other
end
Community
  • 1
  • 1
Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
  • 2
    `Class === Class #⇒ true` is worth to appear in the list :) – Aleksei Matiushkin Apr 03 '17 at 14:11
  • _"If I have a drawer labelled `String` would it make sense to put `String` in that drawer?"_ – still sounds reasonable. – Stefan Apr 03 '17 at 14:45
  • _If I have a drawer labelled `Drawer` would it make sense to put `Drawer` in that drawer?_ – Aleksei Matiushkin Apr 03 '17 at 14:46
  • The problem is the labelling. It's not just `a` (statically) but `a`'s definition of of `===`. So `when String` would become _“If I have a drawer labelled ‘instances of `String`’ would it make sense to put `String` in that drawer?”_ – Stefan Apr 03 '17 at 15:07
  • @Stefan: You're right, this definition doesn't always work. Still, it's hard to find something better. In `String === String` case, [eiko's answer](http://stackoverflow.com/a/43187718/6419007) is a better fit : "Does String describe String? #=> false". – Eric Duminil Apr 03 '17 at 15:38
1

How am I supposed to 'switch' on a variable containing one of several class objects?

With Module#<=:

case v
when String then "instance of string"
when ->(c) { c <= String } then "class derived from String"
end

How does this honour the well-established computing principle of 'least surprise'?

Perfectly.

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
1

You can use case without an object:

case 
when v == String
  puts 'v is String class'
when v.is_a?(String)
  puts 'v is an instance of String'
else
  puts 'v is something else'
end

This resembles an if-elsif-else expression.

Stefan
  • 109,145
  • 14
  • 143
  • 218
  • 1
    `v.is_a?(String)` does exactly what triple-equal does, doesn’t it? I understood the question as there is a need to check whether `v` is a `String` (instance of `Class`, `String`, or a subclass of `String`.) – Aleksei Matiushkin Apr 03 '17 at 14:25
  • @mudasobwa sorry, my bad. – Stefan Apr 03 '17 at 14:36
1

Case expressions in ruby and some other functional languages are quite different from their imperative switch statement cousins.

As others have mentioned, case expressions use the === method, which is a pattern-defining method. In the a === b paradigm, a is a pattern which describes a collection of possible instances. b is an instance which potentially fits into that pattern / collection. Think of a === b as:

does 'a' describe 'b'?

or

does 'a' contain 'b'?

Once you understand this, String === String #=> false is not so surprising because String is an instance of Class so it fits into the Class pattern.


The other unique distinction between case expressions and switch statements is that case expressions are just that: expressions. Which means you can do cool stuff like this:

puts case v.name
  when "String"
    :String
  else
    :other
  end if v.is_a? Class

This code executes only if v is a class, then switches on v.name and puts whatever you want, based on the name of the class.

Because of ruby's nature as a duck-typed language, it's exceedingly rare to have a class in a variable and need to switch on it. If you're frustrated that case isn't as elegant as you had hoped, give us more context about your goal and we might be able to come up with an elegant solution which avoids switch all together.

Stefan
  • 109,145
  • 14
  • 143
  • 218
eiko
  • 5,110
  • 6
  • 17
  • 35
  • 1
    Good answer. I lke `does 'a' describe 'b'` as a loose definition for `===`. It probably doesn't always work, but it might cover cases not covered by "If I have a drawer labelled `a` would it make sense to put `b` in that drawer?" – Eric Duminil Apr 03 '17 at 15:40