追記: 20130923
当記事で挙げたbugは潰した。
Dart風のautomatic field initializationをRubyで http://c4se.hatenablog.com/entry/2013/09/23/075129
bug bug。bugだらけ。特に、引数にdefault値を持たせられる形式に就いては、一部しか対処出来てゐない。rest引数も、Ruby 1.9以降の、rest引数の後ろにもrequired引数を置ける形式には、未だ手を付けてゐない。
AspectR (AOP, aspect oriented programming) を持ち出してるけど、大仰な気はする。
rest引数は、わりと解決出来る。
default引数 (optional引数) のdefault値を取得出来なくて、辛い。Ripperでparseしてるけど、S式の種類が多過ぎて死ぬ。もっと素直な方法は無いのか。
実装
Gemfile
source 'https://rubygems.org' gem 'aspectr'
実装
# coding=utf-8 # license: Public Domain require 'bundler' Bundler.require require 'ripper' $DEBUG = true # {{{ util # Rubyで、D言語風にassertionを直書きする簡易unit test - c4se記:さっちゃんですよ☆ # http://c4se.hatenablog.com/entry/2013/08/15/022137 # # @param test_name [String] def unittest test_name, &proc if $DEBUG include Test::Unit::Assertions proc.call puts "#{test_name} ok." end end if $DEBUG require 'test/unit/assertions' end # }}} # https://twitter.com/ne_sachirou/status/367675729367924738 # https://twitter.com/ne_sachirou/status/367676091294425088 # Dart like auto instance variable setter. module AutoInstanceArgs class AutoIaAspect < AspectR::Aspect # @params klass [Class] # @params names [Symbol[]] def initialize klass, names = [] detect_params klass @names = names @names = @params.map{|param| param[1] } if @names.length == 0 end def pre_initialize method, object, exitstatus, *args @names.each do |name| index = @params.find_index{|param| param[1].to_sym == name.to_sym } param = @params[index] case param[0] when :req arg = args[index] when :opt arg = args[index] || object.instance_eval(param[2]) when :rest arg = args[index..-1] arg.pop if @params[index + 1] when :key arg = args.last[param[1]] when :block next end object.instance_variable_set(:"@#{name}", arg) if arg end end private def detect_params klass initialize_method = klass.instance_method :initialize @params = initialize_method.parameters analyze_opt_params initialize_method end def analyze_opt_params initialize_method initialize_line = File.open(initialize_method.source_location[0], 'r'). read. each_line. to_a[initialize_method.source_location[1] - 1]. strip params_sexp = find_params_from_sexp(Ripper.sexp initialize_line + "\nend"). select{|sexp| sexp != nil }[1] params_sexp = [params_sexp] if params_sexp[0].class == Symbol params_sexp.zip @params do |sexp, param| next if param[0] != :opt param << sexp[1][1] end end def find_params_from_sexp sexp return nil if sexp.methods.none?{|m| m == :each } sexp.each do |sexp| v = find_params_from_sexp sexp break v if v != nil && v[0] == :params end end end module AutoIa # @params names [String[]] def auto_instance_args *names aspect = AutoIaAspect.new self, names aspect.wrap self, :pre_initialize, nil, :initialize end end end class Class include AutoInstanceArgs::AutoIa end unittest 'AutoInstanceArgs can set instance variables' do class CReq attr_accessor :a, :b, :c def initialize a, b, c end auto_instance_args :a, :c end c_req = CReq.new 4, 5, 6 assert_equal 4, c_req.a assert_nil c_req.b assert_equal 6, c_req.c end unittest 'AutoInstanceArgs can worl for optional params' do class COpt attr_reader :a, :b def initialize a = 2, b = 3 end auto_instance_args end c_opt = COpt.new 4 assert_equal 4, c_opt.a assert_equal 3, c_opt.b c_opt = COpt.new assert_equal 2, c_opt.a assert_equal 3, c_opt.b end unittest 'AutoInstanceArgs can work for rest param' do class CRest attr_reader :r def initialize *r end auto_instance_args end c_rest = CRest.new 2, 3 assert_equal [2, 3], c_rest.r c_rest = CRest.new assert_equal [], c_rest.r end # unittest 'AutoInstanceArgs can work on keyword params' do # class C4 # attr_reader :p1, :p2, :opt # # def initialize p1: 'p1d', p2: 'p2d', **opt # end # auto_instance_args # end # # c4 = C4.new p1: 'p1d', p2: 'p2d' # assert_equal 'p1', c4.p1 # assert_equal 'p2', c4.p2 # # c4 = C4.new p2: 'p2', p3: 'p3' # assert_equal 'p1d', c4.p1 # assert_equal 'p2', c4.p2 # assert_equal({ p3: 'p3' }, c4.opt) # end unittest 'AutoInstanceArgs can work for hash param' do class C5 attr_reader :h def initialize h = {} end auto_instance_args end c5 = C5.new p1: 'p1', p2: 'p2' assert_equal({ p1: 'p1', p2: 'p2' }, c5.h) end unittest 'AutoInstanceArgs can work with mix features.' do class CMix attr_reader :a, :b, :c, :d, :e, :p1, :p2 def initialize a, b, c = 9, d = 8, *e, p1: 'p1d', p2: 'p2d', &f assert_equal 3, @b @b = 'b' end auto_instance_args end c_mix = CMix.new(2, 3, 7, 6, 'e1', 'e2', p2: 'p2'){|v| "block #{v}" } assert_equal 2, c_mix.a assert_equal 'b', c_mix.b assert_equal 7, c_mix.c assert_equal 6, c_mix.d assert_equal ['e1', 'e2'], c_mix.e # assert_equal 'p1d', c_mix.p1 # assert_equal 'p2', c_mix.p2 end # vim:set et sw=2 sts=2 ff=unix foldmethod=marker: