Skip to content

Instantly share code, notes, and snippets.

@pvande pvande/a-match-operator-wishlist.rb Secret
Last active Jun 28, 2019

Embed
What would you like to do?
# We can already do destructuring array assignments, but they're not particularly strict.
x, y, z = [1, 2]
# => x = 1, y = 2, z = nil
x, y, z = [1, 2, 3, 4]
# => x = 1, y = 2, z = 3
# The match operator should be strict.
x, y, z <match-op> [1, 2]
# => fails; wrong number of elements
x, y, z <match-op> [1, 2, 3, 4]
# => fails; wrong number of elements
x, y, z <match-op> [1, 2, 3]
# => succeeds; same as `x, y, z = [1, 2, 3]`
# The match operator should also permit non-variables in the pattern.
# These should be tested using case equality (===).
nil, y, z <match-op> [1, 2, 3]
# => fails; first element is not === `nil`
Integer, &:even?, z <match-op> [1, 2, 3]
# => succeeds; z == 3
# The match operator should also permit nested destructuring and matches
x, y, [a, b] <match-op> [1, 2, [3, 4]]
# => succeeds; x == 1, y == 2, a == 3, b == 4
x, y, [9, b] <match-op> [1, 2, [3, 4]]
# => fails; nested condition did not match
# The match operator should also allow us to destructure hashes.
# It's probably reasonable to restrict this to hashes with Symbolic keys.
data = { a: 1, b: 2, c: 3 }
{ a:, b:, c: } <match-op> data
# => succeeds; same as `a, b, c = data.values_at(:a, :b, :c)`
# Similarly, this matching should be strict.
data = { a: 1, b: 2, c: 3, d: 4 }
{ a:, b:, c: } <match-op> data
# => fails; wrong number of keys
{ a:, b:, c:, ** } <match-op> data
# => succeeds; same as `a, b, c = data.values_at(:a, :b, :c)`
# Similarly, this matching should permit case-equality tests.
data = { a: 1, b: 2, c: 3 }
{ a: &:odd?, b: 2, c: } <match-op> data
# => succeeds; c == 3
# Deep comparisons are also useful.
data = { name: "ko1", age: 39, address: { postal: 123, city: "Taito-ku" } }
{ name:, age: (20..), address: { city: "Taito-ku", ** } } <match-op> data
# suceeds; name == 'ko1'
# My preference would be that block matching is only a terser form of the inline
# matching behavior.
data = [log_level, "Message"]
if :debug, x <match-op> data
STDOUT.puts x
elsif :debug, x <match-op> data
STDOUT.puts x
elsif :debug, x <match-op> data
STDERR.puts x.to_s.downcase
elsif :debug, x <match-op> data
STDERR.puts x
elsif :debug, x <match-op> data
STDERR.puts x.to_s.upcase
else
raise :NotMatched
end
# vs.
<match-block> data
<match-op> :debug, x
STDOUT.puts x
<match-op> :info, x
STDOUT.puts x
<match-op> :warn, x
STDERR.puts x.to_s.downcase
<match-op> :error, x
STDERR.puts x
<match-op> :fatal, x
STDERR.puts x.to_s.upcase
end
# Like the case statement, I would like to see an operator that allows for a
# single line match expression.
<match-block> data
<match-op> :debug, x then STDOUT.puts(x)
<match-op> :info, x then STDOUT.puts(x)
<match-op> :warn, x then STDERR.puts(x.to_s.downcase)
<match-op> :error, x then STDERR.puts(x)
<match-op> :fatal, x then STDERR.puts(x.to_s.upcase)
end
# I would also like to see some form of multi-pattern matching.
# For symmetry with `case`, each pattern would traditionally be separated by
# commas, which would require patterns to be "contained" somehow to avoid
# syntactic ambiguity. I'm not opposed to (e.g.) requiring patterns to present
# as Array- or Hash-like "literals".
<match-block> data
<match-op> [:debug, x], [:info, x]
# Executed for both :debug and :info.
STDOUT.puts x
<match-op> [:warn, x]
STDERR.puts x.to_s.downcase
<match-op> [:error, x]
STDERR.puts x
<match-op> [:fatal, x]
STDERR.puts x.to_s.upcase
end
# Alternately, I like the idea of the "when" statement in these matching blocks
# implicitly "falling through" if (and only if) there's no body. It deals
# nicely with the "hanging indent" problem of multiple long patterns broken
# across multiple lines. I do recognize this is a break in continuity with
# `case`, that there are potential parsing ambiguities, and that it fails to
# provide a way to provide a reasonable "no-op" case, so I fully expect to see
# this vetoed.
<match-block> data
<match-op> :debug, x
<match-op> :info, x
# Executed for both :debug and :info.
STDOUT.puts x
<match-op> :warn, x
STDERR.puts x.to_s.downcase
<match-op> :error, x
STDERR.puts x
<match-op> :fatal, x
STDERR.puts x.to_s.upcase
end
# I've seen the idea of an "alternation operator" (`|`) floated for joining
# multiple patterns, but I feel like there's a high likelihood that would
# further strain the readability of an already fairly dense pattern language.
<match-block> data
<match-op> (:debug, x) | (:info, x)
# Matches [:debug, _] and [:info, _]
# What does `:debug, x | :info, x` mean?
# What does `(:debug, x) | (:info, x), y` mean?
# What does `(:debug, x) | (:info, x) | y` mean?
<match-op> (:debug | :info), x
# Also matches [:debug, _] and [:info, _].
# What does `:debug | :info, x` mean?
# What does `:debug | :info, x, y` mean?
# What does `:debug | :info, x | y` mean?
end
# Overall, I most prefer the approach of having multiple sequential patterns be
# collapsed, in part because patterns cannot be assigned as values (to simplify
# the "comma-separated" case), and in part because it improves the readability
# of the cases where multiple similar patterns should all match to the same
# branch.
# I would support using `case` as the `<match-block>` keyword, but mixing
# pattern matching and value matching in the same `case` expression would seem
# both supported and undefinable. That holds true whether `<match-op>` is a new
# keyword or a reuse of `when`.
# I think my preference would be to introduce a new `<match-block>` keyword, and
# possibly reuse `when` as the `<match-op>` in that context.
match data
when a, b, 3, /[bB][a-z]*/, *rest
puts rest, [a, b].inspect
end

The feature proposed in #15865 catches most of the what's described here, with a couple differences:

  • Matching is not strict.
  • The proposed <match-op> is in.
  • The pattern is on the RHS.

I could be persueded that strictness is undesirable, but I also see it as a potential way to differentiate destructuring assignment and pattern matching (particularly in the case where no actual tests are a part of the pattern).

<expr> in <pattern> looks great as a standalone construct on paper, but the question posed by data in <pattern> or [1, 2, 3] in <pattern> is hard to swallow in English. The implication is something like "[is] <expr> in [the set of all possible values that will match] <pattern>", which is mathematically sound, but not a natural way to read the expression. It's much more likely to be understood as "[is this] <expr> [contained by] <pattern>" (as in <pattern>.includes?(<value>)), which the feature actually bears little similarity to. The ordering of <expr> in <pattern> is also a reversal of how the same operator is currently used in Ruby (e.g. for x, y in { a: 1, b: 2 }).

As for putting the pattern on the RHS, I personally dislike it. The pattern syntax already makes it a bit difficult to tell what's being assigned to vs. tested against, and it feels as though the pattern is the more important part of the expression – doubly so, since the pattern cannot be stored in a variable and must be written inline. Unfortunately, the proposed <match-op> also reads strangely in when using <pattern> in <expr>, with the implied reading being akin to "[can you see a match for this] <pattern> in [this] <expr>". Arguably better, but still awkward.

The ideal English reading, IMHO, is something akin to "[does this] <pattern> [match this] <expr>". To that end match or matches would seem to be a better operator name than in, but come with the caveat of being common variable names.

@eregon

This comment has been minimized.

Copy link

eregon commented Jun 28, 2019

The match operator should be strict.

case/in is already strict, isn't it?

> case [1,2,3] in a,b; p 2 ; in a,b,c; p 3; else; p :else; end
(pry):1: warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!
3
=> 3
@pvande

This comment has been minimized.

Copy link
Owner Author

pvande commented Jun 28, 2019

That certainly seems to be the case! Assuming it would return else in the case without three arguments, I would be satisfied with that. Thanks for confirming that!

For reference, this “wishlist” was written without regard for prior discussion or implementation – it’s simply an expression of what I think I would expect this feature to look and function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.