of specifying patterns to which some data should conform and then checking to see if it does and deconstructing the data according to those patterns" • Learn You a Haskell for Great Good! (Miran Lipovaca) • For Rubyist • case/when + multiple assignment
if person[:name] == "Alice" children = person[:children] if children.length == 1 && children[0][:name] == "Bob" p children[0][:age] #=> 2 end end case JSON.parse(json, symbolize_names: true) in {name: "Alice", children: [{name: "Bob", age: age}]} p age # => 2 end • Without pattern matching
first one that matches • If no pattern matches, the else clause is executed • If no pattern matches and no else clause, NoMatchingPatternError exception is raised case expr in pattern [if|unless condition] ... in pattern [if|unless condition] ... else ... end
an existing variable's value, use ^ • It is same as the pin operator in Elixir a = 0 case 1 in ^a # means `in 0` :unreachable end #=> NoMatchingPatternError
solely for array objects • An array pattern matches if: • Constant === object returns true • The object has a #deconstruct method that returns Array • The result of applying the nested pattern to object.deconstruct is true pat: Constant(pat, ..., *var, pat, ...) | Constant[pat, ..., *var, pat, ...] | [pat, ..., *var, pat, ...] # Syntactic sugar for BasicObject(...)
objects • A hash pattern matches if: • Constant === object returns true • The object has a #deconstruct_keys method that returns Hash • The result of applying the nested pattern to object.deconstruct_keys(keys) is true pat: Constant(id: pat, id:, ..., **var) | Constant[id: pat, id:, ..., **var] | {id:, id: pat, **var} # Syntactic sugar for BasicObject(...)
implementation • If we implement deconstruct_keys imprudently, the results might be very inefficient class Time def deconstruct_keys(keys) { year: year, month: month, asctime: asctime, ctime: ctime, ..., yday: yday, zone: zone } end end case Time.now in year: p year #=> 2019 end
specified in the pattern • You can ignore any keys that are not included in keys • If **rest is specified in the pattern, nil is passed instead • In such case, you must return all key-value pairs class Time VALID_KEYS = %i(year month ...) def deconstruct_keys(keys) if keys (VALID_KEYS & keys).each_with_object({}) do |k, h| h[k] = send(k) end else {year: year, month: month, ...} end end end now = Time.now case now in year: # Calls now.deconstruct_keys([:year]), # receives {year: 2019} end
Published pattern-match gem • 2012/09/15: Sapporo RubyKaigi 2012 - "Pattern Matching in Ruby" • 2016/05/28: Tokyo RubyKaigi 11 - "Re: Pattern Matching in Ruby"
match(["foo-bar@example.com"]) do pattern(Array.(EMail.(/(\w+)-(\w+)/.(firstname, 'bar') :: name, domain))) do p [firstname, name, domain] # => ["foo", "foo-bar", "example.com"] end end
with Matz • 2016/09/25: Published Prototype (1) • 2017/09/20: RubyKaigi 2017 - "Pattern Matching in Ruby" by @yotii23 • 2017/09/20: Published Prototype (2) • 2018/06/04: "Toward Ruby 3, there is no progress on pattern matching..." by @_ko1 • 2018/07/15: Feature #14912: Introduce pattern matching syntax
{addresses: ['foo-bar@example.com', 'baz-bar@example.com']} => {addresses: [*head, EMail(name && /(\w+)-(\w+)/(firstname, “bar”), domain)]} p name #=> "baz-bar" p firstname #=> "baz" p domain #=> "example.com" end
C def deconstruct [0, 1, [2, 1], x: 3, y: 4, z: 5] end end case C.new => =(0, *, =(b && Integer, _), x: 3, y:, **z) p b #=> 2 p y #=> 4 p z #=> {z: 5} end
introduces the following changes to Ruby • Syntax • case/in • Namespace • NoMatchingPatternError • deconstruct, deconstruct_keys • We should be careful about their effects
NoMatchingPatternError • If NoMatchingPatternError has already been used, it will not work • deconstruct, deconstruct_keys • No effect will result if pattern matching is not used • Still, it is preferable to choose a name that is not currently used
in statically typed functional programming languages • Ruby is a dynamically typed object oriented language • It is necessary to combine these essences efficiently • Powerful Array, Hash support • Encourage duck typing
are very important data structures for Ruby • e.g. multiple assignment, keyword arguments • Provide array patterns, hash patterns • Brackets and braces are optional • id: : syntactic sugar for id: id
array pattern is an exact match and a Hash pattern is a subset match • Because these designations fit the major use cases Exact match Subset match Array pattern case [0, 1] in [a] :unreachable end case [0, 1] in [a, *] :reachable end Hash pattern case {a: 0, b: 1} in {a: 0, **rest} if rest.empty? :unreachable end case {a: 0, b: 1} in {a: 0} :reachable end
a subset match, it matches any hash object match_with_empty_hash_pattern({a: 0, b: 1}) #=> :matched match_with_empty_hash_pattern({a: 0}) #=> :matched match_with_empty_hash_pattern({}) #=> :matched match_with_empty_hash_pattern({a: 0, b: 1}) #=> NoMatchingPatternError match_with_empty_hash_pattern({a: 0}) #=> NoMatchingPatternError match_with_empty_hash_pattern({}) #=> :matched • However, a user might expect {} to match only an empty hash object • Matz selects the latter
to write code to check that the object is an instance of a specific class • Provide succinct syntax for duck typing case time in year:, month: ... in Time(year:, month:) ... end
to write code to check that the object is an instance of a specific class • Do not provide a way to match against arguments # Syntax NG def m(a, b) in Integer, String ... end
case/when expressions • Do not create a new scope • You can see the value of a variable even if it failed to match • It is obviously strange, so this behavior is going to be changed • What should we do? • Return nil • Raise NameError case [0, 1] in x, y unless y == 1 :unreachable in x, z :reachable end p y #=> 1
we write such a hash pattern? => ? • How do we write an as pattern? • Should we introduce & (and pattern) instead? • It seems an and pattern is too powerful to be used instead of an as pattern in Integer & i in {'key' => value}