petitviolet_blog

@petitviolet blog

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 

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