Hi,
I’m thinking of submitting a RCR. Here is the draft. Comments are welcome.
Thanks,
Cao
···
====================================================
RCR title: An enhanced case…when…else…end syntax
This RCR involves (check all that apply):
… a new feature x
… a syntax change x
… refactoring only
… backwards incompatibility
Abstract…
This RCR proposes an enhanced case…when…else…end syntax.
Problem…
It’s a very common task to group code based on a combination of values of
multiple variables. However, the current Ruby syntax only allows one
expression/variable being used in “case” clause. People have to use
if…else…end which requires more typing and is less intuitive.
For example, suppose I’m generating a image, the color of each point is
defined based on its x and y coordinates.
0 ... 50...100...200
0 +----------------+
> white |
50 |----------------|
> > grey |
100 |blue ±---------|
> > black |
150 |----------------|
> white |
200 ±---------------+
Use if…else…end:
if 0…50 === x || 150…200 === x
color = white
elsif 0…50 === y
color = blue
elsif 50…100 === x and 50…200 === y
color = grey
else
color = black
end
Use new case…when…else…end:
case x : y
when 0…50, 150…200 # x in 0…50 or 150…200
color = white
when _ : 0…50 # y in 0…50
color = blue
when 50…100 : 50…200 # x in 50…100 and y in 50…200
color = grey
else # otherwise
color = black
end
Note: If the color is only dependent on x, we can use the current "case"
syntax. However if it’s dependent on more than one variable, “case” is
incapable of handling it.
Proposal…
The enhanced syntax looks like below:
case expr1 : expr2
when expr3 : _ : expr4
do_something
when expr5
do_different_thing
else
do_default_thing
end
- “case” takes 0 or any number of expressions separated by “:”;
If there’s no expression, each clause separated by “:” in “when” is
evaluated to true or false and “:” is treated the same way as “and”.
- “when” takes 1 or more clauses separated by “:”; each clause by itself
can be separated by “,” (this is supported in current syntax).
If there are less clauses than the number of “case” expressions, the
rest of “case” expressions are not evaluated.
If there are more clauses than the number of “case” expressions, the
rest of “when” clauses are evaluated to true or false. “:” is treated
the same way as “and”
-
“else” remains the same.
-
“_” means skipping evaluation of the corresponding “case” expression.
Analysis…
What benefits do we get?
- It’s a more general form, which means it can be used much wider than
what “case…when” is used for today; - less typing; there’s no need to type x, y, === in the when clause;
- this syntax change is backward compatible with Ruby 1.x
Implementation…
Because this request proposes syntax enhancement, it can’t be achieved
without changing Ruby interpreter.
The code below demonstrated the idea by extending Object class and
utilizing thread-local variables. It uses “ccase…cwhen…celse…end” to
simulate the proposed syntax. It uses “,” to separate ccase/cwhen clauses
which are just arguments. It uses “true” instead of “_” to indicate
skipping evaluation.
Source:
#!/usr/bin/ruby
To utilize Ruby’s existing functionality to demonstrate the idea, I’m doing
this:
add instance methods “ccase”, “cwhen”, “celse” to class Object which is the
top one in the hierarchy so all classes inherit them.
Its usage is like this:
ccase a, b do
cwhen [1,2], 1…3 do
…
end
cwhen 3, true do
…
end
celse do
…
end
end
Problems so far:
1. how to let “cwhen” method body to access “ccase” arguments and
intermediate objects;
Solution: use thread-local variable
2. how to make it work in multi-threaded program;
Solution: use thread-local variable
3. “do…end” or “{…}” is needed to pass block to ccase/cwhen/celse.
Solution: ???
ccase = “customized case”
cwhen = “customized when”
celse = “customized else”
class Object
customized case
def ccase arg0, *args
begin
args.unshift arg0
Thread.current[“in_ccase”] = true
Thread.current[“ccase_args”] = args
Thread.current[“hit_cwhen”] = nil
if block_given?
yield
end
ensure
Thread.current[“in_ccase”] = nil
Thread.current[“ccase_args”] = nil
Thread.current[“hit_cwhen”] = nil
end
end
customized when
def cwhen arg0, *args
skip if already matched
return if Thread.current[“hit_cwhen”]
args.unshift arg0
hit = true # hit or not?
ccase_args = Thread.current[“ccase_args”]
if ccase_args and ccase_args.length > 0
ccase_args.each_index do |idx|
ccase_arg = ccase_args[idx]
cwhen_arg = args[idx]
# cwhen_arg is not present, hit = true
# cwhen_arg is true and ccase_arg is not false, hit = true
# ccase_arg is not present, hit = if cwhen_arg evaluates to true
# both ccase_arg and cwhen_arg are present, hit = if cwhen_arg === ccase_arg
if (not cwhen_arg) or (not ccase_arg and cwhen_arg) or (cwhen_arg == true and not ccase_arg == false) or (cwhen_arg === ccase_arg)
# hit = true
elsif cwhen_arg.is_a? Array
hit = false
cwhen_arg.each do |arg|
if arg === ccase_arg
hit = true
break
end
end
else
hit = false
end
s = if cwhen_arg.nil? then "nil" else cwhen_arg.to_s end
s1 = if ccase_arg.nil? then "nil" else ccase_arg.to_s end
print "#{s} === #{s1} : #{hit.to_s}\n"
break if not hit
end
else
args.each {|arg| hit &= arg}
end
return if not hit
Thread.current[“hit_cwhen”] = true
if block_given?
yield
end
end
customized else to be combined with ccase/cwhen
def celse
return if Thread.current[“hit_cwhen”]
if block_given?
yield
end
end
end
if FILE == $0
def test a,b,c
ccase a,b,c do
cwhen [1, 3, 6], 1…2 do
# if a is in [1,3,6] and b === 1…2
print "hit 1\n"
end
cwhen 4, 3, 1 do
# if a === 4 and b === 4 and c === 1
print "hit 2\n"
end
cwhen 5, true, 7 do
# if a === 5 and c === 7
print "hit 3\n"
end
celse do
# otherwise
print "no hit\n"
end
end
end
test 1,2,3 # hit 1
test 4,3,0 # no hit
test 4,3,1 # hit 2
test 5,6,7 # hit 3
end