Ruby – Pattern Matching – Second Impressions
Paweł Świątkowski
Posted on April 18, 2019
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.
Posted on April 18, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.