DRY症候群

Rubyistの間では「DRY原則」が浸透しているので、彼らは重複や同じことの繰り返しを極端に嫌います。コードの中に繰り返しがあると、目や肌が乾燥してきて痒くなり落ち着きがなくなります。「DRY!DRY!DRY!」と叫び出す人もいます。アサヒスーパードライを飲み始める人もいます。これらの症状を総称して「DRY症候群」と言います。

Rubyの言語仕様はプログラマーがハッピーにコーディングできるよう考えられているので、RubyistをしてDRY症候群が発症することは稀ですが、日常的にDRY症候群を発症している人たちもいるようです。

以下は、DRY症候群を検査するためのテストコードです。コードを数秒間眺め、痒みが出てきたらあなたは重度のDRY症候群を患っています。

class User
  attr_reader :name, :income
  def initialize(name, income)
    @name = name
    @income = income
  end

  def real_user?
    @income > 10_000_000
  end
end

痒みが出なかった人たちに向けて説明すると、「initializeにおけるインスタンス変数への引数の引き渡しがDRYでない」、ということです。

それで大抵、彼らは以下を試し、うなだれるのです。

class User
  attr_reader :name, :income
  def initialize(@name, @income)
  end

  def real_user?
    @income > 10_000_000
  end
end

# ~> -:3: formal argument cannot be an instance variable
# ~>   def initialize(@name, @income)
# ~>                       ^
# ~> -:3: formal argument cannot be an instance variable
# ~>   def initialize(@name, @income)
# ~>                                ^

DartとCoffeeScriptの場合

グーグルが開発したプログラミング言語「Dart」では、Automatic field initializationという機能によって、以下のコードを、

class User {
  String name;
  num income;

  User(String name, num income) {
    this.name = name;
    this.income = income;
  }
}

以下のように書けるそうです。

class User {
  String name;
  num income;

  User(this.name, this.income);
}

同様に、「CoffeeScript」でも、以下を、

class User
  constructor: (name, income) ->
    @name = name
    @income = income

以下のように書けるそうです(classes, Inheritance, and Super)。

class User
  constructor: (@name, @income) ->

羨ましい。

これをもって、DartまたはCoffeeScriptへの鞍替えを検討しているRubyistも少なくないと聞きます。

さっちゃんによるRuby版AFI

さっちゃん」こと、@ne_sachirou殿がこの問題を解決すべく、Ruby版AFIの実装を公開してくれてます。

Dart風のautomatic field initializationをRubyで - c4se記:さっちゃんですよ☆

記事によれば、以下のようにinitializeの定義のあとでauto_attr_initクラスメソッドを呼べば、initializeの引数が自動で同名のインスタンス変数にセットされるそうです。

require 'auto_attr_init'

class Point
  attr_accessor :x, :y

  def initialize x, y; end
  auto_attr_init
end

p = Point.new 2, 4
assert_equal 2, p.x
assert_equal 4, p.y

自動セットする変数を限定したい場合は、auto_attr_initにシンボルで渡します。

実装は、ソースをRipperで解析して、必要な処理を施して、sorcererというツールでRubyのコードに戻す、ということをやってるそうです。スゴイです。

@merborneによるなんちゃってRuby版AFI

さっちゃんのお陰で、DRY症候群問題は無事解消しました。

したがって僕の出番はないんですが、Ruby芸人を目指す身(!)としては、もっと安直にやれる方法を模索したくなりました。

で、私、@merborneも同じような機能をもった、「AutoAttrSet」というモジュールを作ったので公開します。

対象のクラスでAutoAttrSetモジュールをextendします。そしてCoffeeScriptのように、initializeでを付けて変数を渡します。

require 'auto_attr_set'

class User
  extend AutoAttrSet

  attr_reader :name, :income
  def initialize(name, income)
    
  end
  
  def real_user?
    @income > 10_000_000
  end
end

a_user = User.new('Charlie', 300_000)
a_user.name # => "Charlie"
a_user.income # => 300000
a_user.real_user? # => false
a_user.instance_variables # => [:@name, :@income]

デフォルト値が渡せない、*&を伴う引数の後に渡せないなどの制約はありますが、このコードはシンタックスエラーにはなりません。普通の引数を間に挟むこともできます。

えっ?

…。

…バレちゃいましたか?




initializeの

これは'@'.ord # => 64ではなく、'@'.ord # => 65312なんです…。

AutoAttrSetの実装

実装です。

簡単に説明します。Class.newをオーバーライドして、そこでset_instance_variablesというメソッドを呼びます。set_instance_variablesでは、initializeをMethodオブジェクト化してMethod#parametersでその引数を取り出します。取り出した引数のうち特定のフォームを持ったものを対象として、instance_variable_setを使ってインスタンス変数をセットします。特定のフォームはname_for_instance_variable?のcandidatesに登録しておきます。ここでは、'@'または'at_'で始まる名前が登録されています。

at_の例も示しておきます。

class Point
  extend AutoAttrSet
  attr_accessor :x, :y
  def initialize(at_x, z, at_y, *rest)
    
  end
end

po = Point.new(10, 20, 30)
po.x # => 10
po.y # => 30
po.instance_variables # => [:@x, :@y]

こんなもの書いてる暇があるなら、さっちゃんのコード読んでろって話ですが。


=== Ruby関連電子書籍100円で好評発売中! ===

M’ELBORNE BOOKS

ruby_trivia ruby_parallel rack



blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS