専用クラスを作ってキーワード引数の代わりにする

引数をハッシュにするより専用クラスを作って(ry - きみのハートを8ビットキャスト

class Freq
  def initialize( freq ) ; @freq = freq ; end
  def to_i               ; @freq        ; end
end

class Bit
  def initialize( bit ) ; @bit = bit    ; end
  def to_i              ; @bit          ; end
end

class Channel
  def initialize( ch )  ; @ch = ch      ; end
  def to_i              ; @ch           ; end
end

おもしろいテクニックだが、DRY原則に反してるからあかん。
せめてまとめなくちゃ。

def define_unit(methname)
  Class.new do
    define_method(:initialize) {|x| instance_variable_set("@#{methname}", x) } 
    attr_reader methname
  end
end

Freq = define_unit :to_i
Bit = define_unit :to_i
Channel = define_unit :to_i


# Numericからサクッと作れるように仕込む
class Numeric
  def hz      ; Freq.new( self )        ; end
  def bit     ; Bit.new( self )         ; end
  def channel ; Channel.new( self )     ; end
end

# 実際のWavクラスの初期化(抜粋)
class Wav
  def initialize( *args )
   args.each { |arg|
      case arg
      when Channel ; @channels    = arg.to_i
      when Freq    ; @sample_rate = arg.to_i
      when Bit     ; @sample_bit  = arg.to_i
      end
    }
  end
end

Wav.new 44100.hz,16.bit, 1.channel
# => #<Wav:0x83896ec @sample_rate=44100, @sample_bit=16, @channels=1>

クラス名と単位を管理しないといけない上、クラス名での分岐が出てるのがいやだ。
Rubyにおいて、クラスで分岐しているのはリファクタリングのサインであることが多い。
もうちょっと抽象化してみる。レッツ、DSL

Numeric#hz等は value と unit の2つのメンバを持つ構造体オブジェクトを返す。
値を格納するだけのクラスは構造体を使えばよい。

で、Object#set_instance_variable_by_unit では、インスタンス変数名→単位名のハッシュに基いてインスタンス変数をセットする。

module Unit
  Value = Struct.new :value, :unit
  def self.define(unit)
    Numeric.module_eval do
      define_method(unit) { Value.new(self, unit) }
    end
  end
end

class Object
  def set_instance_variable_by_unit(args, hash)
    unit_to_var = hash.invert
    args.each do |arg|
      instance_variable_set unit_to_var[arg.unit], arg.value
    end
  end
  private :set_instance_variable_by_unit
end

# 黒魔術はここでおしまい。
################################################################
Unit.define :hz
Unit.define :bit
Unit.define :channel

class Wav
  def initialize( *args )
    set_instance_variable_by_unit args,
      :@channels    => :channel,
      :@sample_rate => :hz,
      :@sample_bit  => :bit
  end
end

Wav.new 44100.hz,16.bit, 1.channel
# => #<Wav:0xb7dd365c @channels=1, @sample_bit=16, @sample_rate=44100>
Wav.new 1.channel, 44100.hz, 16.bit
# => #<Wav:0xb7dd23c4 @channels=1, @sample_bit=16, @sample_rate=44100>
1.hz    # => #<struct Unit::Value value=1, unit=:hz>
2.hz    # => #<struct Unit::Value value=2, unit=:hz>
1.bit   # => #<struct Unit::Value value=1, unit=:bit>

すっきりしたでしょ?

しかし、同じ単位の引数が複数ある場合は使えないなぁ…

っつーか、active-supportだとNumeric#secとかNumeric#minuteとか、Numeric#megabyteとか単位名メソッドがあるんだよなー。かぶってしまう恐れがあるかも…