loading...

Ruby – Pattern Matching – Second Impressions

katafrakt profile image Paweł Świątkowski ・3 min read

Since I published A quest for pattern-matching in Ruby 3 years ago, I've been called "pattern matching guy" more than once. So obviously, when I learned that PM is inevitably coming to the Ruby core, I was curious to check it out. First Impressions have already been published, so this is "Second Impressions", from my point of view.

Heads up: it's very subjective.

I'm mostly judging it by examples provided in the original Redmine ticket, such as:

class Array
  alias deconstruct itself
end

case [1, 2, 3, d: 4, e: 5, f: 6]
in a, *b, c, d:, e: Integer | Float => i, **f
  p a #=> 1
  p b #=> [2]
  p c #=> 3
  p d #=> 4
  p i #=> 5
  p f #=> {f: 6}
  e   #=> NameError
end

First of all, the code is ugly in a way that makes it hard to reason about. It looks like being added on top of a language which was not designed to support pattern matching (which is exactly the case). This might not be important in the long run, when people get used to it - but here it is, in the second impressions round.

Destructuring (why was it called deconstruction?) looks nice, but I would remove the pipe thingy. Instead of e: Integer | Float => i (which is terribly ambiguous - is it e: (Integer | Float) => i or ((e: Integer) | (Float)) => i, or something else?) it would be better to have a possibility to define a type union like in Pony. For example:

number = typeunion(Integer | Float) # hypothetic keyword typeunion
case n
in number
  puts "I'm a number"
in String
  puts "I'm string"
end

Besides that it's good, especially for getting things out of hashes.

But probably my most important problem with this proposal is that it does not let me have multiple functions defined with different type signatures, ruled by pattern matching. This is what I'm mostly missing on a daily basis working with Ruby, while having it available in Erlang or Elixir. To give you a taste of what I'm talking about:

class Writer
  def write_five_times(text => String)
    puts text * 5
  end

  def write_five_times(text => Integer)
    puts text.to_s * 5
  end

  def write_five_times(text => Object)
    raise NotImplementedError
  end
end

Of course, to achieve what's in code listing above, it would be much larger and complicated change. Basically it would be like introducing proper types to Ruby. It needs to allow having one method defined mutiple times in one class, but without shadowing previous definitions. I don't think that Ruby will ever go this way, yet this is something that would clean up my code in few places significantly.

I also realised that while working on Noaidi – my implementation of pattern matching. I don't really want plain pattern matching somewhere in the code, as I can make most cases work with good old case in Ruby. But I would like to be able to write modules that behave kind of like the ones in Elixir.

And this is being made possible in Noaidi. I have an experimental branch enabling this and I hope I will be able to finish it some day. Such module would look like this:

module Math
  extend Noaidi::DSL

  fun(:fib, 0..1)    { 1 }
  fun(:fib, Integer) { |n| add(fib(n-1), fib(n-2)) }
  fun(:fib, Object)  { raise NotImplementedError }
  funp(:add, Integer, Integer) { |a,b| a + b }
end

Math.fib(1)      #=> 1
Math.fib(20)     #=> 10946
Math.fib("test") #=> NotImplementedError
Math.add(1,3)    #=> NoMethodError (private method `add' called for Math:Module)

Verdinct: I'm kind of disappointed. The quest is not over yet.


This has been also posted on my personal blog.

Discussion

markdown guide
 

Wow, that was really hard to understand!

I'm sure whoever made this happen had put in a lot of effort, but as you said, I don't think it's ideal to just shoehorn a feature in when it doesn't fit in.

By the way, with a quick glance on your snippets below, it looks like what you're asking for isn't really just pattern matching, but rather method overload.

I know Crystal has it which looks pretty cool!

 

From what I know about Crystal, it has method overload only by types (much like Java). And hear it would be possible to also "overload" by particular values - for example different method for hash containing a key "success".

 

Correct, that's what overloading means - defining methods with same name but with different method signatures, and method signatures are usually types & arity.

What you're referring to (in Elixir) is a combination of both - when you have pattern matching, your method signatures aren't just types & arity, but also patterns.

 

No need for language support for your "typeunion" suggestion (though really, it's more generic than that), and of course you can do the same as in my example just with "when String,Integer"; the only thing this buys you is the ability to pass the list around as an object:

class TypeUnion
  def initialize(*args)
    @types = args
  end

  def ===(arg)
    @types.detect{|t| t === arg}
  end
end

def typeunion(*args)
  return TypeUnion.new(*args)
end

def test(arg)
  case arg
  when typeunion(String, Integer)
    puts "String or Integer: #{arg}"
  else
    puts "What's this? #{arg.inspect}"
  end
end

test(4)
test("foo")
test(1.0)
test(false)

As for your suggestion for multiple dispatch, I don't think it's a hard change at all. I'm working on a Ruby compiler, and the way I'd implement something like that would simply be to create hidden methods with the type signature for rewrites at call-sites where I can prove the types at compile time, and make the "plain" method a generated method that does the type checks and dispatches to each implementation. Conceptually it's really just wrapping every variant in a big "case", and not hard at all.

The hard part is getting people to agree to add it.

 

Thanks for giving a proper name for what I'm looking for. Neither pattern matching nor method overload mentioned above did not seem correct. Multiple dispatch clicks.

Perhaps you're right that it soudn't be so hard to implement. But wouldn't it break backwards compatibility somehow? Ruby core team is known for keeping it at all costs.

 

It would break semantics if overloads are allowed in a way that is syntactically valid in older code. So the challenge would be to pick syntax to specify the type signatures that can not occur in old working code.

Finding syntax that doesn't look awful but that is currently invalid would actually likely be the hardest part of adding support for it.