前置き
Rubyの論理演算子には &&、||、and、orの4種類があり、それぞれ優先順位が異なる。
&&と||は演算子としては優先順位が高く、両者の間では&&が優先される。
andとorは演算子としての優先順位が低く、両者の間に優先順位はない。つまり左から順に評価される。
これらは「フロー制御にも用いる事ができる」と解説される場合があるが、その表現は正しくない。
and、orは非常に強力なフロー制御演算子である。フロー制御に使うために実装されたと言っても過言ではないのだ(後述)。
これらをフロー制御に使うべきという提案は Ruby Good Idiom などにより古くから行われてきたが、肝心のそれらの記事も使い方を間違っているという問題もあり、あまり浸透していないのが現状だ。公式リファレンスですらこれらを軽視している。
今回の記事ではその辺りを詳しく説明しつつ、フロー制御演算子の真価について解説していきたい。
フロー制御構文
フロー制御演算子は名前の通り「フロー制御」を行う「演算子」である。フローを制御する機能と、演算子として値を返す機能の二つを併せ持っている。andとorに優先順位が無いのも、フロー制御のための演算子だからだ。つまりフロー制御演算子はフローと戻り値を制御する演算子、と言い換えてもいい。
フロー制御に使うものであるため、その本質は制御構文と変わらない。つまり1行にまとめてネストする使い方はコードの可読性とフローの可視性を著しく下げる間違った使い方と言える。
正しく使うのであれば、1行の行末に使い、改行を伴うのが最も良い。このルールを徹底するだけでも十分な効果がある。
前述の Ruby Good Idiom などでは例外処理にこの演算子を使っていたが、そのような使い方は間違っていると言える。なぜならそもそも例外とはフローの制御から外れたものを指すからだ。例外を投げるならフロー制御に入る前、もしくはフロー制御後にガード節を設けて投げるべきだろう。
一般的な制御文の例
一般的な制御文を用いて戻り値を制御する場合、下記のようなコードになる。
def test_method
if a > 0 && b > 0 # functionを呼ぶための条件比較
function(a, b) # functionの戻り値がtest_methodの戻り値となる
else
false # 条件を満たさなければtest_methodの戻り値としてfalseを返す
end # 仮にelse節が無い場合はtest_methodの戻り値はnilになる
end
理解しやすくはあるが冗長になる。Rubyらしさはかけらも無い。
論理演算子を用いた例
論理演算子を用いて戻り値を制御する場合、下記のようなコードが多い。
def test_method
a > 0 && b > 0 && function(a, b)
# 前の条件比較がfalseになった時点でfalseを返す
# そうでなければfunctionの戻り値がtest_methodの戻り値となる
end
非常にコンパクトになっているが条件比較と関数呼び出しという二つのフローが一行に混在してしまい、コードの可読性、およびフローの可視性が非常に低い。他言語でもよく見かける圧縮記法の悪い例だ。
フロー制御演算子を用いた例
フロー制御演算子を用いて戻り値を制御する場合、下記のコードになる。
def test_method
a > 0 && b > 0 and # 条件比較が満たされれば次にフローを進める
# 満たされなければfalseを返してフローを抜ける
function(a, b) # functionの戻り値がtest_methodの戻り値となる
end
フロー制御構文のルールにのっとり、フロー制御演算子を行末に使い改行している。
条件比較と関数呼び出しという二つのフローが視覚的に分離していてコードの可読性とフローの可視性が高い。加えて制御文のような冗長さもない。Rubyのフロー制御演算子が非常に強力な構文である証明と言える。
Rubyのメソッド仕様
Rubyでは「値とnilのどちらか」を返すというメソッドが非常に多い。これは意図的にそう設計されている。Rubyでは値(nilとfalse以外のオブジェクト)はtrue、nilはfalseとして扱われる。つまりフロー制御演算子で評価できる。まさにフロー制御演算子を使いフローと戻り値を制御するための仕様と言える。これらはフロー制御メソッドと呼ぶのが相応しい。
以下にサンプルコードを示す。
フロー制御メソッドでフローと戻り値を制御する例
def find_employee(name)
役員名簿.find{|m| m.name == name } or
社員名簿.find{|m| m.name == name } or
出向社員名簿.find{|m| m.name == name } or
nil # 存在しない
end
このコードは3つの名簿を上から順に探し、名前が一致する社員を返すメソッドである。フロー制御メソッドfindによって社員が見つかれば値(true)を返すため、フロー制御演算子orがそこで止まり、見つかった値がメソッドの戻り値となる。見つからなければ次の名簿へとフローが移り、最後まで見つからなければnilを返して存在しなかった事を示す(自身もフロー制御メソッドとして振る舞う)。
フロー制御演算子とフロー制御メソッドが美しく噛み合い、無駄なコードが一切無いシンプルなフロー構造が浮き彫りになっている。これこそがRubyが目指した世界なのである。
なお上記コードの名前判定部分の重複が気になるならばクロージャをProc化して渡せばいいだろう。それもまたRubyらしい解決法なのだから。
それと最後のnilは不要では?と思う諸兄もいるだろう。コード上はまさにその通りで最後のfindも値かnilを返すため無くても結果は変わらない。だが、フローを可視化するという点においては必要なのだ。せっかくフロー制御演算子でフローの可視性を高めても、最後にそれを隠蔽してしまっては片手落ちというものだろう。ゆえに私はここにnilを置くのだ。
and と or の使い分け
andは最初のフロー制御例のように、判定式と次のフローを繋ぐ役割を持っている。条件を満たしたら、フローを進める演算子と覚えると良い。
orは最後の例のように、フロー制御メソッドを繋ぐ役割を持っている。フロー制御メソッドによって値が見つからなければ(nilが返ったら)フローを進める演算子と覚えると良い。
まとめ
今回の記事でRubyがいかに美しい設計思想を持っていたのかを再確認してもらえたと思う。フロー制御演算子を使った美しいフロー制御、フロー制御メソッドと併用した際の圧倒的な可読性。Rubyはプログラミングを楽しむために設計された人間重視の言語なのだ。
以前投稿したunlessとuntilの正しい覚え方と併せて、是非とも多くのRubyistに活用して欲しい。
おまけ
フロー分割の基本指針
今回の記事と前述のunlessの記事のサンプルコードを見て気付いた人もいるだろうが、悪い例を除いて否定演算子(!やnot)が一切使われていない。これは私がそういう条件式を選んでそれしか使っていないのではない。そもそも条件式に否定演算子が混ざるのはフローの分割が適切にできていない証拠である。
以下にわかりやすいコード例を示す。
return if !obj || obj.empty? # よく見かけるガード句
上の例はRubyのプログラマなら一度は書いた事があるであろう代表的なガード節だ。しかしこれは良く見れば二つのフローが混在している。このフローを適切に分ければ以下のようになる。
return unless obj # ホワイトリスト nil以外を通す
return if obj.empty? # ブラックリスト 中身が空なら通さない
このようにフローが混在しなければ否定演算子は基本的には混ざらないのである。だからもし条件式に否定演算子が混ざっているようならまずはフローが混在していないかを確認した方がよい。今回の記事のように条件式と関数呼び出しが混ざっていたり、上の例のようにホワイトリストとブラックリストのガード節が一つのガード句としてまとまっているならば、そこでフローを分けて記述するだけでコードの可読性とフローの可視性は劇的に向上するはずだ。
勿論、完全にゼロにすることはできない。例えば !ary.include?(0) のような場合にはどうしようもない事もある。exclude?メソッドを実装するのも手だが、まあこの程度なら可読性が落ちるような事もないので許容範囲といえる。重要なのは否定演算子の撲滅ではない。フローの適切な分割による可読性、可視性の向上、ただそれだけなのだ。
過去の自分は他人
これらの可読性にこだわった書き方は、やもすれば無粋なお節介にも感じるかもしれない。しかし数年後、あるいは数ヵ月後、ある程度現場から離れてから、かつて自分が書いたコードを見ると、まるで他人が書いたスパゲティコードを見るかのように意味が理解できなかったという経験はプログラミングを長く続けている人ほど身に沁みているだろう。
コメントで注釈を入れるのも良いが全てのコードでそれをやるのは冗長だし見栄えも悪い。フローを分離し、コードの意図をコード自身に語らせる設計にした方が何倍もマシなのだ。
大事な事なのであえて二回言おう。「過去の自分は他人」である。それは逆説的に未来の自分も他人であるということだ。たとえ個人開発であっても、いや個人開発だからこそ、将来の自分自身が困惑しないようなコードを書くことが重要なのだ。なぜならそのツケは間違いなく、自分自身に降りかかるのだから。