RCR draft for enhanced "case..when..else..end" syntax

G

Guoliang Cao

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

1. "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".

2. "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"

3. "else" remains the same.

4. "_" means skipping evaluation of the corresponding "case" expression.


Analysis....
What benefits do we get?
1. It's a more general form, which means it can be used much wider than
what "case..when" is used for today;
2. less typing; there's no need to type x, y, === in the when clause;
3. 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
 
R

Robert Klemme

Guoliang Cao said:
Hi,

I'm thinking of submitting a RCR. Here is the draft. Comments are
welcome.

[snip]

Do we need this?

16:41:55 [ruby]: ./ccase.rb
[#<struct Point x=0, y=0>, "white"]
[#<struct Point x=100, y=0>, "blue"]
[#<struct Point x=55, y=55>, "grey"]
[#<struct Point x=120, y=210>, "black"]
16:41:57 [ruby]: cat ccase.rb
#!/usr/bin/ruby

Point = Struct.new( :x, :y )

class CondWhite
def self.===(point); (0..50) === point.x or (150..200) === point.x; end
end

class CondBlue
def self.===(point); (50..150) === point.x and (0..50) === point.y; end
end

class CondGrey
def self.===(point); (50..100) === point.x and (50..200) === point.y;
end
end


points = [
Point.new( 0, 0 ),
Point.new( 100, 0 ),
Point.new( 55, 55 ),
Point.new( 120, 210 ),
]

points.each do |point|
case point
when CondWhite; p [point, "white"]
when CondBlue; p [point, "blue"]
when CondGrey; p [point, "grey"]
else; p [point, "black"]
end
end
16:42:00 [ruby]:

Regards

robert
 
N

nobu.nokada

Hi,

At Tue, 3 Feb 2004 23:35:25 +0900,
Guoliang said:
... backwards incompatibility ?

2. "when" takes 1 or more clauses separated by ":"; each clause by itself
can be separated by "," (this is supported in current syntax).

"when" clause can be terminated by ":" now. It can conflict.
4. "_" means skipping evaluation of the corresponding "case" expression.

It feels ugly a bit, to me.
 
G

Gennady

Guoliang said:
Hi,

I'm thinking of submitting a RCR. Here is the draft. Comments are welcome.

Thanks,
Cao

Do you know about this syntax currently available?

a = "aaa"
b = "bbb"
case
when a == "aaa", b == "bbb"
puts "Lowercase detected"
when a == "AAA", b == "BBB"
puts "Uppercase detected"
else
puts "Detection failed"
end
====================================================

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

1. "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".

2. "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"

3. "else" remains the same.

4. "_" means skipping evaluation of the corresponding "case" expression.


Analysis....
What benefits do we get?
1. It's a more general form, which means it can be used much wider than
what "case..when" is used for today;
2. less typing; there's no need to type x, y, === in the when clause;
3. 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
 
G

Guoliang Cao

---- Original Message ----
At Tue, 3 Feb 2004 23:35:25 +0900,
"when" clause can be terminated by ":" now. It can conflict.

Really? In the Pragmatic Programmer's Guide, it didn't mention ":" but uses
"then":

"...and you also need a then keyword if the expression is on the same line
as the condition."
It feels ugly a bit, to me.

"~" or whichever looks good to most people and doesn't cause backward
incompatibility problem.

Cao
 
G

Guoliang Cao

---- Original Message ----
Do you know about this syntax currently available?
a = "aaa"
b = "bbb"
case
when a == "aaa", b == "bbb"
puts "Lowercase detected"
when a == "AAA", b == "BBB"
puts "Uppercase detected"
else
puts "Detection failed"
end

No. This is close to what I want though. The only drawback is you still
have to type "a", "b" in each when clause.

Cao
 
R

Robert Klemme

Gennady said:
Do you know about this syntax currently available?

a = "aaa"
b = "bbb"
case
when a == "aaa", b == "bbb"
puts "Lowercase detected"
when a == "AAA", b == "BBB"
puts "Uppercase detected"
else
puts "Detection failed"
end

Since which version of Ruby? Thx!

robert
 
G

Gennady

Robert said:
Since which version of Ruby? Thx!

robert

I checked that it works in 1.6.8. I remember somebody (Nobu?) mentioning
it on ruby-talk some time ago.

Gennady.
 
G

Guoliang Cao

---- Original Message ----

---- Original Message ----
Really? In the Pragmatic Programmer's Guide, it didn't mention ":" but uses
"then":
"...and you also need a then keyword if the expression is on the same line
as the condition."

Just tried on Ruby 1.6.8. It uses "then" instead of ":". So there's no
backward incompatibility.

Cao
 
N

nobu.nokada

Hi,

At Wed, 4 Feb 2004 03:11:35 +0900,
Guoliang said:
Just tried on Ruby 1.6.8. It uses "then" instead of ":". So there's no
backward incompatibility.

$ ruby-1.8 -e 'if /^\w+/ =~ "abc": puts $& end'
abc
$ ruby-1.8 -e 'case "abc" when /^\w+/: puts $& end'
abc
$ ruby-1.8 -e 'while l = gets: p l end'
abc
"abc\n"

Now ":" can be placed anywhere instead of "then", and "do" for
loops.
 
R

Robert Klemme

Gennady said:
I checked that it works in 1.6.8. I remember somebody (Nobu?) mentioning
it on ruby-talk some time ago.

I couldn't find it in the Pickaxe - I'll file it under "Hidden Treasures
of Ruby"... :)

Thanks!

robert
 
G

Gennady

Sam said:
Quoteing (e-mail address removed), on Wed, Feb 04, 2004 at 05:35:03PM +0900:
I couldn't find it in the Pickaxe - I'll file it under "Hidden Treasures
of Ruby"... :)


There's a lot of info jammed into the corners of the pickaxe book!

For this syntax, there's an example in the section "Case Expressions",
p84, and then in chapter 18 it shows the syntax:

case target
when comparison [, comparison]]... [then]
body
when comparison [, comparison]]... [then]
body
...
[ else
body ]
end


Cheers,
Sam
We are not talking about general "case" syntax, most of folks here are
well aware of it (thanks for refreshing it anyway ;-)). It is rather
about a special case when <target> is not specified in "case" clause. It
looks like it defaults to "true" so that <comparison> may contain
logical operators like "a == <something>". And I could not find it
mentioned in the Pickaxe either.

Gennady.
 
G

Guoliang Cao

---- Original Message ----
No. It's ugly and (IMO) not needed. Any program which seems to need it is in
need of a redesign. The specific case can be handled as you suggested it, or

Robert's code looks good, but a bit over-engineering to me.
better yet with a lookup table. This sort of problem is handled easily by
intelligent data-driven design.

Don't mix system design with language capabilities please. I think it's not
a bad thing to let "case" to take more than one expressions. Just like a
function can take any number of arguments. Why does "case" have to be
limited to zero or one? Because all our languages are designed this way?
(AFAIK only Ruby "case" can take 0 expressions. That's some good stuff
introduced by Ruby, right? So why not go one step further?) Or because it's
impossible?
There is no need for a new or modified language construct here.

Since 2.0 is a major update to 1.x, why can't we introduce new language
constructs if they really makes sense?

Cao
 
Y

Yukihiro Matsumoto

Hi,

In message "Re: RCR draft for enhanced "case..when..else..end" syntax"

|Don't mix system design with language capabilities please. I think it's not
|a bad thing to let "case" to take more than one expressions. Just like a
|function can take any number of arguments. Why does "case" have to be
|limited to zero or one? Because all our languages are designed this way?
|(AFAIK only Ruby "case" can take 0 expressions. That's some good stuff
|introduced by Ruby, right? So why not go one step further?) Or because it's
|impossible?

Natural extension is not always a good thing. We can inherit from one
class, hey, why not inheriting from multiple classes? Because it
introduces complexity, mix-in is a better solution.

|Since 2.0 is a major update to 1.x, why can't we introduce new language
|constructs if they really makes sense?

You are right, we can, when (and only when) I agree with the new
design. I personally don't see much usage of your new syntax.

matz.
 
G

Guoliang Cao

---- Original Message ----
In message "Re: RCR draft for enhanced "case..when..else..end" syntax"
on 04/02/05, Guoliang Cao <[email protected]> writes:
|Don't mix system design with language capabilities please. I think it's not
|a bad thing to let "case" to take more than one expressions. Just like a
|function can take any number of arguments. Why does "case" have to be
|limited to zero or one? Because all our languages are designed this way?
|(AFAIK only Ruby "case" can take 0 expressions. That's some good stuff
|introduced by Ruby, right? So why not go one step further?) Or because it's
|impossible?
Natural extension is not always a good thing. We can inherit from one
class, hey, why not inheriting from multiple classes? Because it
introduces complexity, mix-in is a better solution.
|Since 2.0 is a major update to 1.x, why can't we introduce new language
|constructs if they really makes sense?
You are right, we can, when (and only when) I agree with the new
design. I personally don't see much usage of your new syntax.

I'm fine with that. Thanks for taking a look, Matz.

Cao
 
G

Guoliang Cao

---- Original Message ----
I'm not. This proposal does. What you're wanting to do is effectively make
it so that case operates in parallel on multiple conditions. I can count on
zero hands the number of times that I've needed this capability. I have

When you are thinking it's not something you'll need, the count is always
zero.
found it more useful to worry about my data design and make sure that I
have a proper lookup table. Robert's design does exactly that with multiple
custom objects instead of a lookup table.
Nothing is impossible. It's a question as to whether it's a good idea.
Frankly, your proposal is a bad idea. I said as much to your initial posting
on RubyGarden.

It might be. I can accept that. ;-)
Except that your construct doesn't make sense.
1. It introduces unnecessary complexity.

I can't see how complex it is. It might be though. I might have mixed too
many things in one proposal.
2. It adds a lot of "magic" that isn't clear (_ or ~ -- both are really
ugly). Ruby2 seems to be getting rid of a lot of that silly magic.

Once it's clear, it's clear.
3. It adds a language "feature" that is better handled by proper program
design.

About my example, I don't think people should do a complex design.
"if..elsif" is the answer in current Ruby capability.

Cao
 
S

Sean O'Dell

About my example, I don't think people should do a complex design.
"if..elsif" is the answer in current Ruby capability.

Personally, I like the idea of a more readable way to replace the if...elsif
paradigm. It's a great idea!

Sean O'Dell
 
R

Robert Klemme

Sean O'Dell said:
Personally, I like the idea of a more readable way to replace the if...elsif
paradigm. It's a great idea!

Hmm... As someone else pointed out, if you are in excessive need of such
a construct, you should consider redesign. Case constructs (or
if...elsif...end cascades) are regarded to be not very OO IMHO.

Regards

robert
 
Y

YANAGAWA Kazuhisa

In Message-Id: <[email protected]>
Guoliang Cao said:
About my example, I don't think people should do a complex design.
"if..elsif" is the answer in current Ruby capability.

I wonder why those who have informed that case which has no following
expressions is equivalent to `case true', and when clause can take an
arbitrary expression, doesn't try:

case
when a == "aaa" && b == "bbb"
puts "lowercase"
when a == "AAA" && b == "BBB"
puts "uppercase"
else
puts "???"
end

That looks nice, doesn't it?
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,982
Messages
2,570,186
Members
46,739
Latest member
Clint8040

Latest Threads

Top