petitviolet blog

    Empower Pattern Matching in Ruby

    2020-04-10

    RubyFunctional Programming

    Ruby2.7 provides Pattern Matching feature as an experimental one.

    https://www.ruby-lang.org/en/news/2019/12/25/ruby-2-7-0-released/

    I used to do pattern matching in Scala, so I'm excited to be able to use it in Ruby!

    tl;dr

    Rstructural Gem what I've been developing lets us coding more efficiently with pattern matching in Ruby.

    What is Pattern Match?

    This deck desribes it precisely.

    It's enough to know what is pattern match in Ruby, however, let me give some examples as followings,

    case [1, 2, [3, [4, 5]]]
    in [a, b, [*c]]
        puts "a+b: #{a+b}, rest:#{c}"
    end
    # => a+b: 3, rest:[3, [4, 5]]
    
    case {a: 1, b: 2, c: {d: 3, e: [4, 5]}}
      in {a:, c: {d:, e: [4, x]}}
        puts "a: #{a}, c: #{c}, d: #{d}, x: #{x}"
    end
    # => a: 1, c: [3, [4, 5]], d: 3, x: 5
    
    module HttpStatus
      OK = 200
      NotFound = 404
      InternalServerError = 500
    end
    
    case 500
    in HttpStatus::OK
      puts "OK!"
    in HttpStatus::NotFound | HttpStatus::InternalServerError
      puts "NG!"
    in unknown
      puts "Unknown: #{unknown}"
    end
    # => NG!
    

    Pattern Match brings a capability of intuitive expression to represent what code does.

    Empower Pattern Matching

    As you know, pattern matching is excellent. However, I'm eager to write Ruby codes more functional way as I do with Scala. Case class in Scala is to represent algebraic data types, and absolutely useful to write intuitive and expressive codes, especially when combining with pattern matching.

    Thus, I import a kind of algebraic data type into the Ruby world. It is Rstructural gem.
    GitHub: https://github.com/petitviolet/rstructural

    Rstructural provides Enum and Algebraic Data Type(ADT), and also applied types(Option, Either).

    Enum

    The simplest ADT is Enum, I think. Unfortunately, Ruby does not have enum as default. On the other hand, Rails has ActiveRecord::Enum, but it's for ActiveRecord as its name says so that the Enum is not for define data structure in pure Ruby world.

    Rstructural provides Enum to define enumerated values.
    It looks like:

    require 'rstructural'
    
    module HttpStatus
      extend Enum
      OK = enum 200
      NotFound = enum 404
      InternalServerError = enum 500
    end
    
    case HttpStatus.of(500) # factory
    in HttpStatus::OK
      puts "OK!" # => OK!
    in HttpStatus::NotFound | HttpStatus::InternalServerError
      puts "NG!"
    in unknown
      puts "Unknown: #{unknown}"
    end
    
    HttpStatus.of(500).is_a?(HttpStatus)
    # => true
    

    Each enum value has a constant, and we can use them as a set of constants, HttpStatus in this example. Also, extending Enum injects a factory method of into the module. It brings us a sort of type-sensitive programming style. Another merit of using Enum is eliminating duplicated values, as:

    module Status 
      extend Enum
      OK = enum 1
      NG = enum 2
      Unknown = enum 2
    end
    # => ArgumentError (Enum '2' already defined in Struct)
    

    This feature protects us from defining wrong constants.

    ADT

    Rstructural::ADT brings characteristics of algebraic data type into Ruby module. An example to define ADTs:

    module Shape
      extend ADT
      Point = const
      Circle = data :radius
      Rectangle = data :width, :height do
        def square?
          width == height
        end
      end
      
      interface do 
        def area
          case self
          in Point
            0
          in Circle[radius]
            3.14 * radius * radius
          in Rectangle[w, h]
            w * h
          end 
        end
      end
    end
    
    rectangle = Shape::Rectangle.new(3, 4)
    rectangle.area
    # => 12
    rectangle.square?
    # => false
    rectangle.is_a?(Shape)
    # => true
    

    An ADT injected module has const and data to define data types.

    • const is to define a constant and look like a singleton object.
      • is similar to object in Scala
    • data :field_a, :field_b, ... is to define a type that holds given fields
      • is similar to case class in Scala

    A combination of const and data in ADT module is similar to the one of object, case class, and sealed trait in Scala. As the example shows, it allows to define instance methods at the declaration of types and also give a block to interface.

    Using ADT brings capabilities to manage a set of data types intuitively and type sensitive way as well as Enum.

    Additionally, Rstructural gem provides Option and Either based on ADT. Please have a look if you are interested in.

    Wrapping Up

    I believe this kind of type abstraction would help you if you are familiar with functional programming way whether or not programming language provides static analysis.