petitviolet_blog

@petitviolet blog

RstructuralにOptionとEitherを追加した

前にRstructuralというGemを作ったという記事を書いた。

それの続編としてOptionEitherを実装した、というだけの話。

Option

Scalaやってる人にはOptionHaskellとかならMaybeで伝わるはず。 「要素があるかも知れないし無いかも知れない」を表現するための型で、Rubyだとすぐにnilが出てきてあらゆるオブジェクトがnilの可能性もあり大変なのでもう少し安全に扱いたいなということで実装した。 しかしそうなるとすべての要素をOptionにする必要があり大変だが、名前の通りオプショナルな値をOptionで包んでおくと便利に扱えるのではないかなと。

Option.ofがファクトリになっていて、mapとかflat_mapが生えている。

x = Option.of(100).flat_map do |v| 
  Option.of(v * 2)
end 
#=> Option::Some(value: 200)
x.get_or_else { 100 }
#=> 200

get_or_elseの引数はデフォルト値でも良いけどブロックを渡せるようにもなっていてOption::Someの時には無駄に評価されないようになっていたりして便利。 Scalaのcall-by-nameの仕組みは便利だったなほんと。

とはいえnilを安全に扱うための言語的なサポートが無いので、Optionがあっても気を付けないと何も変わらないので面倒だしあんまり使わない気もする。

Either

値がLeftかRightのどちらかであることを表現する型。 Rubyだと失敗がnilか例外になることが多いような気がしていてもう少し明示的に扱いたいと思ったので実装した。 Either.try(&block)がファクトリになっていて例外が起きたらEither::Leftになる。

Either.try { 100 } #=> Either::Right(value: 100)
Either.try { raise "this is error" } #=> Either::Left(value: this is error)

明示的にLeftかRightかを指定してnewしたければEither.leftEither.rightが生えている。

Either.right(100)
    .flat_map { |v| Either.left(v * 2) } #=> Either::Left(value: 200)
    .right_or_else { 0 } #=> 0

Either.left(100)
    .map_left { |v| v * 2 } #=> Either::Left(value: 200)
    .left_or_else { 0 } #=> 200

やはりmapflat_mapが生えていて、さらにright-biasedになっていて便利! さらにパターンマッチと組み合わせるとこういう雰囲気

case Either.try { do_something() }
in Either::Right[value]
    value * 2
in Either::Left[error]
    logger.error("error: #{error}")
    0
end

結果の成功失敗をラップする型があるとわりと便利かなと思うこともあり、Optionよりは使い勝手良さそうな気もする。

感想

こういうのがあるとなるべくifとかtryを使わずにパターンマッチだけで実装できて気持ちが良い。

リッチなデータ構造を扱うためのGemを作ってみた

以前、StructをRubyで実装するというのをやった。

じゃあ次は、ということでEnumとAlgebraic Data Type(ADT)に相当するようなものが欲しいな〜と思ってGemとして作ってみた。

EnumRuby言語標準としては用意されていないが、RailsにはActiveRecord::Enumというものがあり、カラムをenum的に管理しつつ便利メソッドを山程生やしてくれるものがあるが、そういうのじゃなくてデータ構造としてenum欲しいんだよねという気持ち。 そうなるといわゆるADTみたいなやつ、Scalaでいうところのsealed trait + case class(object)のあれがわりと好きなので欲しくなる。

といっても静的にパターンマッチの網羅性チェックとか出来ているわけでもないしどれくらい嬉しいかは不明。 EnumEnumとして、ADTをADTとして明示できるようにしたかっただけかも知れない。

使い方

READMEに書いてあるがここにも書く。

まずrequireしておく。

require 'rstructural'

Struct

まずStruct。名前をずらすためにRstructとしてある。 これは前回の記事のやつ。

module StructSample
  Value = Rstruct.new(:value)

  puts Value.name #=> 'StructSample::Value'
  puts value = Value.new(100) #=> 'StructSample::Value(value: 100)
  puts value == Value.new(100) #=> true
  puts value.value == 100 #=> true

  Point = Rstruct.new(:x, :y) do
    def message
      "Point: [#{x}, #{y}]"
    end
  end
  p = Point.new(1, 2)
  puts p #=> StructSample::Point(x: 1, y: 2)
  puts p.message #=> Point: [1, 2]

  case Point.new(1, 2)
    in Point[x, 2]# pattern match (Ruby 2.7~)
    puts "here! x: #{x}" #=> here! x: 1
  else
    raise
  end
end 

0個以上の属性を持つデータ構造を定義できる。 0個の属性を許す、という点でRuby標準のStructとは異なる、はず。

EOF = Rstruct.new
puts EOF #=> EOF
puts EOF.new #=> EOF
puts EOF.new == EOF.new #=> true

to_s(inspect)した結果が同じなので分かりづらいがnewするとインスタンスにはなっている。

Enum

例としてHTTPステータスコードenumとして定義してみる。 extend Enumでこれはenumですよという表明になり、enumメソッドが生える。

module EnumSample
  module Status
    extend Enum

    OK = enum 200
    NotFound = enum 404
    InternalServerError = enum 500 do
      def message
        "Something wrong"
      end
    end
  end

  puts Status::OK #=> EnumSample::Status::OK(value: 200)
  puts Status::OK.value #=> 200
  puts Status::InternalServerError.message  #=> Something wrong

  case Status.of(404)
  in Status::NotFound
    puts "NotFound!!!" #=> NotFound!!!
  else
    raise
  end
end

enum <Value>という雰囲気で列挙するとクラス変数としてenumを保持しているのでStatus.of(value)で取れる、というところが少し便利。 見つからなかったらnil

一応、値の重複排除をやっていて

module Status
  extend Enum
  OK = enum 1
  NG = enum 2
  KO = enum 1
end

みたいに同じ値でenumを定義しようとすると、

Traceback (most recent call last):
        3: from sample.rb:36:in `<main>'
        2: from sample.rb:59:in `<module:EnumSample>'
        1: from sample.rb:63:in `<module:Status>'
/path/to/enum.rb:12:in `enum': Enum '1' already defined in EnumSample::Status::OK (ArgumentError)

という感じのエラーがraiseされて安心!

ADT

続いては代数的データ構造。 extend ADTでこれはADTですよという表明になり、constdataメソッドが生える。 constは属性を持たない単一オブジェクトで、dataは1つ以上のフィールドを持つデータ型。 Scalaでいうところのobjectcase classの使い分け。

いい例が思えないが点と円と長方形を表現してみる。

module AdtSample
  module Shape
    extend ADT

    Point = const
    Circle = data :radius do
      def scale(i)
        Circle.new(radius * i)
      end
      def area
        3.14 * radius * radius
      end
    end
    Rectangle = data :width, :height do |mod|
      def area
        width * height
      end
    end

  end

  puts Shape::Point #=> AdtSample::Shape::Point

  puts Shape::Rectangle.new(3, 4) #=> AdtSample::Shape::Rectangle(width: 3, height: 4)
  puts Shape::Rectangle.new(3, 4).area #=> 12
  puts Shape::Circle.new(5).scale(2).area #=> 314.0

  case Shape::Rectangle.new(1, 2)
  in Shape::Rectangle[1, Integer => j] if j % 2 == 0
    puts "here! rectangle 1, #{j}" #=> here! rectangle 1, 2
  else
    raise
  end
end

なんかそれっぽい感じで定義出来ていそう。 ただ、ADTはADTとして共通のインタフェース(Enum#ofのような)ものが特に無いためStructでいいじゃん、というのはその通り。

じゃあ共通のインタフェースを持てるようにしよう、ということでやってみた。

module Option
  extend ADT
  None = const
  Some = data :value

  interface do
    def map(&f)
      case self
      in None
        None
      in Some[value]
        Some.new(f.call(value))
      end
    end
  end
end

puts Option::None.map { |i| i * 2 } #=> None
puts Option::Some.new(10).map { |i| i * 2 } #=> Option::Some(value: 20)

interface(&block)で共通のメソッドを定義出来るようにした。
とはいえ実装ではパターンマッチするなりが必要になるので別々に実装するでも十分な気もするが、定義が分散しないというメリットもあるので使えなくはなさそう。 ここまで来るとようやく単なるStructを使うだけではないメリットが...あるかもしれない。

実装について

パターンマッチを使いたかったのでRuby2.7.0で実装している。 Gemのrequirementもそう。

前の記事で実装したRstructural::StructEnumADT両方で使いまわしているので新規でなにかテクニックを用いたかというとそうでもない。

まず、EnumやADTの各クラスにおいて独自のメソッドを生やしたいというのは容易に想像がつくのでblockを渡してメソッド定義できるようにした。 そのためにもまたblockが与えられていたらRstructが生成したClassオブジェクトに対してclass_evalするということをしている。 共通インタフェースをinterface(&block)で定義できるようにしてあるやつも結局class_evalしているだけではある。

またenumdataメソッド経由でRstruct.newを実行しているためcallerでクラス名を決定するところが壊れるので__callerキーワードで雑にバイパス出来るようにしている。

ADTについて実装をはりつけてみるとこんな感じ。

module Rstructural::ADT
  def self.extended(klass)
    klass.class_variable_set(:@@adt_types, [])
  end

  def const(value = nil, &block)
    if value
      Rstructural::Struct.new(:value, __caller: caller, &block).new(value)
    else
      Rstructural::Struct.new(__caller: caller, &block).new
    end.tap do |k|
      self.class_variable_get(:@@adt_types) << k
      def k.name
        self.class.name
      end
    end
  end

  def data(*fields, &block)
    Rstructural::Struct.new(*fields, __caller: caller, &block).tap do |k| 
      self.class_variable_get(:@@adt_types) << k 
    end
  end

  def interface(&block)
    self.class_variable_get(:@@adt_types).each do |t|
      case t
      in Class
        t.class_eval(&block)
      else
        t.class.class_eval(&block)
      end
    end
  end
end

constは値を持たせることも出来るし、必須ではない。内部ではそれぞれごとにClassオブジェクトを生成しているので==で別のconst同士を比較するとちゃんとfalseになるので実質singleton object的に扱える。

dataは任意の個数のフィールドを持つタプルを生成する。こちらは同じ型でかつ全フィールドが==trueなら、==trueになるという実装をRstructural::Structでしてある。

感想

Rubyには静的型チェックがなくデータコンテナとしてのClassを定義するのも面倒だしHashがインタフェースになりがちだけど少しでもインタフェースに対して制約をつけるためにはこういうのがあると便利かなとは改めて思った。

RubyのStructをRubyで実装する

RubyのStructはこういうやつ。

$ irb
irb(main):001:0> Point = Struct.new(:x, :y)
irb(main):002:0> Point.new(1, 2)
=> #<struct Point x=1, y=2>
irb(main):003:0> Point.class
=> Class 

Structという名前だけあり、データを持たせておくのに便利なクラスを作ることができる。 実装はここ。 ruby/struct.c

ぼくCよめないのでRubyで実装してみよう!ということでやってみた。

やってみた実装はこんな感じ。

module Rstruct
  def self.new(*attributes)
    begin
      names = caller.map do |stack|
        # ".../hoge.rb:7:in `<module:Hoge>'"
        if (m = stack.match(/\A.+in `<(module|class):(.+)>.+/))
          m[2]
        end
      end.reject(&:nil?)
      file_name, line_num = caller[0].split(':')
      line_executed = File.readlines(file_name)[line_num.to_i - 1]
      names << line_executed.match(/\A\s*(\S+)\s*=/)[1] # "  Point = Rstruct.new(:x, :y)\n"
      class_name = names.join('::')
    rescue
      class_name = 'Rstruct'
    end
    Class.new.tap do |k|
      k.class_eval <<~RUBY
        def initialize(#{attributes.join(", ")})
          #{attributes.map { |attr| "@#{attr} = #{attr}" }.join("\n")}
        end

        #{attributes.map { |attr| "attr_reader(:#{attr})" }.join("\n")}

        def inspect
          if #{attributes.empty?}
            "#{class_name}"
          else
            __attrs = Array[#{attributes.map { |attr| "'#{attr}: ' + @#{attr}.to_s" }.join(", ")}].join(", ")
            "#{class_name}(" + __attrs + ")"
          end
        end

        alias :to_s :inspect

        def deconstruct
          [#{attributes.map { |attr| "@#{attr}" }.join(", ")}]
        end
      RUBY
    end
  end
end

なかなか辛いコードになったが、Classを作るためのClassを作るのでメタプロっぽくなるのは仕方がないよね。
Rstruct.newの引数にはattributesとしてArray[Symbol]を受け取ることを想定していて、そのattributesを頑張ってClass.newclass_evalで文字列としてメソッドやらattr_readerやらにねじ込んでいる。

地味に大変だったのはクラス名を決めるところ。
self.class.nameでやるとModuleとなってしまう等うまく取れなかったので、callerで取得できるbacktraceから呼び出し元のプログラムを文字列として扱い、正規表現でクラス名を取り出すという野蛮なことをしている。
irbやpryだと死んでしまうのでrescueしてごまかしている。 mapしてreject(&:nil?)とかやるのループがもったいないからScalaでいうところのcollectPartialFunctionがほしい。あるいはflat_mapで潰してほしい。。

特にデバッグが面倒くさかったがなんとか動くようになり、こんな感じになった。

Value = Rstruct.new(:v)
v =  Value.new(100)
puts v
#=> Value(v: 100)
puts v.v
#=> 100

module Parent
  Point = Rstruct.new(:x, :y)
end

p = Parent::Point.new(10, 20)
puts p
#=> Parent::Point(x: 10, y: 20)
puts p.y
#=> 20

deconstructを実装していることによってパターンマッチが動く!

case Parent::Point.new(10, 20)
in Parent::Point[i, y] if i % 2 == 0
  puts "y: #{y}"
else
  puts "not matched"
end
#=> y: 20 

すごい、すごいぞパターンマッチ!