Pattern matching is no longer experimental

This commit is contained in:
Kazuki Tsujimoto 2020-11-01 13:28:24 +09:00
Родитель 4f8d9b0db8
Коммит b601532411
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: BCEA306C49B81CD7
4 изменённых файлов: 46 добавлений и 64 удалений

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:
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.
<expression> => <pattern>
Therefore, +case+ statement might be used for conditional matching and unpacking:
(Note that +in+ and +when+ branches can *not* be mixed in one +case+ expression.)
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 @@ END
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