[RubyConf2019]Pattern matching - New feature in Ruby 2.7

303dd57f37d64288bb4f0336332a8882?s=47 k_tsj
November 18, 2019

[RubyConf2019]Pattern matching - New feature in Ruby 2.7

303dd57f37d64288bb4f0336332a8882?s=128

k_tsj

November 18, 2019
Tweet

Transcript

  1. 2.

    SELF INTRODUCTION • Twitter: @k_tsj • GitHub: k-tsj • CRuby

    committer • Proposer of pattern matching • power_assert gem author
  2. 3.

    TL;DR • Pattern matching has already been released in 2.7.0-

    preview1 as experimental feature • The specifications remain under discussion • Please try it and give us feedback
  3. 6.

    WHAT IS PATTERN MATCHING? • Definition • "Pattern matching consists

    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 Rubyists • case/when + multiple assignment
  4. 7.

    WHAT IS PATTERN MATCHING? • We extend case expressions for

    pattern matching (case/in) case [0, [1, 2, 3]] in [a, [b, *c]] p a #=> 0 p b #=> 1 p c #=> [2, 3] end
  5. 8.

    WHAT IS PATTERN MATCHING? • Unlike multiple assignment, pattern matching

    also checks the object structure case [0, [1, 2, 3]] in [a] :unreachable in [0, [a, 2, b]] p a #=> 1 p b #=> 3 end
  6. 9.

    WHAT IS PATTERN MATCHING? • Pattern matching also supports Hash

    case {a: 0, b: 1} in {a: 0, x: 1} :unreachable in {a: 0, b: var} p var #=> 1 end
  7. 10.

    EXAMPLE • This is an example of a situation that

    may arise you handle a JSON data { "name": "Alice", "age": 30, "children": [ { "name": "Bob", "age": 2 } ] } case JSON.parse(json, symbolize_names: true) in {name: "Alice", children: [{name: "Bob", age: age}]} p age # => 2 end
  8. 12.

    SYNTAX • The patterns are run in sequence until the

    first one finds a match • If no pattern matches, the else clause is executed • If no pattern matches emerge and there is no else clause, the NoMatchingPatternError exception is raised case expr in pattern [if|unless condition] ... in pattern [if|unless condition] ... else ... end
  9. 13.

    SYNTAX • The pattern may also be followed by a

    guard • The guard expression is evaluated if the preceding pattern matches case [0, 1] in [a, b] unless a == b :reachable end
  10. 14.

    SYNTAX • As of 2.7.0-preview2, single line pattern matching has

    been introduced • If the pattern matches, it returns true • Otherwise, it returns false # Syntax expr in pattern # e.g. if [0, 1] in [0, a] p a #=> 1 end
  11. 15.

    PATTERN • Value pattern • Variable pattern • Alternative pattern

    • As pattern • Array pattern • Hash pattern
  12. 16.

    VALUE PATTERN • A value pattern matches an object such

    that pattern === object pat: literal | Constant case 0 in 0 in -1..1 in Integer end
  13. 17.

    VARIABLE PATTERN • A variable pattern matches any value and

    binds the variable name to that value pat: var case 0 in a p a #=> 0 end
  14. 18.

    VARIABLE PATTERN • You can use _ to drop values

    • It is same as the idiomatic way of multiple assignment case [0, 1] in [_, _] :reachable end
  15. 19.

    VARIABLE PATTERN • A variable pattern always binds the variable

    even if you have outer variable with same name a = 0 case 1 in a p a #=> 1 end
  16. 20.

    VARIABLE PATTERN • When you want to pattern match against

    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
  17. 21.

    ALTERNATIVE PATTERN • An alternative pattern matches if any of

    the patterns match pat: pat | pat | ... case 0 in 0 | 1 | 2 :reachable end
  18. 22.

    AS PATTERN • An as pattern binds the variable to

    the value if the pat matches pat: pat => var case 0 in Integer => a a #=> 0 end
  19. 23.

    AS PATTERN • It is becomes increasingly more convenient as

    the object becomes more complex case [0, [1, 2]] in [0, [1, _] => a] p a #=> [1, 2] end
  20. 24.

    ARRAY PATTERN • Despite its name, the pattern is not

    solely for array objects • Steps to matching: 1.The pattern and the object are compared by Constant === object 2.If it returns true, check if the object has a #deconstruct method that returns Array 3.If the object has a #deconstruct method, its return value is used to perform sub-pattern matching pat: Constant(pat, ..., *var, pat, ...) | Constant[pat, ..., *var, pat, ...] | [pat, ..., *var, pat, ...] # Syntactic sugar for BasicObject(...)
  21. 25.

    ARRAY PATTERN • An array pattern with Array class Array

    def deconstruct self end end case [0, 1, 2] in Array(0, *a, 2) in Object[0, *a, 2] in [0, *a, 2] in 0, *a, 2 # You can omit brackets end p a #=> [1]
  22. 26.

    ARRAY PATTERN • An array pattern with Struct class Struct

    alias deconstruct to_a end Color = Struct.new(:r, :g, :b) p Color[0, 10, 20].deconstruct #=> [0, 10, 20]
  23. 27.

    ARRAY PATTERN • An array pattern with Struct case color

    in Color[0, 0, 0] puts "Black" in Color[255, 0, 0] puts "Red" in Color[r, g ,b] puts "#{r}, #{g}, #{b}" end
  24. 28.

    HASH PATTERN • Hash patterns are not solely for hash

    objects • Steps to matching: 1.The pattern and the object are compared by Constant === object 2.If it returns true, check if the object has a #deconstruct_keys method that returns Hash 3.If the object has a #deconstruct_keys method, its return value is used to perform sub-pattern matching pat: Constant(id: pat, id:, ..., **var) | Constant[id: pat, id:, ..., **var] | {id:, id: pat, **var} # Syntactic sugar for BasicObject(...)
  25. 29.

    HASH PATTERN • A hash pattern with Hash class Hash

    def deconstruct_keys(keys) self end end case {a: 0, b: 1} in Hash(a: a, b: 1) in Object[a: a] in {a: a, **rest} p rest #=> {b: 1} in {a: a, **nil} :unreachable end
  26. 30.

    HASH PATTERN • You can omit braces too • a:

    is syntactic sugar for a: a case {a: 0, b: 1} in a:, b: p a #=> 0 p b #=> 1 end
  27. 31.

    HASH PATTERN • keys argument provides hint information for efficient

    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
  28. 32.

    HASH PATTERN • keys refers to an array containing keys

    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
  29. 33.
  30. 35.

    KEEP COMPATIBILITY • Compatibility is very important • Pattern matching

    introduces the following changes to Ruby • Syntax • case/in • Namespace • NoMatchingPatternError • deconstruct, deconstruct_keys • We should be careful about their effects
  31. 36.

    KEEP COMPATIBILITY: SYNTAX • Do not define new reserved words

    • If we introduce new match expressions, it breaks the following code • We have no choice but to extend the case expression match = foo.match?(bar)
  32. 37.

    KEEP COMPATIBILITY: SYNTAX • What should we use instead of

    the when keyword? • It should be suitable for pattern matching in terms of vocabulary • It must not be placed at the beginning of the expression
  33. 38.

    KEEP COMPATIBILITY: SYNTAX • Ruby has the for expression •

    Legacy loop syntax • It contains an in keyword that meets the requirements for i in [0, 1, 2] p i end
  34. 39.

    KEEP COMPATIBILITY: NAMESPACE • We cannot maintain 100% compatibility •

    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
  35. 40.

    KEEP COMPATIBILITY: NAMESPACE • Investigate code of all gems by

    using gem-codesearch • NoMatchingPatternError: 0 gems • deconstruct: 8 gems • deconstruct_keys: 0 gems • We need better names, especially for deconstruct_keys • deconstruct_by, deconstruct_by_keys, deconstruct_as_hash, …
  36. 41.

    BE RUBY-ISH • Pattern matching is a widely used feature

    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
  37. 42.

    BE RUBY-ISH: POWERFUL ARRAY, HASH SUPPORT • Array and Hash

    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
  38. 43.

    BE RUBY-ISH: ENCOURAGE DUCK TYPING • Should not allow users

    to write code to check that the object is an instance of a specific class • Provide concise syntax for duck typing case time in year:, month: ... in Time(year:, month:) ... end
  39. 44.

    BE RUBY-ISH: ENCOURAGE DUCK TYPING • Should not allow users

    to write code to check that the object is an instance of a specific class • Do not provide a way to make a match with arguments # Syntax Error def m(a, b) in Integer, String ... end
  40. 45.

    FUTURE WORKS • Write documentation • doc/syntax/pattern_matching.rdoc • Define deconstruct,

    deconstruct_keys for builtin/standard library • Few classes have deconstruct/deconstruct_keys now (Array, Hash, Struct) • Improve performance • Extend the pin operator(?) • Revise scope(?) • Allow Non-symbol keys for a hash pattern(?)