Skip to content

Instantly share code, notes, and snippets.

@pvande
Last active June 28, 2019 15:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pvande/822a1aba02e5347c39e8e0ac859d752b to your computer and use it in GitHub Desktop.
Save pvande/822a1aba02e5347c39e8e0ac859d752b to your computer and use it in GitHub Desktop.
# 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
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
Copy link
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