2023年2月某日、Twitterが氷河期になりTLが荒れ狂う最中、私は一つのコードに苛まされていた。
配列の正規化を行うため、以下のコードを実行したところ、
fp = File.new("hoge.mp4")
Array(fp)
=> ["謎のデータ配列"] # [fp] が帰ってくる予定だった
は????????
配列の正規化とは
配列またはObject、いずれかの値がやってくる場合に、
配列または[ Object ]、に変換しその後の処理を簡潔にするためのテクニックのこと
たとえば
def hoge(file_or_files)
# ↓ ここが配列の正規化
files = file_or_files.is_a?(Array) ? file_or_files : [file_or_files]
files.each do |file|
# 個々のファイルに対して処理をする
...
end
end
のように書くと、引数の形式をそこまで気にせずに良くなるってヤツ
自分が確認した中では2種類の記法がある
方法1. 与えられた値を配列で囲んで、その後flattenする方法
書き方:
[item_or_items].flatten
# 配列を入れた場合
items = [:hoge]
[items].flatten
=> [:hoge]
# オブジェクトを入れた場合
items = :hoge
[items].flatten
=> [:hoge]
見たらわかると思うが、ぱっと見で何やってるのかわかりにくい
また当たり前だが、多重配列を使っている場合に、flattenしすぎる場合がある
items = [[:hoge]] # 二重配列
[items].flatten
=> [:hoge] # 一重配列
方法2. Array() って関数を使う方法
書き方:
Array(item_or_items)
# 配列を入れた場合
items = [:hoge]
Array(items)
=> [:hoge]
# オブジェクトを入れた場合
items = :hoge
Array(items)
=> [:hoge]
自分は最初、方法1.の方を使用していたものの
- 方法2. の方がぱっと見で何してるかわかりやすい
- 方法2. では多重配列の次元数が下がりすぎることがない
という理由から、もっぱら最近は 方法2. を利用するようになっていた
今回何が起きたのか
さて、今回起きたことに戻ろう
fp = File.new("hoge.mp4")
Array(fp)
=> ["謎のデータ配列"] # [fp] が帰ってくる予定だった
返り値を見てる限り、なんとなくファイルのデータがそのまま出てきてる感じがする
じゃあなんでそんなことが起きるのか。色々試してたらなんか見つけた
fp = File.new("hoge.mp4")
fp.to_a
=> ["謎のデータ配列"] # Array(fp) の時と同じ値
ぉん?
なるほど、もしかしたら Array(obj)
って to_a
を内部的に行ってるのかもしれない
というわけで以下を試してみた
class A
def to_a
[:dummy]
end
end
Array(A.new)
=> [:dummy]
_人人人人人人人_
> [:dummy] <
 ̄Y^Y^Y^Y^Y^Y ̄
なるほどなぁ
つまり Array(val)
を同値に変換すると
val.respond_to?(:to_a) ? val.to_a : [val].compact
と同じようなコードの可能性が高い。実装読むのは諦めたので間違ってたらごめんなさい
その結果、最初のような想定外の挙動をしたわけだ
対処法
結局 Array(val)
だとエラーが起きるので、とりあえず [val].flatten
に戻して様子を見ることにした。今回のようなエラーもあるので、ものによっては 方法2. がいい場合もあるが、基本的には 方法1. を選択した方がいいように思う。
それよりも本来は
class Array
def self.norm(val)
val.is_a?(Array) ? val : [val].compact
end
end
って関数定義してこれ使った方が想定外の挙動もなく安定する気はするけどね
以上。