Help us understand the problem. What is going on with this article?

ド・モルガンの法則でunlessのややこしい条件をifに読み替えよう

More than 5 years have passed since last update.

はじめに

条件分岐はプログラミングの基本です。
しかし、複雑な条件分岐が出てくると非常にコードが読みにくくなります。

さらに、その複雑な条件が unless と組み合わされていたりすると、ぱっと理解するのが非常に困難になります。
そこで、この記事では複雑な unless の条件を攻略する方法を説明します。

質問: "unless person.married? && !person.rich?" が真になるケースは?

ifとunless

たとえば以下のコードは「personが結婚していたら'Yo!'と声をかける」コードです。

puts 'Yo!' if person.married?

if文(if式)はほぼ全てのプログラミング言語に存在すると思いますが、それに加えてRubyでは false であれば真になる unless も使えます。

たとえば以下のコードは「personがお金持ちじゃなかったら'Yo!'と声をかける」コードです。

puts 'Yo!' unless person.rich?

ifで複数の条件を組み合わせると・・・?

条件分岐ではand(&&)やor(||)を使って、複数の条件を組み合わせることができます。

では、このコードはどういう場合に'Yo!'するでしょうか?

puts 'Yo!' if person.married? && !person.rich?

・・・はい、プログラミングの経験がある方ならわかりますよね。
「personが結婚していて、なおかつお金持ちでない場合」です。
つまり、married?がtrueで、rich?がfalseになる場合です。

unlessで複数の条件を組み合わせると・・・わからん!!

それでは、このコードはどうでしょう?

puts 'Yo!' unless person.married? && !person.rich?

真になる条件がぱっとわかりますか?
'Yo!'するケースを次の中から選んでください。

  1. married?もrich?もtrue
  2. married?がtrue、rich?がfalse
  3. married?がfalse、rich?がtrue
  4. married?もrich?もfalse

この条件を日本語で表現しようとしても「personが結婚していて、なおかつお金持ちでない場合でない場合・・・???」みたいになってしまって表現しづらいです。

ド・モルガンの法則を使って unless を if に変換する

こういう場合はド・モルガンの法則を使って、if文に変換するとわかりやすくなります。

unless person.married? && !person.rich?
if !(person.married? && !person.rich?) となり、以下のように表現されます。

Screen Shot 2014-08-27 at 7.41.05.png
※「∩」は「and」、上の横棒は「not」の意味です。

これをド・モルガンの法則を使って書き換えると次のようになります。

Screen Shot 2014-08-27 at 7.42.47.png
※「∪」は「or」の意味です。

つまり、先ほどのコードは次のようなコードに書き換えることができます。

# puts 'Yo!' unless person.married? && !person.rich?
puts 'Yo!' if !person.married? || person.rich?

日本語で書くなら「personが結婚していない場合、もしくはお金持ちである場合」に'Yo!'することになります。

もう一度先ほどの選択肢を見てみましょう。

  1. married?もrich?もtrue
  2. married?がtrue、rich?がfalse
  3. married?がfalse、rich?がtrue
  4. married?もrich?もfalse

「personが結婚していない場合」に該当するのは選択肢3, 4です。
「personがお金持ちである場合」に該当するのは選択肢1, 3です。
つまり、「3または4」または「1または3」となり、ひとまとめにすると 正解は「1または3または4」 となります。

これは言い換えると、「2以外ならば真」とも言えます。
「2(married?がtrue、rich?がfalse)以外ならば真」と考えたときは、以下のようなコードがイメージに近いかもしれません。

puts 'Yo!' if !(person.married? && !person.rich?)

unlessからifへ置き換える8パターン

unless a && bif !a || !b のように置き換える場合、組み合わせは8パターンあります。
それぞれのケースを一覧化すると次のようになります。

Screen Shot 2014-08-27 at 8.18.30.png

また、参考までに条件が真になる a と b の組み合わせも書いておきます。

unless + and(&&)

unless a && b <=> if !a || !b

  • 真になるのは a が false または b が false の場合
  • 言い換えると、偽になるのは a が true かつ b が true の場合のみ

unless !a && b <=> if a || !b

  • 真になるのは a が true または b が false の場合
  • 言い換えると、偽になるのは a が false かつ b が true の場合のみ

unless a && !b <=> if !a || b

  • 真になるのは a が false または b が true の場合
  • 言い換えると、偽になるのは a が true かつ b が false の場合のみ

unless !a && !b <=> if a || b

  • 真になるのは a が true または b が true の場合
  • 言い換えると、偽になるのは a が false かつ b が false の場合のみ

unless + or(||)

unless a || b <=> if !a && !b

  • 真になるのは a が false かつ b が false の場合のみ

unless !a || b <=> if a && !b

  • 真になるのは a が true かつ b が false の場合のみ

unless a || !b <=> if !a && b

  • 真になるのは a が false かつ b が true の場合のみ

unless !a || !b <=> if a && b

  • 真になるのは a が true かつ b が true の場合のみ

Personに'Yo!'するケースを総当たりで確認してみる

最後に、冒頭で紹介したPersonに'Yo!'するプログラムをRubyで書いてみます。
このプログラムではmarried?とrich?がtrue/falseになる組み合わせと、上で挙げたunlessの8パターンを総当たりで実行し、条件が真になるケースを出力します。

ソースコード

class Person
  attr_reader :married, :rich
  alias_method :married?, :married
  alias_method :rich?, :rich
  def initialize(married, rich)
    @married, @rich = married, rich
  end
  def to_s
    "@married=#{@married}, @rich=#{@rich}"
  end
end

people = [true, false].product([true, false]).map{|married, rich| Person.new(married, rich) }

conditions = [
  "person.married? && person.rich?",
  "!person.married? && person.rich?",
  "person.married? && !person.rich?",
  "!person.married? && !person.rich?",
  "person.married? || person.rich?",
  "!person.married? || person.rich?",
  "person.married? || !person.rich?",
  "!person.married? || !person.rich?"
]

conditions.each do |condition|
  puts "puts 'Yo!' unless #{condition}"
  people.each do |person|
    puts "Yo! (#{person})" unless eval(condition)
  end
  puts
end

出力結果

puts 'Yo!' unless person.married? && person.rich?
Yo! (@married=true, @rich=false)
Yo! (@married=false, @rich=true)
Yo! (@married=false, @rich=false)

puts 'Yo!' unless !person.married? && person.rich?
Yo! (@married=true, @rich=true)
Yo! (@married=true, @rich=false)
Yo! (@married=false, @rich=false)

puts 'Yo!' unless person.married? && !person.rich?
Yo! (@married=true, @rich=true)
Yo! (@married=false, @rich=true)
Yo! (@married=false, @rich=false)

puts 'Yo!' unless !person.married? && !person.rich?
Yo! (@married=true, @rich=true)
Yo! (@married=true, @rich=false)
Yo! (@married=false, @rich=true)

puts 'Yo!' unless person.married? || person.rich?
Yo! (@married=false, @rich=false)

puts 'Yo!' unless !person.married? || person.rich?
Yo! (@married=true, @rich=false)

puts 'Yo!' unless person.married? || !person.rich?
Yo! (@married=false, @rich=true)

puts 'Yo!' unless !person.married? || !person.rich?
Yo! (@married=true, @rich=true)

まとめ: とはいえ脳に優しいコードが一番!unlessの使用は必要最小限に

というわけで今回は unless で複数の条件を組み合わせた場合にどうなるかをいろいろなパターンで検証してみました。

ですが、我々プログラマはそもそも「わかりやすいコード」「脳に優しいコード」を書くことを目標にすべきです。

この記事は積極的に unless を使うことを推奨するために書いたわけではありません。
そうではなく、この記事は他の人が書いたコードにこういう条件分岐が出てきた場合に、どう読み砕けば良いのかを説明するために書きました。

個人的には unless を使う場合は、unless person.married?unless person.rich? など、ごく単純なケースにとどめておいた方が良いと考えています。

もし自分が unless person.married? && !person.rich? みたいな条件を書こうとした場合は、一歩立ち止まって if !person.married? || person.rich? のようなif文に置き換えることを検討してみましょう。

おまけ: 本当は条件をメソッド化してわかりやすい名前を付けるのがベスト

さらに言えば、if !person.married? || person.rich? で終わるのではなく、条件をメソッド化してわかりやすい名前を付けておくと、より「脳に優しいコード」になります。

たとえばこんな感じです。

class Person
  # ...
  def yo_target?
    !self.married? || self.rich?
  end
end
puts 'Yo!' if person.yo_target?

良いコードや良い設計に対する理解を深めるためには、こういった技術書で勉強するのが有効です。

CODE COMPLETE 第2版 上
CODE COMPLETE 第2版 上
posted with amazlet at 14.07.11
スティーブ マコネル
日経BP社
売り上げランキング: 22,218
CODE COMPLETE 第2版 下
CODE COMPLETE 第2版 下
posted with amazlet at 14.07.11
スティーブ マコネル
日経BP社
売り上げランキング: 42,181
リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)
Dustin Boswell Trevor Foucher
オライリージャパン
売り上げランキング: 1,568
jnchito
SIer、社内SEを経て、ソニックガーデンに合流したプログラマ。 「プロを目指す人のためのRuby入門」の著者。 http://gihyo.jp/book/2017/978-4-7741-9397-7 および「Everyday Rails - RSpecによるRailsテスト入門」の翻訳者。 https://leanpub.com/everydayrailsrspec-jp
https://blog.jnito.com/
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away