2010年1月7日木曜日

if と真偽値

Java の if 文での検査は真偽値のみ

最初に馴染んだ言語が Java だったので、「if 文での検査で使えるのは当然 true または false でしょ」という感覚が染み込んでいる。例えば、Java で

if (0){
 ...
}

をコンパイルしようとすると、

Exception in thread "main" java.lang.RuntimeException: Uncompilable source code - 互換性のない型
  期待値: boolean
  検出値:    int

というエラーが表示される。

Java言語規定 ブロック及び文 の 「14.8 if 文」によると、

if ( Expression ) Statement

… 式(Expression) は,論理型をもたなければならない。そうでなければ,コンパイル時エラーが発生する。

(太字は引用者による)

という仕様であるため。

 

C 言語 の if 文での検査は 0 が 偽

しかし、C 言語では上記のコードでもコンパイルが通る。なぜなら、初級C言語Q&A(5) によると、

Q 【真偽値】

他の言語には真偽の値を表現するための型が用意されているものがあるが、なぜC言語には用意されていないのか。

… 最も大きな理由は、それがなくても実用上差し支えないからでしょう。実際、1という値を真、0という値を偽であることにすれば、整数型を使って真偽を表現することが可能ですから。

この `0’  を偽 と見なすのは最初違和感があった。。 (@_@;

数値の比較をしてみると、

1 == 1

で 1 が返り、

1 < 1

で 0 が返る。

この仕様だと、自分でブール型を用意しなければ、真偽値を返すことを意図した関数と、数値を返す関数をコンパイラが区別できない。 (+_+)

ただし、プログラミング言語 C の新機能 によると C99 で、

今度の C 言語では新しい整数型 _Bool 型を導入することでその問題を解決します。この型は 0 と 1 が入れば十分な大きさとされており、必ずしも int 型と同じサイズであるとは限りません。

となり、型的にはスッキリとする。

 

Python, Ruby の真偽テスト

Python はたくさんの値が 偽 と解釈される

Python は 2.3.1 真値テスト で述べられているように、たくさんの型の値が条件文で 偽 として扱われる。例えば、リストと文字列の上位にシーケンス型があり、空文字・空リスト

”” , []

は 偽 。(これにより、if の検査で リストと文字列を透過的に扱う ことができる。)

これを「真偽テスト」というのは言い過ぎでないの?ってくらい多い。 ^^; バグを誘発しそうで怖い。。

 

Ruby は false に加えて nil のみ

Ruby はこれに対して、false に加え nil が偽と判定されるのみ。 Python 比べたらスッキリしている。ただし、Java で

if (null) {

なんて書いたらエラーになるので、これもやや違和感を覚えた。

しかし、Ruby で上記のような書き方は実際に多用されているのだろうか?いや、そもそも多用されていたら Null Object パターン に持ちこむか。 JavaScript では DOM の操作でよく見るけれど。

 

JavaScript の falsy

4873113911JavaScript の場合は、

以下に、条件式が falsy, つまり偽と評価される値を示す。

  • false
  • null
  • undefined
  • 空文字
  • 数値の 0
  • 数値の NaN

(JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス, p.14 より)

false ならぬ falsy という単語で述べられているように、Python に負けず劣らず 偽 と解釈される値が多い。 (@_@;

(ちなみに、最初 JavaScript 見て嫌だなと思ったのがこの真偽テストに絡んでいる。「え?この if で何を確認してるの?」と、馴染まない内はイラつかされるオブジェクトの存否チェック、そしてネストネストでズラズラズラ…。あ~、どこに本質的な仕事が書かれているんだろう?とコードの森で迷い。。)

総じて Script 系の言語は、さっと書くという利便性のために、「どこまでを if の検査で 偽 と見なすか」について設計思想が違うということかな。

 

型とは

4873112753話変わって、「型」とは何だっただろうか? 以前 にも引用したが 「データベース実践講義」 の 「2.3 型とは」 (p34) によると、

型とはいったい何か。基本的には、値の名前付き有限集合である。…

すべての型が、その型の値もしくは変数に作用する演算子の連想集合を持つ …

とのこと。

ある値の集合が型であり、その型に属する値に対して適用できる関数や操作が定義されるという見方。

 

Java のプリミティブ型としての boolean

Java に戻り、真偽値について確認してみると、

boolean は,リテラル true 及び false (3.10.3) で示す二つの可能な値をもつ論理的な量を表す。

(Java言語規定 型,値及び変数 の 4.2.5 boolean型 及び boolean値 より)

値の集合が定義され、適用できる論理演算子が言語仕様として示されている。ただし、プリミティブ型である true, false が Java 言語で書ける枠内で、型に属する値として定義されているわけでない。 &&, || のような論理演算子についても同様。 Java を使う側は概念的イメージとして boolean 型を意識し、それに即してコードを書くとコンパイラが型をチェックしてくれる。

 

C の真偽は数値

前述の通り、C言語では C99 より前は真偽値に対応する型は用意されてなかった。真偽値として見なしたい値は数値なので、数値に対する演算子が適用できてしまう。 boolean 型として扱うとプログラマが意識したところで、コンパイラにとっては区別不能。

 

Ruby の TrueClass , FalseClass

Ruby では、

Ruby の面白いのは、Boolean というクラスは無く、 true と false はそれぞれ TrueClass と FalseClass というクラスのインスタンスである。

(「各言語におけるtrue/falseまとめ - 床のトルストイ、ゲイとするとのこと」 より)

これ読むまで、てっきり Boolean というクラスがあると思っていた。 ^^;

確認のため TrueClass のスーパークラスとインクルードしているモジュールを表示させてみると、

p TrueClass.ancestors   #=> [TrueClass, Object, Kernel, BasicObject]

となり、true と false だけをまとめる型は定義されていない。つまり、Ruby の言語の枠内でブール型というものが独立して明示されていない。だから、 and や or のようなものが、

再定義できない演算子(制御構造)

(演算子式 - Rubyリファレンスマニュアル より)

として、何となく微妙な感じの位置付けになっているのかな?

 

Python の真偽は数値のサブタイプ

ついでなので Python についても調べたら、 3.2 標準型の階層

ブール型 (boolean)

… ブール型は整数のサブタイプで、ほとんどの演算コンテキストにおいてブール型値はそれぞれ 0 または 1 のように振舞います。ただし、文字列に変換されたときのみ、それぞれ文字列 "False" および "True" が返されます。

うげぇ~ (@_@; 整数のサブタイプだったとは。。

ということは、次の計算がエラーとならない。

print True + True + False #=> 2
print True * 10           #=> 10

上記のような計算を成立させることの積極的な意味は何だろう???

ともあれ、`0’ が if 文において 偽 と解釈される理由はこういう設計に依るのかも。どうしても ごちゃごちゃしているという感が否めない。 (+_+)

PEP 285 -- Adding a bool type では次のように述べられている。、

There's a small but vocal minority that would prefer to see
"textbook" bools that don't support arithmetic operations at
all, but most reviewers agree with me that bools should always
allow arithmetic operations.

6) Should bool inherit from int?

=> Yes.

ということで、この設計はこのままずっと行くみたい。

 

Haskell の Bool 型

これに比べて Haskell はシンプル。 Data.Bool に、型とその値の集合が次のように定義されており、

data Bool  = False  | True

Bool 型に適用できる関数が示されている。

(&&) :: Bool -> Bool -> Bool
(||) :: Bool -> Bool -> Bool
not  :: Bool -> Bool

そして、条件式 である if – then – else の検査では Bool 型しか許されない。 case 式と同等なものとして以下のことが成り立つようになっている。

if e 1 then e 2 else e 3 = case e 1 of { True -> e 2 ; False -> e 3 }

( The Haskell 98 Report: Expressions  3.6 Conditionals より)

先ほどの 「型とは何か?」 という視点から見ると、なんてスッキリとしているんだろう。 ^^ 

… とは言ったものの、最初は「何でこんなものが普通の関数と同じように定義されているの?」 と思ったけれど。 ^^;

 

型ごとに if の解釈を変えたいならオーバーロードで

もし、Python のように Bool 型に加え、空リスト、空文字、0 で False としたいなら、if-then-else とは異なる if’ 関数を定義し、それぞれの型でオーバーロードするのがいいかも。

if を関数で定義するとは、表計算で、

if (条件, 真の場合, 偽の場合)

のように使うときのイメージで。 (cf. Data.Bool.HT の if’ 関数)

 

まずは、値に適用したら Bool 型を返す isTrue 関数を持つ、型クラス Boolean を定義。

class Boolean b where
    isTrue :: b -> Bool

Bool, Integer, [a] 型を、型クラス Boolean のインスタンスにして、それぞれの型に応じた True , False を定義。

instance Boolean Bool where
    isTrue True  = True
    isTrue False = False

instance Boolean Integer where
    isTrue 0 = False
    isTrue _ = True

instance Boolean [a] where
    isTrue [] = False
    isTrue _  = True

これを使って if’ 関数を定義。 x の値が 真 と見なした値の場合 e1 が評価され、偽 の場合 e2 が評価される。

if' x e1 e2 = if isTrue x then e1 else e2

 

試しに使ってみる。数値の場合、

*Main> if' 0 "hoge" "piyo"
"piyo"
*Main> if' 1 "hoge" "piyo"
"hoge"

リストの場合、

*Main> if' [] "hoge" "piyo"
"piyo"
*Main> if' [1,2,3] "hoge" "piyo"
"hoge"
*Main> 

リストで定義しているので、文字列の場合でもちゃんと動作する。

*Main> if' "" "hoge" "piyo"
"piyo"
*Main> if' "test" "hoge" "piyo"
"hoge"

 

if’ を適用できる型を忘れてしまっても :i で調べられる

上記の if’ 関数の型を調べると、

*Main> :i if'
if' :: (Boolean b) => b -> t -> t -> t

もし、if’ 関数は何の型を 真・偽 と見なすか忘れてしまっても、 :i で 型クラス Boolean のインスタンスを表示できる。

*Main> :i Boolean
class Boolean b where t :: b -> Bool
instance Boolean Bool
instance Boolean Integer
instance Boolean [a]

 

if の解釈は所与のものであってほしくない

上記のように if – then – else は ブール型の検査のみで、それ以外の値を 真・偽 と見なして評価したいなら別腹で定義する方が好み。これなら評価の方法を自分の好きなように任意の型で定義できる。

Python でも同じように、クラスごとに真偽テストの挙動をカスタマイズできる。

以下の値は偽であると見なされます …

__nonzero__() または __len__() メソッドが定義されているようなユーザ定義クラスのインスタンスで、それらのメソッドが整数値ゼロまたは bool 値の False を返すとき

(2.3.1 真値テスト より)

(このメソッド名を見ても、あくまでも bool は int のサブタイプだぞ、覚えておけよ ! という気迫 を感じる。 ^^;)

結局のところ、if で型ごとに 真偽 が異なることは、オーバーロードのことだと解釈できる。これを言語が所与のものしてしまうと、「一体それはどういう訳なんだろう?」と疑問がわき、頭の中がごちゃごちゃになり、すぐに忘れるこの脳みそ。 (+_+)

 

Scheme

最近ちょっといじりはじめた Scheme ではどうなっているか調べたら、Structure and Interpretation of Computer Programs によると、

17 ``Interpreted as either true or false'' means this: In Scheme, there are two distinguished values that are denoted by the constants #t and #f . When the interpreter checks a predicate's value, it interprets #f as false. Any other value is treated as true. (Thus, providing #t is logically unnecessary, but it is convenient.)

真偽は #t, #f の定数として定義され、#f のみが偽で、それ以外は真。 #t は実質的にはいらないと。

うーーーん (@_@; 違和感を感じるなぁ~。

 

関連記事

 

参考サイト