Edited at

[Ruby] Kernel.#Array に困ったら Active Support コア拡張機能の Array.wrap を使おう


TL;DL

require 'active_support/core_ext/array/wrap'

hash = { name: '中川夏紀', instrument: 'ユーフォニアム' }
Array(hash)
#=> [[:name, "中川夏紀"], [:instrument, "ユーフォニアム"]]
Array.wrap(hash)
#=> [{:name=>"中川夏紀", :instrument=>"ユーフォニアム"}]

range = 1..10
Array(range)
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Array.wrap(range)
#=> [1..10]


Kernel.#Array について

さまざまなオブジェクトを配列に変換してくれる Kernel.#Array、便利ですよね。

Array(nil)

#=> []

Array(1)
#=> [1]

Array([1, 2, 3])
#=> [1, 2, 3]

しかし、ときに困った振る舞いをします。例えば Hash オブジェクトを、それを要素とした配列に変換したいときに Kernel.#Array を使うと

hash = { name: '中川夏紀', instrument: 'ユーフォニアム' }

# [hash] を期待したが、実際はキーと値のペアの配列を返す。
Array(hash)
#=> [[:name, "中川夏紀"], [:instrument, "ユーフォニアム"]]

のように期待と異なる値を返します。

この振る舞いは Kernel.#Array のリファレンスマニュアル に記載されている


arg.to_ary と arg.to_a をこの順に呼び出して、返ってきた配列を変換結果とします。

arg に to_ary, to_a のいずれのメソッドも定義されていない場合は 一要素の配列 [arg] を返します。


という性質によるものです。Hash オブジェクトには Hash#to_a メソッドが定義されているため、それが呼ばれるわけです。

hash = { name: '中川夏紀', instrument: 'ユーフォニアム' }

hash.to_a
#=> [[:name, "中川夏紀"], [:instrument, "ユーフォニアム"]]

hash.to_ary
# NoMethodError

では「Hash オブジェクトをそれを要素とした配列に変換したいとき」はどうすればよいのでしょうか?


Array.wrap について

ここで役に立つのが Active Support コア拡張機能 に含まれる Array.wrap です。使用するにはまず Active Support をインストールしてください。

gem install activesupport

Array.wrap メソッドに Hash オブジェクトを渡すとどうなるでしょうか?

require 'active_support/core_ext/array/wrap'

hash = { name: '中川夏紀', instrument: 'ユーフォニアム' }

Array.wrap(hash)
#=> [{:name=>"中川夏紀", :instrument=>"ユーフォニアム"}]

無事やりたいことが実現できました :tada:

この振る舞いは Rails ガイドの Active Support コア拡張機能のページ に記載されている


引数がnilの場合、空の配列が返されます。

上記以外の場合で、引数がto_aryに応答する場合はto_aryが呼び出され、to_aryの値がnilでない場合はその値が返されます。

上記以外の場合、引数を内側に含んだ配列 (要素が1つだけの配列) が返されます。


という性質によるものです。Hash オブジェクトには to_ary メソッドが定義されていないので「引数を内側に含んだ配列を返す」ということです。


その他の例

同様の理由で、例えば Range オブジェクトを引数に渡した場合も Kernel.#Array と Array.wrap とで振る舞いが異なります。

range = (1..10)

range.to_a
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

range.to_ary
# NoMethodError

# Range#to_a が呼ばれる。
Array(range)
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# 引数を要素として含んだ配列を返す。
Array.wrap(range)
#=> [1..10]

うーん、このかゆいところに手が届く感じが好き :hearts:


参考