配列の最後にアイテムを追加する

Rubyで配列の最後にアイテムを追加するときは通常、Array#<<, Array#pushを使いますよね。

list = [1,2]
list << 3 # => [1, 2, 3]
list.push(4) # => [1, 2, 3, 4]
list # => [1, 2, 3, 4]

これは元の配列を改変する破壊的代入です。しかし、元の配列を破壊せずにアイテムを追加した新たな配列を得たい、というときもあります。その場合は、Array#+を使います。

list = [1,2]
list2 = list + [3] # => [1, 2, 3]
list # => [1, 2]

でも追加要素を[]で括らなければいけないのが、ちょっとイケてないですよね。できれば、次のように関数型言語風に結合したい。

list = [1,2]
list2 = list + 3 # => [1, 2, 3]
list3 = list2 + :a # => [1, 2, 3, :a]
list # => [1, 2]

これはもちろん、Array#+を再定義することで可能になりますが、次のような方法でも実現できます。

class Object
  def method_missing(name, *arg, &blk)
    return [self] if name == :to_ary
    super
  end
end

list = [1,2]
list2 = list + 3 # => [1, 2, 3]
list3 = list2 + :a # => [1, 2, 3, :a]
list # => [1, 2]

Array#+はその引数にArrayオブジェクト以外を渡すと、そのオブジェクトのto_aryメソッドを呼び出します。ほとんどのオブジェクトはto_aryメソッドを持っていないので、結果としてObject#method_missingが呼ばれて、[self]が実行されることになり、これで配列との結合ができるようになります。

もう少し凝って、[self]の前にself.to_aを呼ぶようにすると、HashやStructのオブジェクトを配列展開して結合させることもできます。

class Object
  def method_missing(name, *arg, &blk)
    case name
    when :to_ary then self.to_a
    when :to_a   then [self]
    else super
    end
  end
end

list = [1,2]
list + {:a => 1} # => [1, 2, [:a, 1]]

Person = Struct.new(:name, :age)
charlie = Person['Charlie', 12]
list + charlie # => [1, 2, "Charlie", 12]

m = "2012June23".match(/(\d+)(\D+)(\d+)/)
list + m # => [1, 2, "2012June23", "2012", "June", "23"]

これらの方法の利点は、配列に対してどのように結合されるかの決定権を、そのオブジェクトに留保できる点にあります。オブジェクトがその決定権を行使したい場合はto_aryを定義すればいいです。

class Person
  def initialize(name, age, job)
    @name, @age, @job = name, age, job
  end

  def to_ary
    ["#@name/#@age/#@job"]
  end
end

p1 = Person.new('Charlie', 12, :programmer)
p2 = Person.new('Bob', 29, :teacher)
[] + p1 + p2 # => ["Charlie/12/programmer", "Bob/29/teacher"]

Array#+を再定義するとその決定権が奪われてしまいます。

でも、Object#method_missingを弄るなんて、明らかに筋が悪すぎますよね..

先に行きます…

配列の先頭にアイテムを追加する

配列の「先頭に」アイテムを追加した新たな配列を返したい、ってときもありますよね。普通、こうします。

list = [2,3]
list2 = [1] + list # => [1, 2, 3]
list # => [2, 3]

やっぱり、[]が、イケてない。できればこうしたい。

list = [2,3]
1 + list # => [1, 2, 3]
2.0 + list # => [2.0, 2, 3]

でも実際には、次のようなエラーが返ります。

# ~> -:14:in `+': Array can't be coerced into Fixnum (TypeError)

Fixnum#+に渡されたArrayは型変換できない、と言っています。で、Arrayに次のようなcoerceメソッドを定義します。

class Array
  def coerce(other)
    [Array(other), self]
  end
end

すると..

list = [2,3]
1 + list # => [1, 2, 3]
2.0 + list # => [2.0, 2, 3]

coerceでother(ここでは数値)をKernel.Arrayで型変換しています。こうすれば、Array#+が呼ばれてlistとの結合が可能になるのです。へぇ〜という感じですが、残念ながらこの手法は、coerceが適用できる数値クラス(Numericのサブクラス)にしか使えません(泣)

list = ['world', 'of', 'ruby']
'hello' + list # => 
# ~> -:9:in `+': can't convert Array into String (TypeError)

とりあえずモンキーパッチでStringに対応します..

class String
  alias :__plust__ :+
  def +(other)
    return [self] + other if other.is_a?(Array)
    super
  end
end

list = ['world', 'of', 'ruby']
'hello' + list # => ["hello", "world", "of", "ruby"]

そしてまた、Object#method_missingにご登場いただいて、+を未定義のオブジェクトにも対応します(懲りてない^ ^;)…

class Object
  def method_missing(name, *args, &blk)
    case name
    when :to_ary then [self]
    when :+ then self.to_ary + args.first
    else super
    end
  end
end

list = [1, 2]
:abc + list # => [:abc, 1, 2]

p1 = Person.new('Charlie', 12, :programmer)
p2 = Person.new('Bob', 29, :teacher)
[p1, p2].inject([]) { |m, x| m + x } # => ["Charlie/12/programmer", "Bob/29/teacher"]
[p1, p2].inject([]) { |m, x| x + m } # => ["Bob/29/teacher", "Charlie/12/programmer"]

「素直に[]付けろよ」というブコメが聞こえてきたので、そろそろこの投稿を終了させて頂きます m(__)m


coerceの活用は、プログラミング言語 Rubyの「7.1.6 演算子の定義」の説明を参考にしました。



blog comments powered by Disqus
ruby_pack8

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