Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rubyスタイルガイドを読む: 文法(8)配列や論理値など

こんにちは、hachi8833です。長くなりましたが、「Rubyスタイルガイドを読む」シリーズの文法編もやっと今回で終わりです。次回からの「命名編」もどうぞご期待ください。

文法(8) 配列や論理値など

2-58【統一】文字列を引数に取るArray#*はわかりにくいので避け、Array#joinを使うこと

Favor the use of Array#join over the fairly cryptic Array#* with a string argument.

# 不可
%w(one two three) * ', '
# => 'one, two, three'

# 良好
%w(one two three).join(', ')
# => 'one, two, three'

Rubyリファレンスマニュアル: Array#*では以下のAとBの2種類が記載されています。Aが本来望ましい用法と思われるので、乗算などとまぎらわしいBの用法を避けてArray#joinで記述するということですね。

# A: 繰り返しの *
p [1, 2, 3] * 3  #=> [1, 2, 3, 1, 2, 3, 1, 2, 3]

# B: 結合の *(避ける)
p [1, 2, 3] * ","  # => "1,2,3"

2-59【統一】「変数が配列でない場合は配列に変換する」処理はArray()で書くこと

Use Array() instead of explicit Array check or [*var], when dealing with a variable you want to treat as an Array, but you're not certain it's an array.

# 不可1 (変数が配列かどうかをチェックするためだけの1行目が冗長)
paths = [paths] unless paths.is_a? Array
paths.each { |path| do_something(path) }

# 不可2 (Arrayインスタンスが毎回作成されてしまう)
[*paths].each { |path| do_something(path) }

# 良好 (かつ読みやすい)
Array(paths).each { |path| do_something(path) }

配列になっている変数を#eachで回して処理するのはRubyでは定番中の定番ですが、変数を素直に#eachすると、変数が配列でない場合にエラーになってしまいます。

paths = "a"
paths.each { |path| puts path } # pathsが配列になってないとエラーになる

かといって、最初の「不可1」コード例のように変数が配列かどうかをわざわざチェックするのは残念です。


「不可2」のコード例では[*paths].eachのような記法を使って条件分岐をなくしています。

paths = "a"
[*paths].each { |path| puts path } # 動くが、Arrayのインスタンスが常に生成されてしまう

[*paths]は変数が配列であってもなくても動作しますし、一見よさそうですが、変数が配列であるかどうかにかかわらず常にArrayインスタンスが生成されてしまいます。morimorihogeさんがすかさず動作を確認してくれました。

arr = ['a', 'b']
arr.object_id
[*arr].object_id  # 毎回異なる
[*arr].object_id  # 毎回異なる
...

確かにobject_idが毎回異なっており、[*arr].object_idにアクセスするたびにインスタンスが生成されていることがわかります。arr.object_id[*arr].object_idも異なっています。これは効率が悪そうです。

なお[*paths]のアスタリスクは、実引数側で「配列を展開して渡す」ためのものです。

メソッドに引数を渡す場合に、配列を展開してメソッドの引数にすることもできます。メソッドの呼び出しの際に、「*配列」の形式で引数を指定すると、配列そのものではなく、配列の要素が先頭から順にメソッドの引数として渡されます。ただし、配列の要素の数とメソッドの引数の数は一致していなければいけません。
たのしいRuby』第5版第1刷、p115より

[tmkm-amazon]B01C804DO8[/tmkm-amazon]


代わりに推奨されているのが「良好」コード例で示されているArray(paths).eachのような書き方です。

Array(paths)だとインスタンスが生成されないかどうか試してみましょう。

arr = ['a', 'b']
arr.object_id
Array(arr).object_id  # arr.object_idと同じ
Array(arr).object_id  # arr.object_idと同じ
...

確かにobject_idは毎回同じになっています。arr.object_idArray(arr).object_idも同じなので、インスタンスは生成されていません。

babaさんの指摘でわかりましたが、Array()Kernel#Arrayという、Kernelモジュールのメソッド(Ruby全体で使える関数的なメソッド)です。

大文字で始まる珍しいメソッド名なので、Array#initializeでインスタンスを生成しているように一瞬見えてしまいますが、KernelモジュールはObjectクラスにインクルードされるのでnewせずにRubyのどこででも使え、最終的にrb_Arrayという関数を呼んでいます。

DevDocなどのKernel#arrayのリンク先で「show source」をクリックするとrb_arrayを呼んでいることを確認できます。

なおrb_arrayは、objがArrayでない場合は#to_aを使ってArrayに変換するので、Kernel#Arrayに渡す変数が配列ではない場合は以下のように毎回インスタンスが生成されます。変数の要素が1つなら#eachは1回で終わるので問題にはならないと思います。

arr = "a"             # 配列でない変数
arr.object_id
Array(arr).object_id  # 毎回異なる
Array(arr).object_id  # 毎回異なる
...

2-60【統一】範囲演算子やComparable#between?を使って比較をできるだけ簡潔に書く

Use ranges or Comparable#between? instead of complex comparison logic when possible.

# 不可
do_something if x >= 1000 && x <= 2000

# 良好
do_something if (1000..2000).include?(x)

# 良好
do_something if x.between?(1000, 2000)

2-61【統一】比較条件ではeven?zero?などの述語メソッドが望ましい(==の直接使用は避ける)

Favor the use of predicate methods to explicit comparisons with ==. Numeric comparisons are OK.

ただし数値の比較に==を使うのは許容されるそうです。

# 不可
if x % 2 == 0
end

if x % 2 == 1
end

if x == nil
end

# 良好
if x.even?
end

if x.odd?
end

if x.nil?
end

if x.zero?
end

if x == 0   # これはOK
end

多くのプログラミング言語では「条件部分になるべくリテラルを書かないようにする」ことが推奨されていますが、Rubyではさらに進んで述語メソッド(論理値--truefalseのいずれかだけ--を返すメソッド)の利用が推奨されます。

2-62【統一】 nilでないことのチェック(nilチェック)は、論理値を扱っていることが確実でない限り行わないこと

Don't do explicit non-nil checks unless you're dealing with boolean values.

# 不可
do_something if !something.nil?
do_something if something != nil

# 良好
do_something if something

# 良好 - 論理値に変換している
def value_set?
  !@some_boolean.nil?
end

2-63【統一】 BEGINブロックは避ける

Avoid the use of BEGIN blocks.

2-64【統一】 ENDブロックは避け、Kernel#at_exitにする

Do not use END blocks. Use Kernel#at_exit instead.

# 不可
END { puts 'Goodbye!' }

# 良好
at_exit { puts 'Goodbye!' }

2-65【統一】フリップフロップは避ける

Avoid the use of flip-flops.

何もサンプルがありませんが、ここで言うフリップフロップは「呼ぶたびにオン/オフやtrue/falseなどの状態を反転するメソッド」を指すと考えられます。

フリップフロップを避けるということは、たとえば明示的に「オンにするメソッド」「オフにするメソッド」で書くということですね。

追記(2018/06/17)

Rubyには..記号を用いた特殊なフリップフロップ構文があります。

if i==3..i==5

参考: Rubyのフリップフロップ - monamonamonad.github.io

追記(2019/02/13)

フリップフロップはRuby 3で廃止が決まりました。Ruby 2.6ではwarningが表示されます。

参考: サンプルコードでわかる!Ruby 2.6の主な新機能と変更点 - Qiita

2-66【統一】制御で条件をネストすることは避ける

Avoid use of nested conditionals for flow of control.
Prefer a guard clause when you can assert invalid data. A guard clause is a conditional statement at the top of a function that bails out as soon as it can.

以下にあるように、guard clauseの利用が推奨されています。用が済んだらすぐ脱出することで、条件の無駄なネストを避けられます。

# 不可
def compute_thing(thing)
  if thing[:foo]
    update_with_bar(thing[:foo])
    if thing[:foo][:bar]
      partial_compute(thing)
    else
      re_compute(thing)
    end
  end
end

# 良好
def compute_thing(thing)
  return unless thing[:foo]                          #<= guard clauseその1
  update_with_bar(thing[:foo])
  return re_compute(thing) unless thing[:foo][:bar]  #<= guard clauseその2
  partial_compute(thing)
end

2-67【統一】ループではnextの利用が望ましい

Prefer next in loops instead of conditional blocks.

if〜endunless〜endといった条件ブロックをこしらえなくても、nextと後置条件の合せ技にすることですっきりと1行で書けます。

# 不可
[0, 1, 2, 3].each do |item|
  if item > 1
    puts item
  end
end

# 良好
[0, 1, 2, 3].each do |item|
  next unless item > 1       # 簡潔
  puts item
end

2-68【統一】以下の別名メソッドについて優先順位の目安を定める

優先順位: 低 優先順位: 高い
#collect #map
#detect #find
#find_all #select
#inject #reduce
#length #size

同じ行のメソッドは互いに別名になっています。

Prefer map over collect, find over detect, select over find_all, reduce over inject and size over length. This is not a hard requirement; if the use of the alias enhances readability, it's ok to use it.

このスタイルは別名メソッドを統一する目安にとどめられており、厳しくはありません。injectはRubyでは相当有名なメソッドなので、突然reduceに変えるとかえっていぶかしがられるかもしれませんね。

The rhyming methods are inherited from Smalltalk and are not common in other programming languages. The reason the use of select is encouraged over find_all is that it goes together nicely with reject and its name is pretty self-explanatory.

#collect#detectといった「韻を踏んだ」メソッド名がSmalltalk由来で他の言語で馴染みが薄いからという程度の理由付けです。言われてみれば表の左は〜ectで終わる名前が多いですね。

#find_all#selectだけ#selectが優先されているのは、#selectの方が#rejectと(英語的に)相性もよく意味もわかりやすいからだそうです。

2-69【統一】#count#sizeの意味で使わないこと

Don't use count as a substitute for size. For Enumerable objects other than Array it will iterate the entire collection in order to determine its size.

Arrayオブジェクト以外のEnumerableオブジェクトではサイズを確定するためにコレクション全体を列挙します。

# 不可
some_hash.count

# 良好
some_hash.size

2-70【統一】 #map#flattenではなく#flat_mapを使うこと

ただしflat_mapだと階層が3つ以上の場合に完全にフラットにしきれないので、その場合は#map#flattenを使ってよいそうです。

Use flat_map instead of map + flatten.
This does not apply for arrays with a depth greater than 2, i.e. if users.first.songs == ['a', ['b','c']], then use map + flatten rather than flat_map. flat_map flattens the array by 1, whereas flatten flattens it all the way.

  • #flat_map: 配列の階層を1段階浅くする
  • #flatten: 配列の階層を完全にフラットにする
# 不可
all_songs = users.map(&:songs).flatten.uniq

# 良好
all_songs = users.flat_map(&:songs).uniq

2-71【統一】#reverse.eachよりも#reverse_eachが望ましい

Prefer reverse_each to reverse.each because some classes that include Enumerable will provide an efficient implementation. Even in the worst case where a class does not provide a specialized implementation, the general implementation inherited from Enumerable will be at least as efficient as using reverse.each.

reverse_eachは少なくともreverse.eachより効率が落ちることはなく、クラスによってはreverse_eachの実装の方が効率がよいそうです。

# 不可
array.reverse.each { ... }

# 良好
array.reverse_each { ... }

文法編は今回で完了です。来週はいよいよ「命名編」に進みます。ご期待ください。

関連記事



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。