blog.petitviolet.net

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 is helpful to use with Pattern Match in Ruby.

What is Pattern Match?

This deck is excellent.

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.