Perl の真偽値について

  • 7
    Like
  • 1
    Comment

Perl の真偽値はわかりづらいと言われます。

この記事では以下のような考察をしました。

Perl の真偽値は、C言語のそれを踏襲したものになっていることはすぐにわかります。Perlにおける真偽値の複雑さというのは、C言語にはない文字列(C言語のようなchar型へのポインタといったものではなくプリミティブなもの)やリストや未定義状態といったものに対するPerlの考えといったものが入ったからだと思えます。元々のC言語が真偽値型を導入せず整数型(int型)で真偽値型を代用した功罪をPerlも受け継いでいるといえるのではないでしょうか。

もう少し考察していきましょう。

Perl における偽と評価される値

Perl には真偽値型(ブール型)といったものはありません。Perl での真偽の違いはわかりづらいと言われますが、この記事で複雑に見えるルールを簡単にしていきます。

Perl で偽と評価されるのは以下。

  • 数値では0、および 0 と評価できる数値表現(数値リテラル)
    • 0.00
    • 0e2 ($0*10^2$ のこと)
    • 0x0 (16進数表現での0)
    • 計算の結果 0 となる数値計算式 (0.5 - 1/2sin(0) など)
    • などなど
  • 文字列では:空文字列 """0"
  • 未定義変数や未定義値
    • my などで宣言だけして代入をしなかった、または undef によって未定義にされた変数のこと
    • undef を含む、返り値を返さないサブルーチンの評価結果
  • 空リスト、空ハッシュ

もちろん、比較演算子で偽となる条件文(たとえば 2 > 3 )も当然そうですが、この記事では自明のものとして議論していません。

未定義「値」について

未定義変数は、多くの文献では「未定義値」 (undefined value) と説明されます。また Perl の公式ドキュメントでも undefined value という言葉は普通に使われており、「未定義値」という言葉は正式なものです。

とはいえ最近の私は「未定義というのは値ではなく状態のことで、未定義値と便宜的に読んでいるのは、まだ定義をしていない状態を指した言葉に過ぎない」という解釈をする場合があります。状態であって値でないのなら ==eq で値を比較することはできず、defined 演算子によって検査するのです。これは特殊な考えではなく、例えば SQL でいう NULL などと近い考えです。SQL の NULLIS NULL という構文で検査をします。

そうは言っても「未定義値」と値の一種であると扱うことは簡便でもあり、この記事でも誤解が無ければ「未定義値」という言葉を使います。

undef 関数(または単項演算子)はスカラー変数の左側に置くことで、そのスカラー変数を未定義状態に戻すことができます。

#!/usr/bin/perl
# __LINE__ キーワードでその行数を得ることができる

my $x = "foo";
print "\$x is " . ( defined $x ? "defined" : "undefined" ) . " at line " . __LINE__ . "\n"; # '$x is defined at line 5'
undef $x;
print "\$x is " . ( defined $x ? "defined" : "undefined" ) . " at line " . __LINE__ . "\n"; # '$x is undefined at 7'

もっとも、undef 自体が(左側単項演算子として右側に何かあってもなくても)何も返さない、つまり「未定義値」を返す(perldoc -f undef でも "Always returns the undefined value" と説明されています)ので、上記 undef $x$x = undef と書いても同様です。

#!/usr/bin/perl
# __LINE__ キーワードでその行数を得ることができる

my $x = "foo";
print "\$x is " . ( defined $x ? "defined" : "undefined" ) . " at line " . __LINE__ . "\n"; # '$x is defined at line 5'
$x = undef;
print "\$x is " . ( defined $x ? "defined" : "undefined" ) . " at line " . __LINE__ . "\n"; # '$x is undefined at 7'

この挙動は undef 関数だけでなく、「何も返さない」任意の関数に言えることです。

#!/usr/bin/perl

# 何も返さない関数を定義
sub nothing_return {}

my $x = "foo";
print "\$x is " . ( defined $x ? "defined" : "undefined" ) . " at line " . __LINE__ . "\n"; # '$x is defined at 7'
$x = nothing_return;
print "\$x is " . ( defined $x ? "defined" : "undefined" ) . " at line " . __LINE__ . "\n"; # '$x is undefined at 9'

もし「代入演算子 = の右側が何も返さなかった(未定義値を返した)場合、左側の変数の値は変わらない」という挙動だと、プログラムのバグ追跡がとても難しいものになるでしょうから、この挙動は理にかなっています。こういう部分も、未定義を値として扱うもっともらしい理由のように思えます。

上述の通り、未定義値は偽として扱われます。

空リストについて

上記で偽値として評価されると説明した空リスト。変数が値の入れ物であるなら、配列はリストの入れ物ともいえます。変数に未定義値があるなら、配列の場合は空リストがそれにあたると考えるのは自然です。

#!/usr/bin/perl

my $string = "foo";
$string = undef; # 未定義値の代入でスカラー変数を未定義に戻した

my @array = ("bar", "buz");
@array = (); # 空リストの代入で配列を未定義に戻した

上述の通り、空リストは偽として扱われます。

# unless は if の条件が逆バージョン
print "空リストは未定義\n" unless( () );

「持っているリストが空リスト」ともいえる空配列も上述の通り偽として扱われます。それは後述します。

Perl 内部の挙動をつぶさに観察すると、空配列が偽になる理由と空リストが偽になる理由は微妙に違っているように思えるのですが、マニアックな話題にも思えるので最後に付記します。

それぞれの型での真偽値

数値は 0 が偽

数値の偽値については、上述の通り 古典的なC言語を参考にした と考えるのが自然でしょう。

また Perl が生まれた時代に存在していた AWK もまた、数値 0 を偽として扱っていたことも、この言語仕様に結びついたのかもしれません。

#!/usr/bin/perl.

# unless は if の逆
print "0 は偽\n" unless 0;

評価すると結果的に数値の 0 となる式も同様です。

my $zerovalue1 = 0;
my $zerovalue2 = 0.00;
my $zerovalue3 = sin(0);
my $zerovalue4 = 0E0; # 0 * 10^0 = 0
my $zerovalue5 = 2 * 3 - 6;
my $zerovalue6 = 123456 * 0;

上記、全て右辺の計算結果は 0 なので、左辺のスカラー変数に入っている数値も 0 ですし、偽と評価されます。

空文字列が偽となる理解

上記で、数値においては 0 (および計算結果・評価結果が 0 となる式)が偽となることはC言語からの延長だと類推しました。とはいえ、Perlが生まれた頃の原始的なC言語には「文字列」という純粋な型はありません。文字列に見える例のアレは実際は char 型へのポインタです。

その他にPerlが生まれた当時あったものを考え合わせると、Perl が空文字列を偽として解釈するようにしたのは、ひとつは同じく文字列というデータ型を持っていた AWK がそうだったからというのが理由だと思います。

自然な発想として、リストの「存在しない」が空リスト、数値の「存在しない」が 0 だとすれば、文字列の「存在しない」は空文字列と考えるのは自然で、文字列というデータ型を独自に持つことにした AWK や Perl のこの言語仕様もまた自然に思えます。

"0" という文字列が偽となる理解

文字列(というか1文字)である "0" が偽とされる理由は、単純にPerlが数値列と文字列の厳密な区別がないからでしょう。

実はPerlの内部では、数値の 0 と文字の "0" の区別はついているのですが

$ perl -MDevel::Peek -E 'my $x = 0; say Dump($x)'
SV = IV(0x7fd76880e4a8) at 0x7fd76880e4b8
  REFCNT = 1
  FLAGS = (IOK,pIOK)
  IV = 0

$ perl -MDevel::Peek -E 'my $x = "0"; say Dump($x)'
SV = PV(0x7fdac8003e60) at 0x7fdac88254b8
  REFCNT = 1
  FLAGS = (POK,IsCOW,pPOK)
  PV = 0x7fdac7c039e0 "0"\0
  CUR = 1
  LEN = 10
  COW_REFCNT = 1

外部入力から取ってきたデータの場合、そこから必ず「数字」が取れるとしてもPerl内部では文字列「型」で扱われることになります。この時 0"0" を区別することにした上で 0 とは違って"0" を真とすることは、少し考えただけでも混乱の方が大きいでしょう。

0"0" を区別せずとも、Perlの数値に関わる二項演算子は、両辺が数値のように見える文字列「型」でも適切な数値にしてくれます。

$ perl -E 'say "1" + "2";'
3
$ ruby -e 'puts "1" + "2"'
12

オブジェクト指向言語は、両辺のオブジェクトが間にある二項演算子の振る舞いを決めるわけですが、Perlは間にある二項演算子が両辺のデータの扱いを決めるという部分は特徴的です。このPerlの特徴を、文脈に応じて「コンテキスト指向」と呼ぶ人もいます。

(Perlでもクラスとオブジェクトは定義でき、2つのオブジェクトの二項演算子として + などの動作を新たに定義することができます。overloadを参照)

確かに、このあたりがある種明快な後発のモダンなプログラミング言語の立場から「ややこしい」と言われてしまうと「ゴメンナサイ」としか言えません。また、オブジェクト指向言語が普及した現在、ある種その裏返しとも言える「コンテキスト指向」がわかりづらいのも無理はないでしょう。

ただ、C言語の表現力とAWKのような手軽さを求めたPerlの黎明期においては、このオブジェクト指向の裏返しのような「コンテキスト指向」にも手軽さがあったことでしょう。大規模開発では静的型付けが好まれるけれど、小規模であれば動的型付けでサッと書ける方が好まれるといった話に通じるものを感じます。

PHP、Ruby、JavaScript といったシェアの大きそうなLL言語の 0"0" は以下のようになっています。

JavaScript PHP Ruby
0
"0"

マニュアルを見ても PHP の真偽値は Perl の影響を受けているのが分かります。上記の Perl のような理解ができそうです。PHP も + は数値演算で、文字列連結をするときには . 二項演算子が登場するのも Perl に似ていますし、空の配列が偽と評価されるのも似ています。

Ruby は falsenil は偽、あとは真という明快なルールを設けることによって、真偽値による迷いを一掃しています。よって 0"0" も真です。

JavaScript はその中間というか、Perl っぽく 0 は偽であっても、"0" は偽というところが面白いです。JavaScript の文字列は定義方法の差はあれ( var x = "123";var x = new String("123"); か)、 typeof などで数値と簡単に区別可能だし、数値型へキャストするための parseInt が用意されていたりと、文字の "0" を特別扱いしなくても良いのも理解できます。

言語ごとの真偽値の思想については、もっともっと深掘りできそうですが、そろそろ Perl の話に戻ります。

配列、ハッシュの場合

配列の真偽値は配列が空であれば偽、そうでなければ(中身があれば)真となります。

これも、今までの「存在しない」が偽となる解釈だと自然な流れと言えましょう。

なお、「配列が空」というのは、宣言だけしてまだ何も代入されていない配列、または空リストの代入で中身を空にさせられた状態を言います。

my @array; # 宣言だけして代入をしていない空の配列
@array = ("foo", "bar", "buzz"); # 中身が3つ入った配列
@array = (); # 空リストを代入され、空となった配列

ハッシュも「キーとバリューの組が要素になっている偶数個の要素を持つ特殊な配列」だと考えると、配列と全く同じ真偽値の考え方で理解できます。もっとも、ハッシュ自体の真偽値を比較することは、配列のそれより稀なことだと思います。

リファレンスを含むオブジェクトはすべて真

Perl は Perl5 より「リファレンス」という参照データ構造があり、このリファレンスはすべて真と評価されます。

またリファレンスの応用として実現されているPerlのオブジェクトも原則すべて真として解釈されます。これは「全てのデータはオブジェクト」な Ruby が falsenil を除いてすべて真であることに似ています。

(上記で「原則」と書いたのは、Perlの「クラス定義」の中で、そのオブジェクトの真偽値評価を上書きすることができるためです。詳細は overload を参照)

スカラーコンテキストと配列の真偽値

空の配列が偽として解釈されるもう一つの視点として、スカラーコンテキストでの配列の評価値があります。

配列が文の中に置かれると、多くの場合、その配列が持っているリストを出すことになります。

my @array1 = ("ship", "airplain");
printf "海を %s で、空を %s で進む。\n", @array1;
# 実際は `printf "海を %s で、空を %s で進む。\n", "ship", "airplain";` と書いたかのようになる

Perl で配列が含む要素数を取得する際には、以下のどれかを使用します

  • スカラーコンテキストに配列を置く
  • scalar 関数(単項演算子)を使う

Perl には length という組み込み関数があるのですが、これは「文字列の長さを取得する関数」であって、配列に適用することはできない(適用しても見当違いの数字が帰ってくる)ことに注意です。Perlが生まれた当時、配列の要素数取得の意味でlengthキーワードを採用していた言語はまだ珍しかったのではないでしょうか。

上記のコードサンプルの各段階で要素数を調べてみます。

my @array;
printf "test1: %d\n", scalar @array;
@array = ("foo", "bar", "buzz");
printf "test2: %d\n", scalar @array;
@array = ();
printf "test3: %d\n", scalar @array;

この実行結果は

test1: 0
test2: 3
test3: 0

となります。

実は Perl には「スカラーコンテキスト」という、「この場所に配列を置いても配列の中身のリストを返すことはせず、配列の要素数の数字を返す」という文法上の場所があります。

my @array1 = ("ship", "airplain");
my @array2 = ("bus", "train", "taxi");

my $array1_size = @array1; # `$var =` の右辺はスカラーコンテキスト
my $sum = @array1 + @array2; # `+` の両辺はスカラーコンテキスト
printf "%d\n", $sum; # 5が表示される

スカラーコンテキストはそれほど多くなく、多くの場合は「配列を置くと配列の中身のリストを返す」という「リストコンテキスト」です。本来はリストコンテキストの場所だけど、スカラーコンテキストで得られる評価値を強制したいときに使うのが scalar であり、配列をスカラーコンテキストで評価すると要素数が出てくるので必要に応じて scalar を使うというわけです。

実は if (...)while (...) の丸括弧の中など、真偽値を要求する部分もスカラーコンテキストで評価されます。

  • 実際に内部的には狭義のスカラーコンテキストである真偽値コンテキストであるのですが、スカラーコンテキストであると理解してもほぼ差し支えないので、上記のように書いています。組み込み関数 wantarray では両コンテキストの違いは区別できませんが、 Contextual::Returnなどで区別することができます

なので、 my @array1 = ("ship", "airplain"); としたとき if (@array1) というのは配列がスカラーコンテキストに置かれてその要素数が評価され、結果的に if(2) を評価しているのと同じことが起こっていると考えると、中身があれば真、中身がない空の配列であれば偽(スカラーコンテキストで 0 となるから)ということが自ずとわかります。

Perl初学者に時々あるのですが、未定義値1つのみを持つ以下の配列

my @array = (undef);

が偽と評価されてしまうという勘違いがあります。

実際は「未定義値」が1つ要素としてあるので要素数は1であり

my @array = (undef);
printf "要素数は %d\n", scalar @array; # => 要素数は 1

上記のスカラーコンテキストや配列の要素数の理解があれば、この配列自身は真であることが分かります(もちろん $array[0] は偽です)。

ちなみに my @array = (undef, undef, undef); であれば、この配列の要素数は scalar @array であり 3 です。 undef を「間引いて」しまうと、自作関数の戻り値をリストの形にする際

my @array = (func1(), func2(), func3());

もし自作関数のどれかが未定義値を返したためにこの要素数が3でなくなったとしたら、いったい何が間引かれたのか不安ですよね。

スカラーコンテキストとリストの真偽値

空配列が偽であることと同じ類推で、空リストが偽であることが理解できるでしょうか。

上述の通り、真偽値評価は狭義のスカラーコンテキストである真偽値コンテキストで行われるので、実際に空リストをスカラーコンテキストで評価してみると、未定義値が返ることがわかります。

#!/usr/bin/perl

# 空リストは未定義と表示される
print "空リストは未定義\n" if !defined scalar(());

前節で「配列はスカラーコンテキストで評価すると要素数が返るので、空配列をスカラーコンテキストで評価すると 0 となるので偽」だったのですが、空リストの場合は 0 ではなく未定義値となりました。

実は、リストをスカラーコンテキストで評価すると、リストの末尾の値が返るというPerlの仕様があります。「リストをスカラーコンテキストで評価する」という言葉自体でワケワカですが…。

my $value = ("foo", "bar", "buz");
print "$value\n"; # => buz

この仕様は、Perlの上級者が初めて知って驚愕するという仕様の一つでもあり、Perlのコンテキストへの批判の元となっている仕様の一つのようです。もっとも、「リストをスカラーコンテキストで評価」することはほぼ無いので、この仕様を知って驚愕することになるのはPerlを長年書いた中上級者なのかもしれません。

つまり、空リストの場合はリストの末尾の値というものもないのでそれは未定義値であり、スカラーコンテキストで空リストを評価すると未定義値となるということのようです。

奥が深いのか業が深いのか :sweat_smile:

この仕様が採用された理由は過去何度か聞いたのですが(そして忘れている…)、一つの理由として「リストの区切りのカンマをセミコロンの代用とする」だった気がします。

my $line = (chomp, s/#.*//, $_); # 末尾改行と行途中からのコメントを削除したものを $line に入れる

もっとも、現代の Perl には do { ... } などもあるので

my $line = do { chomp; s/#.*//; $_; };

と書くなどしたほうが良さそうです。

余談:値 "0E0" (0 but true) について

サブルーチンやメソッドの中には、0 も含めた数値の返り値を真と解釈すべき意味のあるものとし、異常などを表すときに未定義値を返すものがあります。

そのようなサブルーチンやメソッドは、 0 の代わりに "0E0" という文字列を返す場合があります。"0E0" は文字列として見たら "0" ではないので真であり、スカラーコンテキスト・真偽値コンテキストでも真ですが、 + 二項演算子などで数値として扱おうとすると 0 と同じ意味となる表現の一つです。

有名な使用例として DBI などがあります。

通常のPerlプログラミングでは使われないものです。

余談:文字列 "0" を数値 0 に戻す

上述での Perl の "0" の考察の中で、Perl 内部では 0"0" の区別はついていることを Devel::Peek で例示しました。

$ perl -MDevel::Peek -E 'my $x = 0; say Dump($x)'
SV = IV(0x7fd76880e4a8) at 0x7fd76880e4b8
  REFCNT = 1
  FLAGS = (IOK,pIOK)
  IV = 0

$ perl -MDevel::Peek -E 'my $x = "0"; say Dump($x)'
SV = PV(0x7fdac8003e60) at 0x7fdac88254b8
  REFCNT = 1
  FLAGS = (POK,IsCOW,pPOK)
  PV = 0x7fdac7c039e0 "0"\0
  CUR = 1
  LEN = 10
  COW_REFCNT = 1

上記 IV が Perl 内部で Integer Value と呼ばれている純粋な数値を表すものです(あまりこのあたり詳しくないので不正確かもしれません)。

外部入力から取ってきたデータの場合、そこから必ず「数字」が取れるとしてもPerl内部では文字列「型」で扱われることになります。この時 0"0" を区別することにした上で 0 とは違って"0" を真とすることは、少し考えただけでも混乱の方が大きいでしょう。

JavaScript は parseInt があるけれど、Perl には…という話もしましたが、実は内部的な文字列を数値にする方法もあります。

$ perl -MDevel::Peek -E 'my $x = "0"; $x += 0; say Dump($x)'
SV = PVIV(0x7f96ff80be20) at 0x7f96ff80e4a0
  REFCNT = 1
  FLAGS = (IOK,pIOK)
  IV = 0
  PV = 0

両辺に数値を強制する二項演算子 + を使って、 $x + 0 を評価するという技。

通常のPerlのプログラミングでこれが出てくる場面は完全に無いと言ってよく、これを JavaScript の parseInt のように使用を求めるのはあまりにも無理がありすぎます。話のネタ程度でしょう。

(数値計算をするときに多少のパフォーマンスアップするくらいでしょうか)

まとめ

各種真偽値を考察したことで、偽となる値を少し整理できそうです。

  • 未定義、未定義変数
    • 空リストは未定義値と同じ扱い
  • 数値 0
    • C言語からの伝統?
    • 計算結果が 0 になる数値計算(sin(0), 5 - 2.5 * 2 など
    • 評価結果が 0 になる数値リテラル (0.00, 0x0, 0E0 など)
    • 空配列や空ハッシュは真偽値コンテキストで 0 として評価されるので偽
  • 文字列 "0"
    • 言語仕様的に 0 と区別をつけるのが難しいから
    • "0E0" "0.00""0" と等しくない文字列は全て真
  • 空文字列 ""
    • 未定義値や数値 0 などの「存在しない」感からの文字列への類推、または AWK からの借用か

その他、特殊なオブジェクトの真偽値の取り扱いなどに気をつければ良さそうですが、そういう場面は稀でしょう。

根底の仕様を垣間見ることで演繹的に考えて個別に覚えることが減るのは数学のようです。たまには深掘りして考える心がけをすることで、基礎力も付きそうですね。私もPerl以外はさっぱりわからず、頑張ります。