It’s worth noting that JS has a pattern matching feature under consideration as well. https://github.com/tc39/proposal-pattern-matching
So, story time then as far as how this all started.
Fog City Ruby, Pat Shaughnessy was talking about FP in Ruby and lamenting that it didn’t really have pattern matching. Me and @havenwood were playing with some syntax after with case statements to try and make a psuedo-system on it and ended up writing this gist as a “proof of concept”:
https://gist.github.com/baweaver/611389c41c9005d025fb8e55448bf5f5
Afterwards we’d joked about maybe making it a gem, but I hadn’t quite thought it through. I saw @maybekatz tweet about pattern matching in JS (beginning of the proposal) not too long afterwards and thought why not? So I decided to make Qo and see how far I could push it.
Qo was meant far more as a “what if” and proof of concept than a “do this” exactly. I wanted to use it as a catalyst for discussion on how Ruby could have pattern matching while retaining some of its own natural look and feel. After it was released I pushed hard to get it in front of people and spread the idea around to see if it took hold.
Matz has said before that good language feature proposals start as gems of some sort and evolve, so that was the entire goal. Get them talking, give them ideas, and try and qualify all the concerns and nuances around it.
The part I didn’t quite expect was Zverok posting the issue. As soon as that dropped I basically took all the notes and thoughts I had and dumped them into the issue, including references to the TC39 proposal that really got me started on it.
So the short version of all of this is no, the timing was not an accident. This was the best time to try and get the issue brought up and get real attention to it, and it looks like we’re having some degree of success with it so far.
Pattern matching is one of the only features I regularly feel is lacking in Ruby.
There are other things I dislike or wish weren’t present, but the only missing feature is this.
Combining conditionals and assignment just feels 10x better than having them separate.
I even built a pattern matching library with some gross fun hacks on top of a specific namespace, thread-local singleton instance variables and caller_locations to allow for kinda-mostly pattern matching. I’m going to see if I can dig up the code, because aside from a first/rest pairing, I managed to get nested matching across Arrays and Hashes, and an Any object to match anything.
Then I bumped into a Ruby Tapas video on the topic and stole the hubcaps to implement Erlang-like guard clauses.
Have you looked at Qo at all? If so, any strong opinions about where it’s lacking? (Admittedly, it’s a giant hack, but it’s a pretty good giant hack.)
Hello! Author of Qo here. Yeah, you’re right, it is a bit of a giant hack XD
Anyways, I’d love to hear any ideas or suggestions on it. Typically I use the issue tracker to keep tabs on what I want to do with it, so feel free to jump in there as well.
Admittedly I’m also the one who wrote a small-scale novel on pattern matching in the description to try and cover bases as I really really really want this feature, as evidenced by writing Qo in the first place.
What do you think about the %p() or %m() syntaxes? I think they’re really ugly personally. Is it possible to make a spin on case which parses the when clauses as patterns without any special extra delimiters? You somewhat hit on that when talking about Scala style I think. Something like this maybe?
match val
when [:constant, "constant", [1], variable_binding]
variable_binding + 1
else
0
end
If you already covered that, apologies, I read your comments quickly last night and might have missed it.
EDIT Codefied my idea here - https://bugs.ruby-lang.org/issues/14709#note-6
match(value) do |m|
m.when(/name/, 42) { |name, age| Person.new(name, age) }
m.else { |object| raise "Can't convert!" }
end
An example practical usage:
def get_url(url)
Net::HTTP.get_response(URI(url)).then(&match do |m|
m.when(Net::HTTPSuccess) { |response| response.body.size }
m.else { |response| raise response.message }
))
end
Original:
Not a problem, there are a lot of them (may Matz and Koichi forgive me)
I hadn’t quite covered it yet. That’s always been the trick about this: what should it look like?
Truthfully I don’t know quite yet, but I’m working on ideas. In regards to the %p and %m I would agree with some Reddit comments that they can tend slightly Perl-ish. I’d like a more fluent syntax if possible that reads intuitively, and I don’t think that quite does it.
I had initially proposed this:
new_value = match value
when %m(:_, 20..99) { |_, age| age + 1 }
else { |object| ... }
end
…which is quite similar to your suggestion. I’d almost consider switching the syntax a bit to something more like this:
new_value = match value
when matches(:_, 20..99) { |_, age| age + 1 }
else matches(:_) { |object| ... }
end
When possible I would prefer very clear words as syntax over the percent shorthands. With this you could almost amend case to look for matches
Ahhh, I hadn’t seen how binding the match results as block arguments would be valuable, but your example of using === overloads like Class.=== and Regexp.=== have convinced me. I learned pattern matching in Erlang so I was thinking about the Erlang style mostly, and I didn’t think of how pattern matching would be most useful in Ruby. Blocks are a good way to reuse the case style matching that’s already well understood.
Can someone explain to me what the when statement in the example code does? Is it just like an advanced if?
Putting the syntax here for continuity:
The idea is that any variable name that’s not bound would be bound as a local variable inside the
whenstatement. In this casestatusis the match that would get assigned to500.Note that there are several other suggestions for how to perform this task syntactically in the thread, and personally I’d lean against any shorthand that’s not immediately clear what it does.
Thanks for that. Yeah it seems to me there should be a clearer way to express this.