Useless Ruby sugar: Pattern matching(zverok.space)
zverok.space
Useless Ruby sugar: Pattern matching
https://zverok.space/blog/2023-10-20-syntax-sugar2-pattern-matching.html
50 comments
I think we now have an, “I worked on a Ruby implementation,” support group at work.
I know Java evolves quite slowly, and people complain about it, but I really respect the thought that goes into new Java features.
I know Java evolves quite slowly, and people complain about it, but I really respect the thought that goes into new Java features.
I really cant say i agree here. Ruby has fantastic pattern matching, and is simplifies so much code. I do work for shopify (but unfortunately not on the rails or ruby teams) so maybe a lot of these changes just jive with our culture and company. But why would you not want pattern matching? Ive seen it make pretty huge complicates pieces of code significantly simpler.
I might also have the "grumpy old man" syndrome, but I'm with you here. But it's not entirely unusual for the languagr. Despite Ruby being great for object oriented programming, it has a lot of features to allow for more functional approaches and other general coding styles. I'm not a fan of it, but if it makes someone happy, good for them. Ruby is meant to be the happy engineering language, so it's all on point. But I'd ask people from refraining from posting these new approaches as the "right way" to do things, as people are apt to do.
> I feel like a lot of the changes we get now are more "wouldn't it be cool if..."
I mean.....that is the ethos of Ruby from day 1, is it not?
I mean.....that is the ethos of Ruby from day 1, is it not?
Elixir does a great job with pattern matching. When I first heard that ruby was introducing pattern matching, I was super excited and hoped it might be a subset of what elixir can do. I was disappointed with what ruby shipped.
Same. I love Ruby so much but after doing so much in Elixir, the lack of elixir-style pattern matching and a pipe operator are so painful. Ruby is still a wonderful language and one of my favorites, but these were major missed opportunities. I got the impression that they wanted something different than what other langs have done, and I think it's great to consider alternative suggestions but it crossed the line a bit into NIH syndrome IMHO and I think we're worse off for it. Either that or the decision makers didn't really understand the point/purpose of it, but that feels unlikely to me.
As presented, this one is quite dangerous because it's easy to miss the variable assignment.
File.read('data.txt')
.split("\n")
.map { |ln| some_processing(ln) }
.select { |ln| some_filter(ln) }
.and_so_on => result
I never knew this type of assignment was possible in Ruby, but I will use it if the => var can follow on the next line. A quick test (in 3.1) suggests that it only works with an explicit line continuation: File.read('data.txt')
.split("\n") \
=> result
...which makes it less appealing. I'll have to try it for a bit to see if I prefer it over the normal `result =` assignment syntax.Solid read. Looking forward to part 2.
I've been looking for a good deep dive on Ruby's pattern matching after spending a lot of time with Elixir over the past few years. It can make your life a lot easier in many circumstances.
I've been looking for a good deep dive on Ruby's pattern matching after spending a lot of time with Elixir over the past few years. It can make your life a lot easier in many circumstances.
Ha, we "jinxed" eachother on posting about this and Elixir. Given your experience, are you seeing benefits of shared language patterns in multi-language systems at work (e.g. a ruby service, an elixir service, etc)?
It helps me mentally because I jump back and forth a lot.
I've been an Elixir fan for years to the point that it changed the way I think about programming. At the same time, it's really hard even for Elixir to beat a lot of the productivity benefits of Ruby and there's plenty of Ruby-oriented work out there too so I live in both worlds.
I write Ruby in a much more functional style now than I previously did, but there are always going to be times when you hit one of those "this would be so much easier in Elixir" moments (not just for Ruby) because there are certain hard things that are just simple in Elixir. Anytime I can utilize some of those techniques in Ruby it lightens my mental load.
I've been an Elixir fan for years to the point that it changed the way I think about programming. At the same time, it's really hard even for Elixir to beat a lot of the productivity benefits of Ruby and there's plenty of Ruby-oriented work out there too so I live in both worlds.
I write Ruby in a much more functional style now than I previously did, but there are always going to be times when you hit one of those "this would be so much easier in Elixir" moments (not just for Ruby) because there are certain hard things that are just simple in Elixir. Anytime I can utilize some of those techniques in Ruby it lightens my mental load.
> At the same time, it's really hard even for Elixir to beat a lot of the productivity benefits of Ruby
Can you give an example or explain what you mean?
Can you give an example or explain what you mean?
That's probably a blog post in and of itself. It's hard to explain but it comes down to a combination of the flexibility and malleability of the language itself, commitment from the community that shows in the tooling and ecosystem among other benefits.
It's the closest thing I've seen to Aspect Oriented Programming.
It's the closest thing I've seen to Aspect Oriented Programming.
Anyone using this and anyone using this with an Elixir background? I am curious if programming/language patterns like this, shared across languages, can help teams that have multi-language (eco)systems (e.g. ruby and elixir).
I don't have an Elixir background (though I envy Elixir in many ways), but I absolutely utilize pattern matching in my Ruby code bases:
- https://github.com/keygen-sh/typed_params/blob/4e4982b7d2b26...
- https://github.com/keygen-sh/typed_params/blob/4e4982b7d2b26...
- https://github.com/keygen-sh/keygen-api/blob/36cd61db143cc1c...
- https://github.com/keygen-sh/keygen-api/blob/36cd61db143cc1c...
- https://github.com/keygen-sh/typed_params/blob/4e4982b7d2b26...
I love it. I want even more pattern matching too, like defp: https://bugs.ruby-lang.org/issues/19764 (with original credit to https://zverok.space/blog/2023-05-05-ruby-types.html).
- https://github.com/keygen-sh/typed_params/blob/4e4982b7d2b26...
- https://github.com/keygen-sh/typed_params/blob/4e4982b7d2b26...
- https://github.com/keygen-sh/keygen-api/blob/36cd61db143cc1c...
- https://github.com/keygen-sh/keygen-api/blob/36cd61db143cc1c...
- https://github.com/keygen-sh/typed_params/blob/4e4982b7d2b26...
I love it. I want even more pattern matching too, like defp: https://bugs.ruby-lang.org/issues/19764 (with original credit to https://zverok.space/blog/2023-05-05-ruby-types.html).
I use both and I believe pattern matching helps a lot to be more defensive in the programs (verify pre-conditions) and raise accordingly, with a low effort in code.
I wrote Ruby for 10 years and Elixir for about 4.
The Ruby version is a bit awkward because it has to make compromises with existing syntax … however, I have already been able to make use of it. My Ruby code these days have gotten a lot more functional, and the type of code I write these days (devops tooling, not Rails code) involves complex transformation or parsing, which makes this useful.
I have not tried this with flow control (with :ok and :error tuples) … but I can tell you the missing half missing from this picture are function clauses. (How that would work with being able to redefine functions, I have no idea; just saying though ….)
Pattern matching comes all the way from logic programming (Prolog). It should not be surprising that this really shines when working with complex logic.
The Ruby version is a bit awkward because it has to make compromises with existing syntax … however, I have already been able to make use of it. My Ruby code these days have gotten a lot more functional, and the type of code I write these days (devops tooling, not Rails code) involves complex transformation or parsing, which makes this useful.
I have not tried this with flow control (with :ok and :error tuples) … but I can tell you the missing half missing from this picture are function clauses. (How that would work with being able to redefine functions, I have no idea; just saying though ….)
Pattern matching comes all the way from logic programming (Prolog). It should not be surprising that this really shines when working with complex logic.
https://docs.ruby-lang.org/en/master/syntax/pattern_matching... is worth a read to get a better understanding of the pattern matching capabilities than is conveyed in the various Ruby version announcements that added and expanded it. I'm a big fan of this syntax and use it constantly in my codebase. I also love numbered parameters, endless methods, and Ractors ;)
I have been envious of pattern matching for decades. It is one of those language features I saw (almost certainly in OCaml) and just desired it. I have not worked professionally in any language that supports it.
One tiny annoyance I have with many implementations of this feature:
But I hate nesting. In most cases when I am asserting some form of data what I want to do is exit the function if the data is invalid, perhaps with some error code.
So what I want is:
One tiny annoyance I have with many implementations of this feature:
# standalone `in` just returns `false` the pattern doesn't match, useful in `if`:
if point in [x, y]
# it was a 2-element array, and we checked and deconstructed it
# ...
end
This pattern, where `x` and `y` are introduced into the block is a pretty common way to expose a kind of pattern matching guard. You can be sure that inside the `if` block you have valid non-null references to both `x` and `y`.But I hate nesting. In most cases when I am asserting some form of data what I want to do is exit the function if the data is invalid, perhaps with some error code.
So what I want is:
unless point in [x, y]
log.error("Invalid point")
return /* ideally some Option/Result */
end
# code assuming valid x & y
This is one of those "wouldn't it be nice" kinda syntax sugar things, but if I have a series of guards that are progressively narrowing types it ends up feeling much nicer to me.Jakt [1] shares your design philosophy, and has implemented syntax like this: [2]
[2]: https://github.com/SerenityOS/jakt/blob/main/samples/guard/i...
guard foo is Bar(x) else {
println("not matched")
return -1 as! c_int
}
println("hello there: {}", x)
[1]: https://github.com/SerenityOS/jakt[2]: https://github.com/SerenityOS/jakt/blob/main/samples/guard/i...
Swift has that feature as well, restricted to optionals
guard let (a, b) = foo else {
return whatever
}
Rust recently, finally, stabilised a full pattern matching version: let Ok((a, b)) = foo else {
return whatever;
}
Although it's been available as a third-party macro (https://crates.io/crates/guard) for years: guard!(let Ok((a, b)) = foo else {
return whatever;
});I was thinking of `guard` in Swift when I was writing but I haven't used it so I was unsure of its exact semantics. It is a bit of a shame it is only useable on optionals and not on arbitrary types.
Nice to see a full pattern matching guard in Rust. I think the terminating `?` operator makes it a bit less necessary as I would guess a large portion of cases where I would want that early-return behavior I would just take advantage of that operator. But their choice to reuse `else` makes a lot of sense and the pattern reads well.
I was watching the recent Rust stream by Jon Gjengset where he builds a BitTorrent implementation [1]. One thing he did frequently was add a `.context` method to function invocations that would terminate in `?`. My assumption just looking at that pattern is that it would allow the error system to tag the errors to help guide the programmer to the specific line the error was generated from, almost like an annotated stack trace. But again, I don't have direct experience programming large Rust applications.
The reason I consider this topic is because this kind of error handling would be ideal in Go. I often end up with the classic pattern:
1. https://www.youtube.com/watch?v=jf_ddGnum_4&t=9465s&ab_chann...
Nice to see a full pattern matching guard in Rust. I think the terminating `?` operator makes it a bit less necessary as I would guess a large portion of cases where I would want that early-return behavior I would just take advantage of that operator. But their choice to reuse `else` makes a lot of sense and the pattern reads well.
I was watching the recent Rust stream by Jon Gjengset where he builds a BitTorrent implementation [1]. One thing he did frequently was add a `.context` method to function invocations that would terminate in `?`. My assumption just looking at that pattern is that it would allow the error system to tag the errors to help guide the programmer to the specific line the error was generated from, almost like an annotated stack trace. But again, I don't have direct experience programming large Rust applications.
The reason I consider this topic is because this kind of error handling would be ideal in Go. I often end up with the classic pattern:
if value, err := someFunc(); !err {
// value is useable
}
// handle err
And I would just really, really like if Go would let me: if value, err := someFunc(); err != nil {
// handle err
}
// value is useable
But, I suppose due to Go block scoping `value` is only valid within the if block.1. https://www.youtube.com/watch?v=jf_ddGnum_4&t=9465s&ab_chann...
I use it in Elixir. My Ruby has gotten functional these days so pattern matching is a welcome addition for me.
The other half of pattern matching is function clauses …
The other half of pattern matching is function clauses …
Pattern matching is an objectively good language feature. I wasn't aware the Ruby community sees it as controversial. Why does the Ruby ecosystem do so many things backwards compared to modern software engineering? Pattern matching bad, method_missing good?
I didn't know it was controversial either! D:
I also work at the small mom and pop e-commerce site, and i havent seen it being considered controversial anywhere. And once people see its benefits i think theve been picking it up. Just this week we had a PR that it serioisly reduced the code by 30%
Awesome talk at rails world btw.
Awesome talk at rails world btw.
Thank you!
Ya, I really enjoy pattern matching. I think new language features just take a long time to percolate through the community. I remember when `->` was controversial, but that's the only way I write lambdas today. :D
Ya, I really enjoy pattern matching. I think new language features just take a long time to percolate through the community. I remember when `->` was controversial, but that's the only way I write lambdas today. :D
Pattern matching bad, method_missing good?
`method_missing` hasn't been considered good practice in a very long time. i mean, most people never thought it was great, but it's a non-issue IMEthere are only a few usages of it remaining in activerecord. i thought they were all gone, honestly. (keep in mind about 1/2 of these are tests)
https://github.com/search?q=repo%3Arails%2Frails+path%3A%2F%...
I guess you haven't read Eloquent Ruby!
The book from twelve years ago?
I carry no brief for Ruby, and method_missing (and its extensive abuse in early Rails) is a major reason why. If you can find no more recent advocacy for that pattern than this, maybe it's time I took another look at the language in earnest.
I carry no brief for Ruby, and method_missing (and its extensive abuse in early Rails) is a major reason why. If you can find no more recent advocacy for that pattern than this, maybe it's time I took another look at the language in earnest.
I actually love method_missing. I don't think it should be used for the vast majority of use cases, and most ruby devs shouldn't even know it exists, but the power to reach deeply into things and tap sources is one of the things that makes Ruby amazing IMHO.
Oh, I'm not averse to reflection in general! It can be very elegant and still easily understandable when well used, ideally at compile time. No, it's just that this particular example was once egregiously overused, with an excess of cleverness aforethought that made debugging anything far more difficult than it really had any need to be.
It's not even that I can't see how the Rails implementors got there; on the one hand it was very early in the development of the modern web concept and nobody really knew what worked and what didn't yet, and on the other, a more modern pattern would have probably been very slow anyway in those days. Still, the experience left a bad taste that I've found hard to shake.
It's not even that I can't see how the Rails implementors got there; on the one hand it was very early in the development of the modern web concept and nobody really knew what worked and what didn't yet, and on the other, a more modern pattern would have probably been very slow anyway in those days. Still, the experience left a bad taste that I've found hard to shake.
Ruby is really good as a language these days, and has only gotten more performant.
Rubocop has gotten wide adoption and has standardized a lot of good practices. By default it's awfully strict.
I worked full-time with Ruby for 9 years across several companies and I solemnly swear that I never saw a single person use method_missing or consider it as anything other than a mostly terrible idea.
Never saw it used once, or even considered once, in actual application code.
Generally that was the case with any of those "sharp knives" or "potential footgun" language features. Monkey patching is another good example. Used to be extremely common and even recommended, but now it is extremely frowned-upon and used only with extreme care/desperation in application code.
The "problem" with Ruby is that it has become kind of a monoculture around Rails. Rare to see it used for anything but Rails these days. People who want a fun casual language are using Python because Python has that booming ML-adjacent ecosystem and a strong foothold in academia/science.
Rubocop has gotten wide adoption and has standardized a lot of good practices. By default it's awfully strict.
I worked full-time with Ruby for 9 years across several companies and I solemnly swear that I never saw a single person use method_missing or consider it as anything other than a mostly terrible idea.
Never saw it used once, or even considered once, in actual application code.
Generally that was the case with any of those "sharp knives" or "potential footgun" language features. Monkey patching is another good example. Used to be extremely common and even recommended, but now it is extremely frowned-upon and used only with extreme care/desperation in application code.
The "problem" with Ruby is that it has become kind of a monoculture around Rails. Rare to see it used for anything but Rails these days. People who want a fun casual language are using Python because Python has that booming ML-adjacent ecosystem and a strong foothold in academia/science.
Now you have your recent advocacy, which is common in the Ruby community. Embrace magic, embrace indirection.
(Author here)
Well, unfortunately a lot of new stuff becomes controversial on the pure ground of "no new language changes! it is good enough for me!"
TBH, I thought about a post series like this for a long time, but the last trigger was an announce of a special Rubocop addon to "disable useless syntax sugar"[1], and pattern matching was one of the "useless sugars" in the list. So I felt I need to cover it, too. And in the end of the day, I think it is an interesting excercise to analyse it in the same way as other, less significant and more controversial, features (the most intresting stuff would be in the second part, though).
1: https://www.reddit.com/r/ruby/comments/16slc10/announcing_ru...
Well, unfortunately a lot of new stuff becomes controversial on the pure ground of "no new language changes! it is good enough for me!"
TBH, I thought about a post series like this for a long time, but the last trigger was an announce of a special Rubocop addon to "disable useless syntax sugar"[1], and pattern matching was one of the "useless sugars" in the list. So I felt I need to cover it, too. And in the end of the day, I think it is an interesting excercise to analyse it in the same way as other, less significant and more controversial, features (the most intresting stuff would be in the second part, though).
1: https://www.reddit.com/r/ruby/comments/16slc10/announcing_ru...
I wasn't aware of that new cop. Anecdotally, find Rubocop (and Flog and Reek) to be backwards tools. To me, they pattern match (ha) as the same core problems with the Ruby ecosystem. Ignore best practices that other language ecosystems have evolved, and embrace what other languages agree are bad practice.
I'm glad I missed that r/ruby thread. I wouldn't have been very nice. I honestly can't tell if it's a troll or not. I love all of those features of Ruby, and use them daily.
Ruby is generally an object-oriented language, at least in practice if not by design, and pattern matching goes against the object-oriented grain (in my opinion, at least-would love to hear others).
The object oriented approach isn't to try and extract conclusions on a data structure so that we may utilize it correctly, but instead to use an object with a interface we can interact with regardless of the organization of the data. Neither approach is a right or wrong way to program, but culturally Ruby leans towards the object oriented mentality.
The object oriented approach isn't to try and extract conclusions on a data structure so that we may utilize it correctly, but instead to use an object with a interface we can interact with regardless of the organization of the data. Neither approach is a right or wrong way to program, but culturally Ruby leans towards the object oriented mentality.
I think you're onto what might be biggest internal struggle that I've noticed since starting in Ruby in 2011: The tension between object oriented and functional. In many ways the two are not in conflict, but occasionaly they do clash, and that's where things get messy. As someone who loves the functional approach, it saddens me when object-oriented ways get in the way of a great functional feature or pattern, but at the end of the day Ruby is thoroughly an object oriented system. IMHO it should err on the side of object oriented. For people that really want functional, it's probably better to look at a language like Elixir that is inspired greatly by Ruby but functional from the start.
If it helps at all, keep in mind that many people who think they're doing object-oriented ruby are actually doing functional programming. Take a look at the Service Object pattern--A globally accessible reference with a singular method class (`.call`) that takes arguments and returns a singular value, and without you ever interacting with an instance of the class, etc etc. This is just a function in disguise! Every true service object could be reimplemented as a proc without the rest of the codebase knowing anything changed.
Writing Object Oriented code is hard. If you don't have a solid understanding of SOLID, at best you end up reinventing functional programming, at worst you end up with a big plate of spaghetti code. That's why most ruby codebases are a huge mess. It's still my preferred approach, but it's certainly not for everyone, and it's certainly easy to muck up. Thankfully Ruby is flexible and allows both--people just need to be honest about what they're actually doing, and not present any implementation of Ruby as object oriented.
Writing Object Oriented code is hard. If you don't have a solid understanding of SOLID, at best you end up reinventing functional programming, at worst you end up with a big plate of spaghetti code. That's why most ruby codebases are a huge mess. It's still my preferred approach, but it's certainly not for everyone, and it's certainly easy to muck up. Thankfully Ruby is flexible and allows both--people just need to be honest about what they're actually doing, and not present any implementation of Ruby as object oriented.
The way 'syntax' is implemented in much of Ruby isn't processed by a separate parser, but rather is interpreted during execution of the source to create the objects/classes or modify them to do what is desired by the language feature. Thinking of things this way, it becomes more clear that it's not using the OOP features to get things done but rather mangling them to make pretty DSLs. I would object to too much of that too--except that in this case, I really like pattern matching.
Really like that second pattern matching construct that’s cited—for validating/raising an exception on and binding a data structure. Now i’m jealous and wish Python had that.
So let me get this straight, the purpose for pattern matching in Ruby is to destructure objects easily?
That said, I do really like how Elixir does some things.