この記事は グロービス Advent Calendar 2017 の9日目です。
Rubyist の皆さん。unless
文使っていますか?
メジャーな言語では、Ruby 以外では Perl くらいでしか見当たらない構文です。
確かに、通常の条件分岐として使う場合はタイプ量が増えて可読性が落ちるため、unless
を使う意味はほぼありません。ですが、ある書き方をする場合において、unless
というのは、とてつもなく強力な効果を発揮します。それは Ruby という言語の持つ特徴であり、Ruby を使うからには、是非利用したい機能です。
結論は以下です。
unless
は、後置で前提条件を書く場合に、とても強力である
以下の内容を、unless
を題材にして解説します。
- ある式が「数学的に同じ意味(同値である)」であることと、「文章的に同じ意味」であることは一致しない
- プログラミング言語で「処理」できるのは、数学的な意味のみ
- プログラミング言語で文章的な意味も「表現」できる。そう、Ruby ならね
- 前提条件と、本来の要件
通常の条件分岐の場合
まずは、プログラミングを学習して最初に触れる条件分岐の形式を見ます。
# case1
if a != b
puts(a); puts(b); puts("unmatch")
else
puts(a); puts("match")
end
# case2
if !(a == b)
puts(a); puts(b); puts("unmatch")
else
puts(a); puts("match")
end
# case3
unless a == b
puts(a); puts(b); puts("unmatch")
else
puts(a); puts("match")
end
# case4
unless !(a != b)
puts(a); puts(b); puts("unmatch")
else
puts(a); puts("match")
end
上の4つは、数学的には全く同じ意味のことを行なっていますが、case4 は、可読性は低くタイプ量も多く、production のシステムで使おうとする方は皆無でしょう。
case2 と case3 のどちらを採用すべきなのか
case2 と case3 ですが、unless には elsif に当たる処理がなく、かつ、タイプ量も増え、可読性も変わらないため、case2 の書き方を採用すべきです。
単純な条件分岐の場合、unless 文が登場する余地はありません。他言語で採用されないわけです。
case1 と case2 のどちらを採用すべきなのか
case1 の a != b
と、case2 の !(a ==b)
は、数学的には全く同じ意味を指します(同値です)。数学的な意味のみを考えるならタイプ量が少なく、かつ可読性もそう大きく変わらない case1 を採用すべきでしょう。
ですが、全てのケースでそのようなことがあるでしょうか。
- a と b は異なる
- 「a と b は一致する」というケースではない
上記、結果同じだとしても、人間の思考としては、明確に異なります。
以下のユースケースを見てみます。
- プレゼントにブランドバックを送る
- 相手の気持ち
- お気に入りのグッズは全てグッチで揃えていて、それ大前提
- 財布は持っているから、それ以外ならなんでもいい、けど、いい財布なら、まぁ考える
- 自分のゴール
- 彼女に気に入って欲しい(そしてあわよくば、、、)
- 相手の気持ち
上記、文章の通り素直にコーディングしてみます。
class Present
attr_accessor :brand, :type
def initialize(brand, type)
@brand = brand
@type = type
end
end
def give_present(present)
if present.type == "gucci" # プレゼントのブランドはグッチである
if present.type != "wallet" # プレゼントの商品は財布でない
puts("ありがとう!") # お礼をいう
method_wallet_change # 中身を入れ替える
fashion_check
etcetc
else
check_style(present) # 今のよりかわいければ、まぁ、いいか
etcetc
end
end
end
もう少し整理します。
def my_favorite_brand?(present) # お気に入りのブランドは
present.type == "gucci" # グッチである
end
def in_wish_list?(present) # 私の欲しいものリストは
present.type != "wallet" # 財布以外ならなんでもいい
end
def give_present(present)
if my_favorite_brand?(present) # 私のお気に入りのブランドの中で
if in_wish_list?(present) # 欲しいものリストに入っている
puts("ありがとう!")
method_wallet_change
fashion_check
etcetc
else
check_price(present)
etcetc
end
end
end
そもそも case1 や case2 であることがまずく、可能な限りメソッド化をして、条件式そのものは if 文の前でカプセル化 して制御構造を見えなくしておくことが必要と思います。
カプセル化と後置if
if
が多段になることは読みづらい文章になります。実際の文章表現でも「かつ」が連続して続く文章はよくなく、そのような場合は短い文章に直すことができます。直しましょう。
def give_present(present)
if !my_favorite_brand?(present); return nil; end # 私のお気に入りのブランドでないなら、なし
if in_wish_list?(present) # 欲しいものリストに入っている場合
puts("ありがとう!")
method_wallet_change
fashion_check
etcetc
else # 入っていない場合
check_price(present)
etcetc
end
end
Ruby は if を後置できます。しましょう。
def give_present(present)
return nil if !my_favorite_brand?(present) # 私のお気に入りブランドでないなら、論外
if in_wish_list?(present) # 欲しいものリストに入っている
puts("ありがとう!")
method_wallet_change
fashion_check
etcetc
else
check_price(present)
etcetc
end
end
前提条件と後置unless
この
return nil if !my_favorite_brand?(present)
ですが、折角カプセル化で消した if 文内に、「!」という制御構造が復活しています。これを消すために
def my_not_favorite_brand?(present); !my_favorite_brand?(present); end
とするのは、決して得策ではありません。結局、意味が冗長になっているだけです。
とはいえ、ifの条件文の中に制御構造が入っていると、意味のあるものなのか、カプセル化が不十分なものなのか、判別がつかないという状態になります。
ここで、unlessを使います。
return nil unless my_favorite_brand?(present)
これにより分かることは下記です。
-
my_favorite_brand
でない場合、return nil
で処理を止める -
my_favorite_brand
なら、以下の処理を続ける - つまり、
my_favorite_brand
は、以下の処理を進める 前提条件 である
つまり、このメソッドは、
def give_present(present)
return nil unless 前提条件
具体的な処理
end
と書くことができます。失敗の場合に何かメッセージを出したいときは
def failed_message
puts("ミッション失敗!")
return nil
end
def give_present(present)
return failed_message unless 前提条件
具体的な処理
end
という形にできます。上記程度なら、丸括弧を使って1行で書いてもいいでしょう。
後置unless と 後置if
後置if と 後置unless を、あるメソッドの冒頭に書くことができます。
def method
return unless 前提条件
return if 足切り条件
具体的な処理
end
具体例を出します。
def 入学試験に合格
# 足切り条件
return false if 素行に問題がある
# 前提条件
return false unless 一次試験で上位XX%に入っている
# 具体的な要件
if スポーツ推薦
スポーツ推薦用の基準を超える
elsif AO入試
AO入試用の基準を超える
elsif 出生地条件(地元優先枠)
出生地条件枠の基準を超える
else
定員から、上記で埋まった数を除いた中で、上位から取る
end
end
return if
によって、足切り条件(ブラックリストに当てはまらない)、return unless
によって、前提条件(ホワイトリストに当てはまる)が表現できます。
どちらを使った方が考えやすいか、は、それぞれの文脈、要件によって変わるでしょう。
大切なこと
- if を多段にしない。山のような if 文を書かない
- まず、自分のやりたい処理の前提条件を出す
- 前提条件は、「それだとわかりやすいように」表現する記法が Ruby にあるので活用する。
- 数学的な意味だけではなく、人間の考えるようにかく
紹介:Array#size と Array#length の使い分け
unless 以外にもう一つ紹介。
コーディングをするとき、Arrayのオブジェクトを、集合として使うか(順番に意味がない)、リストとして使うか(順番に意味がある)、その両方のケースは、あるかと思います。
Array の中の要素をカウントするとき、
irb(main):001:0> [0,5,2,6,9].size # Array を集合として解釈し、その「大きさ」を測っている
=> 5
irb(main):002:0> [0,5,2,6,9].length # Array をリストとして解釈し、その「長さ」を測っている
=> 5
irb(main):003:0>
のエイリアスメソッドを使い分けることによって、それをどのように「見なして/解釈して」コーディングしているか、を頭の切り替えすくなく、文章的に表現することができます。
初めて Ruby を使ったのは大学院の研究のときで、指導教官は「Ruby は好きだけどエイリアスが多いのが気にくわない(覚えること多くて)」と言っていましたが、自分は長く読み続けるコードでは、このエイリアスの多さがメリットになる、と今では思っています。
Ruby の特徴
Ruby は宗教だ、と他言語利用者の方からよく揶揄されます。自分も Rubyist として、さもありなん、と思います。Ruby は、物事の事象を正確に記述しようとすると同時に、それを読む人の心理まで、強く考慮できる言語です。そのような言語を生んだのが Matz というのは、言霊の国日本の文化がどれほど影響があるのか個人的に関心のあるところですが、ある道具を使うからには、その特徴を理解した上で、使いやすいように使う、その文化的背景、歴史を知ろうとするのは決して無駄ではないと思う所存です。