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.new
とclass_eval
で文字列としてメソッドやらattr_readerやらにねじ込んでいる。
地味に大変だったのはクラス名を決めるところ。
self.class.name
でやるとModule
となってしまう等うまく取れなかったので、caller
で取得できるbacktraceから呼び出し元のプログラムを文字列として扱い、正規表現でクラス名を取り出すという野蛮なことをしている。
irbやpryだと死んでしまうのでrescueしてごまかしている。
map
してreject(&:nil?)
とかやるのループがもったいないからScalaでいうところのcollect
とPartialFunction
がほしい。あるいは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
すごい、すごいぞパターンマッチ!