Skip to content
Permalink
Browse files

Pattern matching is no longer experimental

  • Loading branch information
k-tsj committed Nov 1, 2020
1 parent 4f8d9b0 commit b60153241121297c94de976419d421683da4d51b
Showing with 46 additions and 64 deletions.
  1. +17 −1 NEWS.md
  2. +18 −48 doc/syntax/pattern_matching.rdoc
  3. +9 −13 parse.y
  4. +2 −2 test/ruby/test_pattern_matching.rb
18 NEWS.md
@@ -48,7 +48,23 @@ sufficient information, see the ChangeLog file or Redmine
instead of a warning. yield in a class definition outside of a method
is now a SyntaxError instead of a LocalJumpError. [[Feature #15575]]

* Find pattern is added. [[Feature #16828]]
* Pattern matching is no longer experimental. [[Feature #17260]]

* One-line pattern matching now uses `=>` instead of `in`. [EXPERIMENTAL]
[[Feature #17260]]

```ruby
# version 3.0
{a: 0, b: 1} => {a:}
p a # => 0
# version 2.7
{a: 0, b: 1} in {a:}
p a # => 0
```

* Find pattern is added. [EXPERIMENTAL]
[[Feature #16828]]

```ruby
case ["a", 1, "b", "c", 2, "d", "e", "f", 3]
@@ -1,12 +1,8 @@
= Pattern matching

Pattern matching is an experimental feature allowing deep matching of structured values: checking the structure and binding the matched parts to local variables.
Pattern matching is a feature allowing deep matching of structured values: checking the structure and binding the matched parts to local variables.

Pattern matching in Ruby is implemented with the +in+ operator, which can be used in a standalone expression:

<expression> in <pattern>

or within the +case+ statement:
Pattern matching in Ruby is implemented with the +case+/+in+ expression:

case <expression>
in <pattern1>
@@ -19,11 +15,15 @@ or within the +case+ statement:
...
end

(Note that +in+ and +when+ branches can *not* be mixed in one +case+ statement.)
or with the +=>+ operator, which can be used in a standalone expression:

<expression> => <pattern>

Pattern matching is _exhaustive_: if variable doesn't match pattern (in a separate +in+ statement), or doesn't matches any branch of +case+ statement (and +else+ branch is absent), +NoMatchingPatternError+ is raised.
(Note that +in+ and +when+ branches can *not* be mixed in one +case+ expression.)

Therefore, +case+ statement might be used for conditional matching and unpacking:
Pattern matching is _exhaustive_: if variable doesn't match pattern (in a separate +in+ clause), or doesn't matches any branch of +case+ expression (and +else+ branch is absent), +NoMatchingPatternError+ is raised.

Therefore, +case+ expression might be used for conditional matching and unpacking:

config = {db: {user: 'admin', password: 'abc123'}}

@@ -37,11 +37,11 @@ Therefore, +case+ statement might be used for conditional matching and unpacking
end
# Prints: "Connect with user 'admin'"

whilst standalone +in+ statement is most useful when expected data structure is known beforehand, to just unpack parts of it:
whilst the +=>+ operator is most useful when expected data structure is known beforehand, to just unpack parts of it:

config = {db: {user: 'admin', password: 'abc123'}}

config in {db: {user:}} # will raise if the config's structure is unexpected
config => {db: {user:}} # will raise if the config's structure is unexpected

puts "Connect with user '#{user}'"
# Prints: "Connect with user 'admin'"
@@ -113,7 +113,7 @@ Both array and hash patterns support "rest" specification:
end
#=> "matched"

In +case+ (but not in standalone +in+) statement, parentheses around both kinds of patterns could be omitted
In +case+ (but not in +=>+) expression, parentheses around both kinds of patterns could be omitted

case [1, 2]
in Integer, Integer
@@ -378,53 +378,23 @@ Additionally, when matching custom classes, expected class could be specified as

== Current feature status

As of Ruby 2.7, feature is considered _experimental_: its syntax can change in the future, and the performance is not optimized yet. Every time you use pattern matching in code, the warning will be printed:
As of Ruby 3.0, one-line pattern matching and find pattern are considered _experimental_: its syntax can change in the future. Every time you use these features in code, the warning will be printed:

{a: 1, b: 2} in {a:}
# warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!
[0] => [*, 0, *]
# warning: Find pattern is experimental, and the behavior may change in future versions of Ruby!
# warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby!

To suppress this warning, one may use newly introduced Warning::[]= method:

Warning[:experimental] = false
eval('{a: 1, b: 2} in {a:}')
eval('[0] => [*, 0, *]')
# ...no warning printed...

Note that pattern-matching warning is raised at a compile time, so this will not suppress warning:

Warning[:experimental] = false # At the time this line is evaluated, the parsing happened and warning emitted
{a: 1, b: 2} in {a:}
[0] => [*, 0, *]

So, only subsequently loaded files or `eval`-ed code is affected by switching the flag.

Alternatively, command-line key <code>-W:no-experimental</code> can be used to turn off "experimental" feature warnings.

One of the things developer should be aware of, which probably to be fixed in the upcoming versions, is that pattern matching statement rewrites mentioned local variables on partial match, <i>even if the whole pattern is not matched</i>.

a = 5
case [1, 2]
in String => a, String
"matched"
else
"not matched"
end
#=> "not matched"
a
#=> 5 -- even partial match not happened, a is not rewritten

case [1, 2]
in a, String
"matched"
else
"not matched"
end
#=> "not matched"
a
#=> 1 -- the whole pattern not matched, but partial match happened, a is rewritten

Currently, the only core class implementing +deconstruct+ and +deconstruct_keys+ is Struct.

Point = Struct.new(:x, :y)
Point[1, 2] in [a, b]
# successful match
Point[1, 2] in {x:, y:}
# successful match
22 parse.y
@@ -502,7 +502,6 @@ static NODE *new_find_pattern(struct parser_params *p, NODE *constant, NODE *fnd
static NODE *new_find_pattern_tail(struct parser_params *p, ID pre_rest_arg, NODE *args, ID post_rest_arg, const YYLTYPE *loc);
static NODE *new_hash_pattern(struct parser_params *p, NODE *constant, NODE *hshptn, const YYLTYPE *loc);
static NODE *new_hash_pattern_tail(struct parser_params *p, NODE *kw_args, ID kw_rest_arg, const YYLTYPE *loc);
static NODE *new_case3(struct parser_params *p, NODE *val, NODE *pat, const YYLTYPE *loc);

static NODE *new_kw_arg(struct parser_params *p, NODE *k, const YYLTYPE *loc);
static NODE *args_with_numbered(struct parser_params*,NODE*,int);
@@ -1661,7 +1660,11 @@ expr : command_call
{
p->ctxt.in_kwarg = $<ctxt>3.in_kwarg;
/*%%%*/
$$ = new_case3(p, $1, NEW_IN($5, 0, 0, &@5), &@$);
$$ = NEW_CASE3($1, NEW_IN($5, 0, 0, &@5), &@$);

if (rb_warning_category_enabled_p(RB_WARN_CATEGORY_EXPERIMENTAL))
rb_warn0L(nd_line($$), "One-line pattern matching is experimental, and the behavior may change in future versions of Ruby!");

/*% %*/
/*% ripper: case!($1, in!($5, Qnil, Qnil)) %*/
}
@@ -2998,7 +3001,7 @@ primary : literal
k_end
{
/*%%%*/
$$ = new_case3(p, $2, $4, &@$);
$$ = NEW_CASE3($2, $4, &@$);
/*% %*/
/*% ripper: case!($2, $4) %*/
}
@@ -4176,6 +4179,9 @@ p_args_tail : p_rest
p_find : p_rest ',' p_args_post ',' p_rest
{
$$ = new_find_pattern_tail(p, $1, $3, $5, &@$);

if (rb_warning_category_enabled_p(RB_WARN_CATEGORY_EXPERIMENTAL))
rb_warn0L(nd_line($$), "Find pattern is experimental, and the behavior may change in future versions of Ruby!");
}
;

@@ -11679,16 +11685,6 @@ new_hash_pattern_tail(struct parser_params *p, NODE *kw_args, ID kw_rest_arg, co
return node;
}

static NODE *
new_case3(struct parser_params *p, NODE *val, NODE *pat, const YYLTYPE *loc)
{
NODE *node = NEW_CASE3(val, pat, loc);

if (rb_warning_category_enabled_p(RB_WARN_CATEGORY_EXPERIMENTAL))
rb_warn0L(nd_line(node), "Pattern matching is experimental, and the behavior may change in future versions of Ruby!");
return node;
}

static NODE*
dsym_node(struct parser_params *p, NODE *node, const YYLTYPE *loc)
{
@@ -1473,13 +1473,13 @@ def assert_experimental_warning(code)
assert_warn('') {eval(code)}
Warning[:experimental] = true
assert_warn(/Pattern matching is experimental/) {eval(code)}
assert_warn(/is experimental/) {eval(code)}
ensure
Warning[:experimental] = w
end
def test_experimental_warning
assert_experimental_warning("case 0; in 0; end")
assert_experimental_warning("case [0]; in [*, 0, *]; end")
assert_experimental_warning("0 => 0")
end
end

0 comments on commit b601532

Please sign in to comment.
You can’t perform that action at this time.