c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

(草稿) Dart風の、constructor引数を自動的にinstance変数に代入するアレをRubyで

追記: 20130923
当記事で挙げたbugは潰した。
Dart風のautomatic field initializationをRubyhttp://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: