Skip to content

Commit

Permalink
Pattern matching is no longer experimental
Browse files Browse the repository at this point in the history
  • Loading branch information
k-tsj committed Nov 1, 2020
1 parent 4f8d9b0 commit b601532
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 64 deletions.
18 changes: 17 additions & 1 deletion NEWS.md
Expand Up @@ -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]
Expand Down
66 changes: 18 additions & 48 deletions doc/syntax/pattern_matching.rdoc
@@ -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>
Expand All @@ -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'}}

Expand All @@ -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'"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 changes: 9 additions & 13 deletions parse.y
Expand Up @@ -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);
Expand Down Expand Up @@ -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)) %*/
}
Expand Down Expand Up @@ -2998,7 +3001,7 @@ primary : literal
k_end
{
/*%%%*/
$$ = new_case3(p, $2, $4, &@$);
$$ = NEW_CASE3($2, $4, &@$);
/*% %*/
/*% ripper: case!($2, $4) %*/
}
Expand Down Expand Up @@ -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!");
}
;

Expand Down Expand Up @@ -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)
{
Expand Down
4 changes: 2 additions & 2 deletions test/ruby/test_pattern_matching.rb
Expand Up @@ -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
Expand Down

0 comments on commit b601532

Please sign in to comment.