2010年1月14日木曜日

被演算子の一つを返す AND, OR - デフォルト値の設定

if と真偽値 の続き。

 

Python のブール演算における例外的なこと

前回見たように、Python では if 文においてたくさんの値が `偽’ と見なされる。ただし、 ドキュメント の最後には次のように書かれている。

ブール値の結果を返す演算および組み込み関数は、特に注釈のない限り常に偽値として 0 またはFalse を返し、真値として 1 または True を返します (重要な例外: ブール演算 "or" および "and" は常に被演算子の中の一つを返します)。

(2.3.1 真値テスト より、太字は引用者による。もしくは 6. Built-in Types — Python v2.6.4 documentation を参照)

例えば、組み込み関数である all, any なら、

l = range(10,100)
print all(map(lambda x: x >= 10, l)) #=> True
print any(map(lambda x: x < 10, l))  #=> False

というようにブール値を返す。では、「重要な例外」と書かれている or, and は、どういう意味で重要なのだろう?なぜ素直にブール値を返さないのか?

ちなみにこれが Java なら、

条件AND式の型は,常に boolean とする。…

条件OR式の型は,常に boolean とする。

(Java言語規定 式 より)

のように必ずブール値を返す。

 

if 文における and, or

偽 と解釈される値の場合

普通、and 演算子は次のように if  と組み合わせて使用される。

x = "hoge"; y = "piyo"

if x != "" and y != "":
    print x, y

上記を実行すると、”hoge piyo” と出力。

ところで、Python の文字列は、if の検査において、空文字 でそれ以外は 真 として扱われた。よって、先ほどの if による空文字かどうかの検査の部分、

if x != "" and y != "":

を次のように書換えることができる。

if x and y:

この一見すると奇妙で意味を汲みにくい書き方を素直に読むなら、

もし x かつ y ならば…

となり、最初に書いた

もし x が空文字であり、かつ、 y も空文字であるならば…

に比べると曖昧に見える。

 

評価のされ方を確認

上記のように の値であることを想定して書いたときの、if 文と and, or 演算子における値の評価を確認してみることに。最初はちょっと奇妙に思えるが、以下のように文字列に and , or 演算子を適用。

print "hoge" and "piyo" and "fuga"  #=> “fuga”
print "hoge" or  "piyo" or  "fuga"  #=> “hoge”

最初見たドキュメントの通り、被演算子の中の一つが返されているのがわかる。このルールは、5.10 ブール演算 (boolean operation) によると、

x and y は、まず x を評価します; x が偽なら、x の値を返します; それ以外の場合には、 y の値を評価し、その結果を返します。

x or y は、まず x を評価します; x が真なら、x の値を返します; それ以外の場合には、 y の値を評価し、その結果を返します。

(andnot も、返す値を 01 に制限するのではなく、最後に評価した引数の値を返すので注意してください。

(太字は引用者による)

これに従うと、

if x and y:

の例では、次のような評価の流れとなる。

  1. and 演算の評価
    • x は空文字でないので真
    • y は空文字でないので真
    • y の値を返す
  2. if の評価
    • and 演算子の結果は空文字でないので真

 

JavaScript で || によってデフォルト値を設定する方法

4873113911ところで、Python に負けず劣らず と解釈される値が多い JavaScript では、Python の or に相当するのは ||JavaScript: The Good Parts (pp.24-25) には、この || 演算子を使った「デフォルト値」を設定する方法が書かれている。

例えば、`名前と年齢’ を対にしたハッシュを定義し、ハッシュの要素ではないものを参照しようとしたとする。

var persons = {"Tarou" : 10, "Jirou" : 20, "Hanako" : 30};
var age = persons["Saburou"] || 0;

|| 0 と書くことにより、 age には 0 が代入される。先ほどの Python の or と同じルールを思い出しながら評価の流れを考えると、

  1. persons[“Saburou”] に相当するものがないので undefined が返される。
  2. undefined は 偽 と見なされる。
  3. || 演算子のルールに従い、|| の右辺の 0 が評価され、0 が返される。

 

Python でも or でデフォルト値を設定

Python でも同じようにして、デフォルト値を設定できる。

a = 0
print a or 100 #=> 100

ん~ (@_@; 一見意味がわかりずらい。。

このようにデフォルト値の設定を or 演算子でできるのは、

  1. 偽 と見なされる値がいくつかあり、
  2. or の結果がブール値ではなく評価した値そのものを返している

ことによる。

 

ところで、どうしても短く書きたいなら、条件式 を使った方が意図が明確になる。
print a if a else 100

いや、正直言うとこの書き方も好きではない。

print a if a != 0 else 100

頭が堅いのかな?こうでないと今一安心できない ^^;

 

Ruby でも || でデフォルト値を設定

Ruby の場合は、nil が 偽 と見なされるので、次のように書ける。

a = nil
p a || 100 #=> 100

しかし、Python や JavaScript の癖が抜けずに、

a = 0
p a || 100 #=> 0

と書くと、意図したものと違う結果になるので注意。

 

ある一つの機能は、一つのことだけのために限ってほしい

結局、言語によって 偽 と見なす値が異なることと、and , or のような演算子がブール値でなく、評価した値のいずれかを返すという仕様が、自分のようなすぐに忘れる脳みそにとってはどうしても好きになれない。 (+_+) そもそもブール演算に用いるものを、デフォルト値の設定に使うという発想が自分はダメ。道具は用途を限り、それにのみ使えるようにしておかないと、脳みその容量が少ないので混乱してしまう。 一時慣れたとしても、しばらく使ってないと忘れる自信ならたっぷりある。(o_ _)o~†

加えて、演算子が場合に応じて異なる型の値を返すということも居心地が悪い。 (+_+)  「返すならブール型だけで勘弁してください」と言いたい気分。異なる値を返すなら、特定のインターフェイスを実装している型、もしくは、Haskell で言うなら同じ型クラスのインスタンスのみに限定してほしい。以前、少しの手間を惜しみ、ある関数で異る型の値を返すようにしたことがある。一時的の`つもり’で書いたのだけれど、それを忘れ、後になって見つけづらいバグになってしまった。 もちろん、「ちゃんとテストコード書いておけよ」という話なんだけれど。

とにかく、ポカミスを連発し、ミスをしてないという思い込み、エラーの発見が苦手な自分のようなタイプにとって、例外的なことは本能的に避けたくなる。多分、そうでない人にとっては、便利さとのトレードオフが考慮された例外的な事柄は便利だと感じるのだろうなぁ。

 

追記 (2010.1.16) : Google Python スタイルガイド では、True/False の評価について次のように書かれている。

可能な場合は、非明示的な false を利用する。…

利点:

Python のブール値を使うと、可読性が高まり、エラーを防ぐことができます。さらにほとんどの場合で、これは高速です。

うーん、可読性高まるのかなぁ~ (@_@; 

 

Haskell で似た関数を定義する

Haskell で同じようにデフォルト値を設定しようと、次のように書くと、

a = 0
b = a || 100

|| が適用する型が一致しないので、当然エラーとなる。どうしても上記のようなデフォルト値を設定する演算子が使いたいのなら、|| とは別に定義する。

デフォルト値を設定する演算子を ||| で定義するなら、

(|||) x d = if x == 0 then d else x

となる。これを使って、

a = 0
b = a ||| 100  -- b は 100

ただし、これでは数値にしか使えない。

 

JavaScript の or に相当する ||| 関数

前回、Haskell で Python のように型ごとに if の解釈を変えるためにオーバーロード を利用した。コードは以下の通り。

class Boolean b where
    isTrue :: b -> Bool

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 isTrue x then e1 else e2

それぞれの型でオーバーロードした if’ 関数は 型クラス Boolean のインスタンスに適用できる。これを元に if’ を定義したときと同じように考える。

(|||) e1 e2 = if isTrue e1 then e1 else e2

… あれ? ||| と if’ の定義は形がよく似ている。こんな冗長なコードを書いていたらだめか。。

if’ を使って書き直すと、

(|||) e1 e2 = if' e1 e1 e2

引数を省略して以下のように書けるけれど、これだと意図がわかりずらいかな。 (@_@;

(|||) e1 = if' e1 e1

試してみる。

*Main> 0 ||| 100
100
*Main> 1 ||| 100
1
*Main> [] ||| [1,2,3]
[1,2,3]
*Main> [1,2] ||| [1,2,3]
[1,2]
*Main> "" ||| "hoge"
"hoge"
*Main> "piyo" ||| "piyo"
"piyo"

 

ついでに Python の and に相当する &&& も定義する。

(&&&) e1 e2 = if' e1 e2 e1

if’ , &&&, ||| を組み合わせて試すと、

*Main> if' ("hoge" &&& "piyo" &&& "fuga") 1 0
1
*Main> if' ("hoge" &&& "piyo" &&& "" &&& "fuga") 1 0
0
*Main> if' (1 &&& 2 &&& 3) 1 0
1
*Main> if' (1 &&& 2 &&& 0 &&& 3) 1 0
0
*Main> if' ("hoge" ||| "" ||| "piyo") 1 0
1
*Main> if' ("" ||| "") 1 0
0
*Main> if' (1 ||| 0 ||| 2) 1 0
1
*Main> if' (0 ||| 0) 1 0
0

全体のコードはこちら

 

Ruby でも同様に

Haskell では if を関数として定義し、型ごとにオーバーロードし、and, or に相当する関数を定義した。同じように Python でもできないかと考えたが、Python は組込クラスに手を出せない。 (+_+) それに対して、Ruby は組込クラスにメソッドを追加することができる。 (cf. リストと文字列を透過的に扱うには ) そこで、Ruby で同じように書けるか試してみることに。

Ruby では偽と解釈される値は false 以外に nil だけだった。これを Python と同じように Array, String, Integer でもそれぞれ偽と解釈される値を想定し、 if をメソッドとし実装。それに伴い and, or メソッドも追加し、or メソッドでデフォルトの値を設定できるようにする。

作成するモジュールとクラスは以下の通り。

img01-13-2010[1].png

Control モジュールで if メソッドを定義。これを使って、Bool モジュールで and, or メソッドを実装。イテレータを実装するときのようにテンプレートメソッドパターンを使って、各々のクラスで値による真偽のルールを記述する。

 

実装

まずは Control と Bool モジュールから。 Control モジュールの if メソッドは、真の値のときに評価されるブロック e1 と、偽の値のときに評価されるブロック e2 を受けとり、if メソッドが呼出されたオブジェクトの値に応じて e1 もしくは e2 を返す。 if_ メソッドは、その場で評価に応じてブロックを実行するようにした。 isTrue? と呼出しているメソッドは、このモジュールをインクルードする組込クラスで定義する。

Bool モジュールは Control モジュールで定義した if メソッドを使って and, or メソッドを定義。

module Control
  def if(e1, e2)
     if self.isTrue? then e1 else e2 end
  end
  def if_(e1, e2)
    self.if(e1, e2).call
  end  
end

module Bool
  include Control
  def and(other)
    self.if(other, self)
  end
  def or(other)
    self.if(self, other)
  end
end

後は、組込クラスで isTrue? メソッドを実装し、真偽と解釈される値の範囲を設定。

class TrueClass
  include Bool
  def isTrue?
    if self == true then true else false end
  end
end

class FalseClass
  include Bool
  def isTrue?
    if self != false then true else false end
  end
end

class NilClass
  include Bool
  def isTrue?
    if self != nil then true else false end
  end
end

class String
  include Bool
  def isTrue?
    if self != "" then true else false end
  end
end

class Integer
  include Bool
  def isTrue?
    if self != 0 then true else false end
  end
end

class Array
  include Bool
  def isTrue?
    if not self.empty? then true else false end
  end
end

まずは if と if_  メソッドを試してみる。変数 x, y に false, nil, "", 0, [] に入れて同じ動作をするか確認。if メソッドにおいて、オブジェクトの値によって設定した真偽が決定し、実行するブロックが決まる。

x = 0
y = 1

x.if(proc{ p "T" },
     proc{ p "F"}).call

x.if_(proc{ 
        p "T" 
        y.if_(proc{ p "TT" }, 
              proc{ p "FF" })
      },
      proc{ 
        p "F"
        y.if_(proc{ p "FT"}, 
              proc{ p "FF"})
      })

次に and, or を試す。

p true.and true                 #=> true          p 1.and 2.and 3                 #=> 3
p "hoge".and "piyo".and "fuga"  #=> “fuga”

p "hoge".or "piyo".or "fuga"    #=> “hoge”
p "".or "hoge"                  #=> “hoge”

ダックタイピングな Ruby なので、こんなのも書けてしまうか。(+_+)

p false.or 0.or "".or [].or 1000

もう何がなんだかわからなくなってきた。まぁ、これが書けたからどうというわけでもなく、あくまでもこんな風にも記述できるという実験ということで。 ^^;

コード全体はこちら

 

Scheme の and , or はどうなの?

そういえば、Scheme でも見かけは同じような動作をする。

(and 1 2 3)                  ;=> 3
(and "hoge" "piyo" "fuga")   ;=> “fuga”
(or 1 2 3)                   ;=> 1
(or "hoge" "piyo" "fuga")    ;=> “hoge”

Structure and Interpretation of Computer Programs によると、

  • (and <e1> ... <en>)

    The interpreter evaluates the expressions <e> one at a time, in left-to-right order. If any <e> evaluates to false, the value of the and expression is false, and the rest of the <e>'s are not evaluated. If all <e>'s evaluate to true values, the value of the and expression is the value of the last one.

  • (or <e1> ... <en>)

    The interpreter evaluates the expressions <e> one at a time, in left-to-right order. If any <e> evaluates to a true value, that value is returned as the value of the or expression, and the rest of the <e>'s are not evaluated. If all <e>'s evaluate to false, the value of the or expression is false.

    (太字は引用者による)

  • こういった評価の方法をするものを special form と言うらしいが、

    Each special form has its own evaluation rule. …

    the evaluation rule for expressions can be described by a simple general rule together with specialized rules for a small number of special forms. …

    Special syntactic forms that are simply convenient alternative surface structures for things that can be written in more uniform ways are sometimes called syntactic sugar,

    (Structure and Interpretation of Computer Programs より)

    Scheme 入門 15. 構文の定義 によると、

    マクロとは式の変換です。

    or, and はマクロで、以下のように再帰的に定義されています。マクロ定義も再帰的に定義できるので、かなり複雑な構文を定義することができます。

    Scheme はまだ全然わからないので、これは保留。ただし、式の評価方法という観点から、他の言語とは違うメタレベルを導入して定義してあるような気が。「ブール型だからこうだよ」というように型の視点からではなく。

     

    参考