Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

“Useless Ruby sugar”: Pattern matching (Pt. 1)

Oct 20, 2023This is a part of a blog post series about “useless” (or: controversial) Syntax elements that emerged in recent Ruby version. The goal of the series is not to defend (or criticize) the features, but to share a “thought framework” for analysis of their reasons, design, and effect the new syntax has on a code that uses it. See also intro post, and the previos text that covered numbered block arguments.Our today’s theme is pattern matching.Pattern matching emerged in Ruby 2.7 as an experimental feature and went through several improvements and scope expansions in 3.0, 3.1, and 3.2.It is a syntax that allows to match nested data structures declaratively and, at the same time, bind some of its parts to local variables. Say,There are several language constructs in which patterns can be used:There is an elaborate yet pretty natural syntax for patterns, which we’ll see in a few paragraphs.The importance of this feature and its effect on the Ruby code is a source of severe disagreements. Some put it in the “mere syntax sugar” bin (which is how it ended up as a part of this series… which I already regret a bit because the feature is a huge thing to discuss!). At the opposite extremum, there are people who believe that pattern matching is a separate paradigm, and “if you want a language with one, you just switch to that language.”At the time of writing, pattern matching definitely got some adoption, and for all I can say, it didn’t (yet?) change Ruby’s style significantly. All in all, if you focus only on the “check the structure” aspect, it is easy to argue that we’ve got just a witty short syntax for a bunch of is_a? and ==. But all the variety that “match-and-bind” brings to a code style requires more layered assessment.You might do yours by reading further!There are many ways to talk about pattern matching’s virtues, but the main intuition that triggers the craving for “something like that” is symmetry.In any modern high-level language, it is very easy to declaratively build a nested data structure of arbitrary depth, width, and complexity. You just spell it “as it is,” and embed variables, constants, and calculations as you please:We take this way of building data structures for granted already, and any language/API that still requires an imperative way to pronounce it step by step (“create an array, put this into first element, then put this in second…”) looks retrograde.But what about the opposite operation? What if one wants to take a big (or not so big, yet nested) data structure and get data from it in a way that will look just as declarative? “We expect this structure, and we need to work those parts of it.”There are many possible answers (lenses are quite cool!), but the usual process of the evolution of programming languages, via ideas blending and migration from academic to everyday seems to have established the common sympathy for a structural pattern matching: some variety of a declarative match-and-bind syntax, preferably looking symmetrically to a data structures construction.Newer languages like Rust arrived with the construct from the start, and older high-level ones started to introduce it all over the industry, from Python to C#.A couple of decades before, the same happened with the idea of regular expressions: once seen as esoteric or, in the best case, specialized library tool, they eventually become ubiquitous.The “can we match other data structures declaratively” is logically the next step—and Ruby is not an exception1. Especially considering that some amount of deconstructing/structural checks is present in the language already—and that seemed to be fortunate, as matching generic data structures requires much deeper integration with the language than regexps do.Ruby does have a structural deconstruction already—unfortunately, only for arrays.For those who missed the recent developments and is confused by p(a:, b:) syntax: I am using keyword argument values omission—a feature introduced in Ruby 3.1. Basically, p(a:, b:) is exactly the same as p(a: a, b: b), and it is super-helpful for debugging, among other things. Oh, and there definitely would be a post later in the series discussing reasons and consequences of this “useless sugar”!It is more powerful than many of us are initially aware, allowing nested structures unpacking:It also works in implicit assignment to method and block arguments:One might’ve dreamt that this worked for hash decomposition and an arbitrary mix of arrays and hashes:…but alas, this is a syntax error!It should be noted that this syntax only provides binding (destructuring), not matching (checking the shape). If you pass data that doesn’t match the intended shape, there would be no error most of the time2, just a bunch of nils/empty arrays in the parts that weren’t matched:The matching in classic Ruby is implemented with the “case equality operator” ===. By default, it is the same as equality, but a lot of classes redefine it, so you have this:The best-known usage of the operator is implicit invocation inside case branching (hence the operator name). In the following code === is implicitly called for each branch, passing the argument there, until one returns true:(Obviously, in most cases the list of options is not that motley; it is more usual to see homogeneous branches like “it is nil, or one of those classes”; “it is definitely a string, but branch by regexp,” and so on.)Despite the operator name, it is useful in a few other core constructs, such as generalized grep:The power of === stops here, though: there is no way to recursively match nested structures, nor is there a way to assign (bind) some variables based on a successful match.case has an “escape hatch” form for when patterns aren’t powerful enough: without object to match, it just works like a tree of if/elsif, but looking more regularly and underlining the branches are homogeneous:Several codebases I worked with encouraged this style of delegating by branches—of course, with reasonable complexity of checks. Others (including the default Rubocop style) consider it a “stylistic error” and misuse of case. In any case, it doesn’t help much with comparison expressiveness, just unifies branches a bit.To those who handle vaguely structured data a lot, there was always a feeling—an intuition, if you want!—that there should be a more expressive and Ruby-idiomatic way to describe the expectations. And so, in Ruby, the quest for a pattern matching frequently was seen as taking the “case equality” power further3.Random fun fact: my Ruby blog was once started because I wanted a place to share my experiments/thoughts on ===-based pattern matching implementation in a library. The conclusion of that early article was: “All in all, powerful pattern matching needs to be a core language feature.”In the wake of the Ruby 3.0 approach, the pressure in the community, “we should finally introduce it,” grew higher. Say, Ruby Conf 2017 had a talk that showed the working prototype of %p() syntax for patterns. At that time, Matz said about such prototypes:If we were going to add pattern matching in Ruby, we should add it with better syntax. […] The problem is that I have no idea for an excellent syntax for the pattern matching right now.I, too, returned to the topic at that time, trying to reason about how that “excellent syntax” might look. A curious reader might enjoy comparing the ideas in that post with the form the pattern matching syntax has taken in Ruby eventually.Finally, in 2.7 (Christmas 2019), the new feature arrived, to some extent as a surprise: it emerged not long before the final release and was documented only by a conference slides link in the language changelog. But it was what Matz finally accepted (and, if I understand correctly, to some extent designed).It turned out to be integrated with the case statement, but not via some new powerful object supporting ===, as many expected. The chosen solution was to use a new in keyword/operator4:The highlight of the solution is that the syntax of the patterns themselves turned out to look incredibly well-aligned with the language user’s habits (or, again, intuitions!):This clarity came with a price that is not that low: the patterns syntax is fully isolated from the rest of the language. You can’t put a pattern into a variable or constant or pass it to a method: there is no such thing as a “pattern object.” It is just a special syntax that works after in.To be fair, that was probably the only way to introduce a proper pattern matching into a language that expressive this late into its life cycle: all of the “natural” syntax of the patterns above ([Integer, Integer] and so on) were already valid syntax constructs in Ruby with different meanings.The feature was cautiously marked “experimental” in Ruby 2.7, yet the basis laid then had proven itself reasonable.In the next versions, pattern matching received some polishing and cleaning up. The most notable change was the establishing of two types of one-line pattern matching statements:In 2.7, there was initially only in form, and it was raising an error when not matched. There was a turbulent discussion about this and also about the order of arguments (in many languages, it is values, to, assign = pattern), which eventually led to establishing the pair of operators we have now.One of the results of this discussion (and comparing with other languages) is emphasizing the one-line statements are also working in the simplest form of “match the whole statement into one variable.” This is mostly useless for in, but made => into a novel form of assignment, dubbed “rightward assignment”:A lot of other things have happened to pattern matching through the recent versions: the introduction of “find patterns,” more powerful pinning, adding support for deconstruction to several core classes, and so on.A big feature, after all. It required some significant compromises, and brought some interesting and far-reaching consequences, which, I believe, haven’t fully materialized yet!The feature is so big, that we are currently at half of its supposed discussion. See you next week for the rest of it. As with the numbered parameters, it would contain the feature grounded critique and analysis:You can subscribe to my Substack to not miss the next version, or follow me on Twitter.Thank you for reading. Please support Ukraine with your donations and lobbying for military and humanitarian help. Here, you’ll find a comprehensive information source and many links to state and private funds accepting donations.If you don’t have time to process it all, donating to Come Back Alive foundation is always a good choice.If you’ve found the post (or some of my previous work) useful, I have a Buy Me A Coffee account now. Till the end of the war, 100% of payments to it (if any) would be spent on my or my brothers’ necessary equipment or sent to one of the funds above.I believe that would Ruby be designed in today’s landscape, when “what is typical structural matching” is established, it would have that in the core of the language immediately—like back at the times it was born, it took a great effort to incorporate then-bleeding-edge-of-mainstream concepts like class methods, iterators, and such. ↩Except for attempt to pass not enough argument on method(*args) call— but even that works only for a limited case of one layer of positional arguments. ↩Sometimes, I wonder what turn the history might’ve had if the discussion focused on “better unpacking” instead and tried to get closer to the goal from this side— But we’ll never know, I guess. ↩in was a reserved keyword in older versions of Ruby, supporting the syntax for el in array (which is almost never used), so it was safe to reuse: in as a local name was always invalid, and there was no conflict with for ... in ... for the parser, even if somebody was using it. ↩I don't build systems. I imagine them, then write them.



This post first appeared on VedVyas Articles, please read the originial post: here

Share the post

“Useless Ruby sugar”: Pattern matching (Pt. 1)

×

Subscribe to Vedvyas Articles

Get updates delivered right to your inbox!

Thank you for your subscription

×