何を変えようとしたのか — 0.1 + 0.2 == 0.3 が True になる言語
Raku では 0.1 + 0.2 == 0.3 が True を返す。小数リテラルが既定で有理数だからだ。この 1 行は、Perl 6 が何を優先したかをよく表している — 素直に書いたときに驚かないこと。シジル不変性、grammar、多重ディスパッチ、メタ演算子、Junction、遅延リスト。並べると分かる。どれも言語 1 つ分の仕事だった。15 年かかった理由の半分はここにある。
まず 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 すると分配結果がそのまま見え、if や so の真偽値文脈で初めて畳まれる。
量子力学の重ね合わせに喩えられる機能だ。
型システムにも影響していて、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 が現れる。 それは動くだけでなく、仕様と実装の関係そのものを作り替えた。