[前][次][番号順一覧][スレッド一覧]

ruby-changes:60171

From: zverok <ko1@a...>
Date: Mon, 24 Feb 2020 00:28:33 +0900 (JST)
Subject: [ruby-changes:60171] 281b350058 (master): Add pattern matching documentation

https://git.ruby-lang.org/ruby.git/commit/?id=281b350058

From 281b3500580f9ec93ee17679c648eaeb4a47f8b6 Mon Sep 17 00:00:00 2001
From: zverok <zverok.offline@g...>
Date: Wed, 25 Dec 2019 20:39:42 +0200
Subject: Add pattern matching documentation

Add separate doc/syntax/pattern_matching.rdoc, add
link to control_expressions.rdoc.

The documentation is "reverse-engineered" from Ruby 2.7
behavior and early preview presentations, and corrected
by pattern-matching feature author @k-tsj.

diff --git a/doc/syntax.rdoc b/doc/syntax.rdoc
index cdcb18d..5895673 100644
--- a/doc/syntax.rdoc
+++ b/doc/syntax.rdoc
@@ -11,6 +11,9 @@ Assignment[rdoc-ref:syntax/assignment.rdoc] :: https://github.com/ruby/ruby/blob/trunk/doc/syntax.rdoc#L11
 {Control Expressions}[rdoc-ref:syntax/control_expressions.rdoc] ::
   +if+, +unless+, +while+, +until+, +for+, +break+, +next+, +redo+
 
+{Pattern matching}[rdoc-ref:syntax/pattern_matching.rdoc] ::
+  Experimental structural pattern matching and variable binding syntax
+
 Methods[rdoc-ref:syntax/methods.rdoc] ::
   Method and method argument syntax
 
diff --git a/doc/syntax/control_expressions.rdoc b/doc/syntax/control_expressions.rdoc
index f7e6d54..e91b03e 100644
--- a/doc/syntax/control_expressions.rdoc
+++ b/doc/syntax/control_expressions.rdoc
@@ -232,7 +232,7 @@ You may use +then+ after the +when+ condition.  This is most frequently used https://github.com/ruby/ruby/blob/trunk/doc/syntax/control_expressions.rdoc#L232
 to place the body of the +when+ on a single line.
 
   case a
-  when 1, 2 then puts "a is one or two
+  when 1, 2 then puts "a is one or two"
   when 3    then puts "a is three"
   else           puts "I don't know what a is"
   end
@@ -255,6 +255,20 @@ Again, the +then+ and +else+ are optional. https://github.com/ruby/ruby/blob/trunk/doc/syntax/control_expressions.rdoc#L255
 The result value of a +case+ expression is the last value executed in the
 expression.
 
+Since Ruby 2.7, +case+ expressions also provide a more powerful experimental
+pattern matching feature via the +in+ keyword:
+
+  case {a: 1, b: 2, c: 3}
+  in a: Integer => m
+    "matched: #{m}"
+  else
+    "not matched"
+  end
+  # => "matched: 1"
+
+The pattern matching syntax is described on
+{its own page}[rdoc-ref:syntax/pattern_matching.rdoc].
+
 == +while+ Loop
 
 The +while+ loop executes while a condition is true:
diff --git a/doc/syntax/pattern_matching.rdoc b/doc/syntax/pattern_matching.rdoc
new file mode 100644
index 0000000..a7f7304
--- /dev/null
+++ b/doc/syntax/pattern_matching.rdoc
@@ -0,0 +1,441 @@ https://github.com/ruby/ruby/blob/trunk/doc/syntax/pattern_matching.rdoc#L1
+= 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 in Ruby is implemented with the +in+ operator, which can be used in a standalone expression:
+
+    <expression> in <pattern>
+
+or within the +case+ statement:
+
+    case <expression>
+    in <pattern1>
+      ...
+    in <pattern2>
+      ...
+    in <pattern3>
+      ...
+    else
+      ...
+    end
+
+(Note that +in+ and +when+ branches can *not* be mixed in one +case+ statement.)
+
+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.
+
+Therefore, standalone +in+ statement is most useful when expected data structure is known beforehand, to unpack parts of it:
+
+   def connect_to_db(config) # imagine config is a huge configuration hash from YAML
+     # this statement will either unpack parts of the config into local variables,
+     # or raise if config's structure is unexpected
+     config in {connections: {db: {user:, password:}}, logging: {level: log_level}}
+     p [user, passsword, log_level] # local variables now contain relevant parts of the config
+     # ...
+   end
+
+whilst +case+ form can be used for matching and unpacking simultaneously:
+
+  case config
+  in String
+    JSON.parse(config)                    # ...and then probably try to match it again
+
+  in version: '1', db:
+    # hash with {version: '1'} is expected to have db: key
+    puts "database configuration: #{db}"
+
+  in version: '2', connections: {database:}
+    # hash with {version: '2'} is expected to have nested connection: database: structure
+    puts "database configuration: #{database}"
+
+  in String => user, String => password
+    # sometimes connection is passed as just a pair of (user, password)
+    puts "database configuration: #{user}:#{password}"
+
+  in Hash | Array
+    raise "Malformed config structure: #{config}"
+  else
+    raise "Unrecognized config type: #{config.class}"
+  end
+
+See below for more examples and explanations of the syntax.
+
+== Patterns
+
+Patterns can be:
+
+* any Ruby object (matched by <code>===</code> operator, like in +when+);
+* array pattern: <code>[<subpattern>, <subpattern>, <subpattern>, ...]</code>;
+* hash pattern: <code>{key: <subpattern>, key: <subpattern>, ...}</code>;
+* special match-anything pattern: <code>_</code>;
+* combination of patterns with <code>|</code>.
+
+Any pattern can be nested inside array/hash patterns where <code><subpattern></code> is specified.
+
+Array patterns match arrays, or objects that respond to +deconstruct+ (see below about the latter).
+Hash patterns match hashes, or objects that respond to +deconstruct_keys+ (see below about the latter). Note that only symbol keys are supported for hash patterns, at least for now.
+
+An important difference between array and hash patterns behavior is arrays match only a _whole_ array
+
+  case [1, 2, 3]
+  in [Integer, Integer]
+    "matched"
+  else
+    "not matched"
+  end
+  #=> "not matched"
+
+while the hash matches even if there are other keys besides specified part:
+
+  case {a: 1, b: 2, c: 3}
+  in {a: Integer}
+    "matched"
+  else
+    "not matched"
+  end
+  #=> "matched"
+
+There is also a way to specify there should be no other keys in the matched hash except those explicitly specified by pattern, with <code>**nil</code>:
+
+  case {a: 1, b: 2}
+  in {a: Integer, **nil} # this will not match the pattern having keys other than a:
+    "matched a part"
+  in {a: Integer, b: Integer, **nil}
+    "matched a whole"
+  else
+    "not matched"
+  end
+  #=> "matched a whole"
+
+Both array and hash patterns support "rest" specification:
+
+  case [1, 2, 3]
+  in [Integer, *]
+    "matched"
+  else
+    "not matched"
+  end
+  #=> "matched"
+
+  case {a: 1, b: 2, c: 3}
+  in {a: Integer, **}
+    "matched"
+  else
+    "not matched"
+  end
+  #=> "matched"
+
+In +case+ (but not in standalone +in+) statement, parentheses around both kinds of patterns could be omitted
+
+  case [1, 2]
+  in Integer, Integer
+    "matched"
+  else
+    "not matched"
+  end
+  #=> "matched"
+
+  case {a: 1, b: 2, c: 3}
+  in a: Integer
+    "matched"
+  else
+    "not matched"
+  end
+  #=> "matched"
+
+== Variable binding
+
+Besides deep structural checks, one of the very important features of the pattern matching is the binding of the matched parts to local variables. The basic form of binding is just specifying <code>=> variable_name</code> after the matched (sub)pattern (one might find this similar to storing exceptions in local variables in <code>rescue ExceptionClass => var</code> clause):
+
+  case [1, 2]
+  in Integer => a, Integer
+    "matched: #{a}"
+  else
+    "not matched"
+  end
+  #=> "matched: 1"
+
+  case {a: 1, b: 2, c: 3}
+  in a: Integer => m
+    "matched: #{m}"
+  else
+    "not matched"
+  end
+  #=> "matched: 1"
+
+If no additional check is required, only binding some part of the data to a variable, a simpler form could be used:
+
+  case [1, 2]
+  in a, Integer
+    "matched: #{a}"
+  else
+    "not matched"
+  end
+  #=> "matched: 1"
+
+  case {a: 1, b: 2, c: 3}
+  in a: m
+    "matched: #{m}"
+  else
+    "not matched"
+  end
+  #=> "matched: 1"
+
+For hash patterns, even a simpler form exists: key-only specification (without any value) binds the local variable with the key's name, too:
+
+  case {a: 1, b: 2, c: 3}
+  in a:
+    "matched: #{a}"
+  else
+    "not matched"
+  end
+  #=> "matched: 1"
+
+Binding works for nested patterns as well:
+
+  case {name: 'John', friends: [{name: 'Jane'}, {name: 'Rajesh'}]}
+  in name:, friends: [{name: first_friend}, *]
+    "matched: #{first_friend}"
+  else
+    "not matched"
+  end
+  #=> "matched: Jane"
+
+The "rest" part of a pattern also can be bound to a variable:
+
+  case [1, 2, 3]
+  in a, *rest
+    "matched: #{a}, #{rest}"
+  else
+    "not matched"
+  end
+  #=> "matched: 1, [2, 3]"
+
+  case {a: 1, b: 2, c: 3}
+  in a:, **rest
+    "matched: #{a}, #{rest}"
+  else
+    "not matched"
+  end
+  #=> "matched: 1, {:b=>2, :c=>3}"
+
+Binding to variables currently does NOT work for alternative patterns joined with <code>|</code>:
+
+  case {a: 1, b: 2}
+  in {a: } | Array
+    "matched: #{a}"
+  else
+    "not matched"
+  end
+  # SyntaxError (illegal variable in alternative pattern (a))
+
+The match-anything pattern <code>_</code> is the only exclusion from this rule: it still binds the first match to local variable <code>_</code>, but allowed to be used in alternative patterns:
+
+  case {a: 1, b: 2}
+  in {a: _} | Array
+    "matched: #{_}"
+  else
+    "not matched"
+  end
+  # => "matched: 1"
+
+It is, though, not advised to reuse bound value, as <code>_</code> pattern's goal is to signify discarded value.
+
+== Variable pinning
+
+Due to variable binding feature, existing local variable can't be straightforwardly used as a sub-pattern:
+
+  expectation = 18
+
+  case [1, 2]
+  in expectation, *rest
+    "matched. expectation was: #{expectation}"
+  else
+    "not matched. expectation was: #{expectation}"
+  end
+  # expected: "not matched. expectation was: 18"
+  # real: "matched. expectation was: 1" -- local variable just rewritt (... truncated)

--
ML: ruby-changes@q...
Info: http://www.atdot.net/~ko1/quickml/

[前][次][番号順一覧][スレッド一覧]