Project

General

Profile

Actions

Feature #20318

open

Pattern matching `case ... in` support for triple-dot arguments

Added by bradgessler (Brad Gessler) 10 months ago. Updated 9 months ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:117021]

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"

Related issues 1 (1 open0 closed)

Is duplicate of Ruby master - Feature #19764: Introduce defp keyword for defining overloadable, pattern matched methodsOpenActions

Updated by bradgessler (Brad Gessler) 10 months ago

  • Description updated (diff)

Add array example.

Updated by rubyFeedback (robert heiler) 10 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) 10 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) 10 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 beginless rescue.

Updated by zverok (Victor Shepelev) 10 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) 10 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) 10 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) 10 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) 10 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) 9 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.

Actions #11

Updated by shyouhei (Shyouhei Urabe) 9 months ago

  • Is duplicate of Feature #19764: Introduce defp keyword for defining overloadable, pattern matched methods added
Actions

Also available in: Atom PDF

Like1
Like0Like0Like0Like0Like2Like2Like0Like0Like0Like0Like0