Feature #20318
openPattern matching `case ... in` support for triple-dot arguments
Description
Premise¶
Sometimes when I'm creating a method for an API, I'd like to do pattern matching against the arguments. Today I have to do something like this:
def foo(*args, **kwargs, &block)
case { args:, kwargs:, block: }
in args: [name]
puts name
in args: [first_name, last_name]
puts "Hi there #{first_name} #{last_name}"
in kwargs: {greeting:}
puts "Hello #{greeting}"
else
puts "No match: #{args}"
end
end
foo "Hi"
foo "Brad", "Gessler"
foo greeting: "Brad"
Or an array like this:
def bar(*args, **kwargs, &block)
case [args, kwargs, block]
in [name], {}, nil
puts name
in [first_name, last_name], {}, nil
puts "Hi there #{first_name} #{last_name}"
in [], {greeting:}, nil
puts "Hello #{greeting}"
else
puts "No match: #{args}, #{kwargs}"
end
end
bar "Howdy"
bar "Bradley", "Gessler"
bar greeting: "Bradley"
Proposal¶
I'd like to propose the same thing, but for ...
, like this:
def foo(...)
case ...
in args: [name]
puts name
in args: [first_name, last_name]
puts "Hi there #{first_name} #{last_name}"
in kwargs: {greeting:}
puts "Hello #{greeting}"
else
puts "No match: #{args}"
end
end
foo "Hi"
foo "Brad", "Gessler"
foo greeting: "Brad"
One thing I'm not sure sure about: the args
, kwargs
, and block
names appear out of thin air, so ideally those could somehow be named or have a syntax that doesn't require those names.
The array would look like this:
def bar(...)
case ...
in [name], {}, nil
puts name
in [first_name, last_name], {}, nil
puts "Hi there #{first_name} #{last_name}"
in [], {greeting:}, nil
puts "Hello #{greeting}"
else
puts "No match: #{args}, #{kwargs}"
end
end
bar "Howdy"
bar "Bradley", "Gessler"
bar greeting: "Bradley"
Updated by rubyFeedback (robert heiler) 8 months ago
Personally I find the double ... rather confusing. I understand the benefit of a more succint syntax - e. g. eliminating "(*args, **kwargs, &block)" - but even then I find the dual-triple-dot very strange. It seems rather "un-ruby" to me, however one wants to define that.
Updated by ko1 (Koichi Sasada) 8 months ago
I prefer
def foo(...)
in [name] # foo('ko1')
puts name
in [first_name, last_name] # foo('ko1', 'ssd')
puts "Hi there #{first_name} #{last_name}"
in {greeting:} # foo(greeting: 'hello')
puts "Hello #{greeting}"
else
puts "No match: #{args}"
end
like
def foo
BODY
rescue
RESCUE
end
to represent method overloading.
(but I understand many difficulties)
Updated by baweaver (Brandon Weaver) 8 months ago
ko1 (Koichi Sasada) wrote in #note-3:
I prefer
...
to represent method overloading.
(but I understand many difficulties)
Personally this is the syntax I would like to see, as I believe that the intention of this issue is method overloading, and it has been a fairly common request. I also agree that it is precedented with the begin
less rescue
.
Updated by zverok (Victor Shepelev) 8 months ago · Edited
@ko1 (Koichi Sasada) A pretty similar effect can be achieved by combining several recent features:
def foo(*, **) = case [*, **]
in name, {}
puts name
in first_name, last_name, {}
puts "Hi there #{first_name} #{last_name}"
in [{greeting:}]
puts "Hello #{greeting}"
in *args
puts "No match: #{args}"
end
foo('ko1') #=> "ko1"
foo('ko1', 'ssd') #=> "Hi there ko1 ssd"
foo(greeting: 'hello') #=> Hello hello
foo('ko1', 'ssd', greeting: 'hello') #=> No match: ["ko1", "ssd", {:greeting=>"hello"}]
On the plus sides:
- it doesn't require any new constructs but cleverly combines existing ones (that also can be combined in different ways, so it is insightful): e.g.,
def foo(args) = statement
can become a popular idiom to emphasize one-statement methods,; - many of things are still explicit, if short: no special names, like
*args
is what you write there; the flexibility of applying this approach to either positional-only, or keyword-only, or both-arg-types methods (*
/**
can be combined any necessary way);
The drawbacks here are:
- some things that feel unnecessary in this case (both positional and keyword args), like "don't forget empty
{}
at the end" or the necessity to wrap{greetings:}
in[]
(and failing to put punctuation properly would not be a syntax error, but patterns suddenly failing/matching in the cases you wouldn't expect)
For methods that have only positional or only keyword args, it would be a bit clearer:
def index(*) = case [*]
in [Integer => index]
p(index:)
in [Integer => from, Integer => to]
p(range: from..to)
in [Range => range]
p(range:)
end
index(1) #=> {:index=>1}
index(2, 3) #=> {:range=>2..3}
index(4..5) #=> {:range=>4..5}
def hey(**) = case {**}
in {rank:, **}
puts "Salute, #{rank}"
in first:, last:
puts "Good day, #{first}, #{last}"
in first:
puts "Hey, #{first}"
end
hey(first: 'John') #=> "Hey, John"
hey(first: 'John', last: 'Smith') #=> "Good day, John Smith"
hey(rank: 'Major', first: 'John', last: 'Smith') #=> "Salute, Major"
It seems to look reasonably close to method overloading but still has its quirks.
Updated by nobu (Nobuyoshi Nakada) 8 months ago · Edited
A patch for @ko1 (Koichi Sasada) style.
Probably the code generation would be more efficient in compile.c.
https://github.com/nobu/ruby/tree/dispatch-case
https://github.com/ruby/ruby/pull/10170
Updated by willcosgrove (Will Cosgrove) 8 months ago
I'm going to throw my suggestion into the ring with no idea how hard it would be to implement.
def foo
when (name) # foo('ko1')
puts name
when (first_name, last_name) # foo('ko1', 'ssd')
puts "Hi there #{first_name} #{last_name}"
when (greeting:) # foo(greeting: 'hello')
puts "Hello #{greeting}"
else
puts "No match: #{args}"
end
I know there's a conflation happening in the pattern matching vocabulary with when
vs in
, but to me when
reads clearer for method overloading. It reads to me as "foo, when called like this ..."
I also think that being able to "pattern match" the same way you would in a method parameter list is more intuitive, at least to me, than the pattern matching syntax.
Updated by ntl (Nathan Ladd) 8 months ago
It might be a good idea to consider how proc/lambda/block arguments could also have comparable syntax. For instance, a block argument example:
HTTP.post("http://example.com/some-resource", "some data") do |...|
in code: (200...300)
puts "Success!"
in code: (300...400), location:
puts "Redirect to #{location}"
# post to location
in code: (500...600)
puts "Server error"
do_retry
end
Updated by mame (Yusuke Endoh) 8 months ago
I strongly feel "bad smell" for this proposal.
In principle, one method should do one thing.
But this syntax introduces a completly different branching depending on the arguments, which could encourage a weird method behavior.
In fact, the motivating example prints different messages depending on the arguments. I believe such a method would never be recommended.
In particular, the method that accepts foo(str)
and foo(greeting: str)
but not foo(str, greeting: str)
looks rather weird to me. I am concerned that such a method could be so easily defined.
Considering the Java overloading, each overloaded method definition would often delegate to the most common (receives the most arguments) definition. Given that, the motivating example should be:
def foo(...)
case ...
in args: [name]
puts "Hello #{ name }"
in args: [first_name, last_name]
foo(first_name + " " + last_name)
in kwargs: {greeting:}
foo(greeting)
else
raise ArgumentError
end
end
I do not find this code easy to understand.
As far as I know, Ruby has the most complicated argument system in the world. I don't think we should make it even more complicated.
Updated by matheusrich (Matheus Richard) 8 months ago · Edited
@mame (Yusuke Endoh) I understand the example is contrived, but even the core library does different things with different arguments. Let's take Random.rand
as an example:
Random.rand(10) # returns an Integer up to 10
Random.rand(10.0) # returns a Float up to 10
Random.rand(0..10) # returns an Integer between 0 and 10
Random.rand(0..10.0) # returns a Float between 0 and 10
The logic to do this is spread out in the method definition with several if/else. This proposal could streamline those types of interfaces. Granted, this can currently be achieved with the approach @zverok (Victor Shepelev) mentioned.
Updated by shyouhei (Shyouhei Urabe) 7 months ago
- Is duplicate of Feature #19764: Introduce defp keyword for defining overloadable, pattern matched methods added