Ruby – Pattern Matching – Second Impressions

Paweł Świątkowski - Apr 18 '19 - - Dev Community

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .