何を変えようとしたのか — 0.1 + 0.2 == 0.3 が True になる言語

Raku では 0.1 + 0.2 == 0.3 が True を返す。小数リテラルが既定で有理数だからだ。この 1 行は、Perl 6 が何を優先したかをよく表している — 素直に書いたときに驚かないこと。シジル不変性、grammar、多重ディスパッチ、メタ演算子、Junction、遅延リスト。並べると分かる。どれも言語 1 つ分の仕事だった。15 年かかった理由の半分はここにある。

perlrakulanguage-designtype-systemoperatorsprogramming-languages

まず 1 行

say 0.1 + 0.2 == 0.3;   # True

多くの言語で False になるこの式が、Raku では True を返す。

理由は単純で、**Raku の小数リテラルは既定で有理数(Rat)**だからだ。 0.1 は「1/10 に最も近い倍精度浮動小数点数」ではなく、分数の 1/10 そのものとして保持される。 浮動小数点が必要なら Num を明示する。

この判断には代償がある。有理数の演算は浮動小数点より遅い。分母が大きくなれば精度の管理も要る。 それでも既定をこちらにしたのは、素直に書いたときに驚かないことを優先したからだ。

この 1 行が、Perl 6 の設計全体の縮図になっている。 正しさを、実装コストで買う。今回はその買い物の一覧である。

1. シジル不変性 — 最も有名な非互換

Perl 5 では、変数のシジルがアクセスの仕方によって変わる

my @array = (1, 2, 3);
my $first = $array[0];      # @ ではなく $
my @slice = @array[0, 1];   # ここは @

規則としては一貫している。「シジルは取り出される値の個数を表す」。 しかし、初学者が最もつまずく点でもあった。

Perl 6 は逆の規則を選ぶ。

my @array = 1, 2, 3;
my $first = @array[0];      # @ のまま

「シジルは変数の型であって、文脈ではない」。

たった 1 つの規則変更だが、これで Perl 5 のコードは文法レベルで動かなくなる。 互換性を切るという決断が、ここに集約されている。

2. twigil — シジルの次の一文字

Perl 6 はシジルの後ろにもう 1 文字置ける。変数の種類を綴りで示す仕組みだ。

twigil 意味
! クラスの属性(private) $!name
. 属性のアクセサ経由 $.name
* 動的変数(呼び出し側から辿る) $*IN, $*CWD
? コンパイル時定数 $?FILE, $?LINE
^ 自動宣言される位置パラメータ { $^a + $^b }

見ただけでスコープの種類が分かる。 Perl 5 の local(動的スコープ)が $* として整理されたのが、いちばん分かりやすい例だ。

3. OO を言語に入れる

第 2 話で書いたとおり、Perl 5 の OO は「既存部品の組み合わせ」だった。 bless は関数であって、new は慣習である。

Perl 6 は class / has / method / role言語機能として持つ。

class Point {
    has $.x = 0;
    has $.y = 0;
    method to-string { "($!x, $!y)" }
}

注目すべきは role だ。多重継承の代替として導入されたもので、 Smalltalk の Traits 研究に由来する。状態と振る舞いを合成可能な単位にまとめ、 衝突はコンパイル時に検出される

そしてこの role とオブジェクトモデルは、完成を待たずに Perl 5 へ逆流した。 Moose である。 第 2 話で触れたが、Moose は Perl 6 のオブジェクトモデルの移植だった。 Perl 6 の設計は、それ自体が出荷される前に Perl 5 を変えていた。

4. 型を書けるようにする(ただし任意)

sub add(Int $a, Int $b --> Int) { $a + $b }

書かなくてもよいが、書けば実行時に検査される。漸進的型付けだ。

さらに subset で述語つきの型が作れる。

subset Even of Int where * %% 2;
subset Positive of Numeric where * > 0;

sub half(Even $n) { $n div 2 }
half(4);   # OK
half(3);   # 型制約エラー

where 節に任意の述語が書ける。 バリデーションを関数の入口ではなく、型の定義側に置ける。

第 1 話で挙げた「簡単なことは簡単に、難しいことは可能に」の、型版にあたる。

5. 多重ディスパッチ

同じ名前の関数を、引数の型・個数・制約で選び分ける。

multi greet(Str $name)          { "Hello, $name" }
multi greet(Int $times)         { "Hi " x $times }
multi greet(Str $name, Int $n)  { "Hello, $name" x $n }
multi greet($x where * < 0)     { "Negative!" }

Java や C++ のオーバーロードが静的な型で解決されるのに対し、 Raku は実行時の値の型で解決する。Common Lisp の CLOS に近い。

候補が複数マッチするときは、より限定的な候補が勝つwhere 制約つきの候補は、型だけの候補より限定的として扱われる。

6. 演算子をメタに扱う

Perl 6 の特徴的な発明がメタ演算子だ。演算子を受け取って、演算子を作る。

メタ演算子 意味
reduce [ ] [+] 1..10 畳み込み。55
hyper »« @a »+» 1 各要素に適用
cross X @a X @b 直積
zip Z @a Z @b 対応づけ
negate ! !== 否定版を自動生成
assign = min= 代入版を自動生成

演算子を N 個足すのではなく、演算子を作る規則を足す。

ここには Perl 5 への反省がある。Perl 5 は演算子を追加する方向で膨らんだ。 第 3 話で書いた「RFC は『足したい』ばかり集まる」という偏りへの、設計側からの答えでもある。

ここに、この連載で 4 つ目の「型」がある。

足す対象を、機能から規則へ引き上げる。 機能は足すほど増えるが、規則は足しても増えない。

7. Junction — 値の重ね合わせ

my $x = 2;
my @list = 1, 2, 3;

if $x == any(@list) { say 'どれかと等しい' }    # 出力される
if $x >  all(@list) { say 'すべてより大きい' }  # 出力されない

say    $x == any(@list);   # any(False, True, False) — 分配された結果がそのまま見える
say so $x == any(@list);   # True — 真偽値文脈で畳まれる

any / all / one / noneであって、比較すると自動的に分配される。 say すると分配結果がそのまま見え、ifso の真偽値文脈で初めて畳まれる。 量子力学の重ね合わせに喩えられる機能だ。

型システムにも影響していて、Junction は Any の下にいない。 だから「普通の型」を受け取る関数に Junction を渡すと、自動的に分配が起きる。 言語の型階層に、この機能のための場所が確保されている。

8. 遅延リストと無限列

my @fib = 1, 1, * + * ... *;   # 無限のフィボナッチ
say @fib[^10];                  # (1 1 2 3 5 8 13 21 34 55)

... は列演算子、末尾の *(Whatever)は「無限に」を表す。 リストが既定で遅延評価されるため、無限列を変数に入れて、後から必要なだけ取れる。

9. 文法そのものを変えられる

Raku のパーサは Raku の grammar として書かれている。 そしてユーザがコンパイル時に文法を拡張できる

sub infix:<∈>($x, @set) { $x (elem) @set }
say 2 ∈ (1, 2, 3);     # True

infix:<...> / prefix:<...> / postfix:<...> という名前で演算子を定義でき、 結合性と優先順位も指定できる。定義した瞬間、それは本当に構文になる。

grammar そのものについては第 11 話で扱う。Raku で最も他の言語に無いものだ。

並べてみて分かること

ここまで 9 項目を並べた。改めて見てほしい。

  • 有理数を既定にする — 数値タワーの設計
  • シジル不変性 — 言語の見た目の全面変更
  • OO を言語に入れる — オブジェクトモデルと MOP の設計
  • 漸進的型付け — 型システムの設計
  • 多重ディスパッチ — ディスパッチ機構の設計
  • メタ演算子 — 演算子体系の設計
  • Junction — 型階層への新しい住人の追加
  • 遅延リスト — 評価戦略の変更
  • 文法拡張 — パーサをユーザに開く

どれか 1 つでも、言語 1 つ分の仕事である。

しかも互いに独立ではない。Junction は型階層に影響し、 遅延評価は多重ディスパッチの解決に影響し、文法拡張は全部に影響する。 組み合わせの数だけ、決めなければならないことがある。

第 3 話の最後に「15 年かかった理由は 4 つある」と書いた。その 1 つ目がこれだ。

設計が壮大すぎた。

これは怠慢の話ではない。やろうとしたことの量の話である。 そして、やろうとしたことは概ね実現した。15 年かけて。

次回からは、それを実際に動かそうとした人たちの話に移る。 まず、5 年近く「動くもの」が無かったという事実から。


次回(第 5 話): 実装が無かった 4 年半と、Pugs の 1 年。 2005 年 2 月、Haskell で書かれた Perl 6 が現れる。 それは動くだけでなく、仕様と実装の関係そのものを作り替えた。

← Back to Perl と Raku の系譜