$30 off During Our Annual Pro Sale. View Details »

[RubyConf2019]Pattern matching - New feature in Ruby 2.7

k_tsj
November 18, 2019

[RubyConf2019]Pattern matching - New feature in Ruby 2.7

k_tsj

November 18, 2019
Tweet

More Decks by k_tsj

Other Decks in Technology

Transcript

  1. PATTERN MATCHING
    NEW FEATURE IN RUBY 2.7
    Kazuki Tsujimoto
    Nomura Research Institute, LTD.

    View Slide

  2. SELF INTRODUCTION
    • Twitter: @k_tsj
    • GitHub: k-tsj
    • CRuby committer
    • Proposer of pattern matching
    • power_assert gem author

    View Slide

  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

    View Slide

  4. AGENDA
    • What is pattern matching?
    • Specification
    • Design

    View Slide

  5. WHAT IS PATTERN
    MATCHING?

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  11. SPECIFICATION

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  15. PATTERN
    • Value pattern
    • Variable pattern
    • Alternative pattern
    • As pattern
    • Array pattern
    • Hash pattern

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  21. ALTERNATIVE PATTERN
    • An alternative pattern matches if any of the patterns
    match
    pat: pat | pat | ...
    case 0
    in 0 | 1 | 2
    :reachable
    end

    View Slide

  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

    View Slide

  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

    View Slide

  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(...)

    View Slide

  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]

    View Slide

  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]

    View Slide

  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

    View Slide

  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(...)

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  33. DESIGN

    View Slide

  34. POLICY
    • Keep compatibility
    • Be Ruby-ish

    View Slide

  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

    View Slide

  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)

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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, …

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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(?)

    View Slide