Rakuの異例な機能
(buttondown.com)- Rakuは完成度の高い単一設計というより、複数のアイデアを直接試す言語機能の実験室に近く、他の言語が採用し得る実験を一か所で見られる
- Junctions は可能な値の集合に演算を伝播させたあと、
all・any・oneのようなブール量化子に畳み込むことで、複雑な条件を短く表現する - Whatevers の
*は無限範囲、無名関数、リスト長ベースのインデックス指定に使われ、簡潔だが見慣れないコードスタイルを生み出す - Rakuの正規表現はPCRE互換を捨てる代わりに、合成可能な文法、値区切り子、読みやすいlookahead、デフォルトでの空白無視といった利便機能を提供する
- Hyperoperators、Pair Syntax、Slangs、RakuAST、Rakudo Starまで見ると、Rakuは文法実験から配布方式まで幅広く実験する言語だと分かる
Rakuが言語機能の実験室のように見える理由
- 1年以上実際にRakuを使ってみた結果、Rakuは「言語機能のための実験室」に近い言語に見える
- 5つの並行性モデルと、ある処理を行う複数の方法は、「何が起こるかを見るため」の実験的性格を示している
- 多くの機能が奇妙に相互作用し、エッジケースが多い理由も、100個の実験を実行することより、それらを磨き上げて一緒に動作させることのほうがはるかに難しいためだ
- ここで扱う機能は、他のプログラミング言語でもいつか見かけそうなRaku流の実験である
Junctions: 可能な値の重なりとブール量化子
- Junctions は可能な値の重なりを表し、junctionに演算を適用すると内部の値すべてに同じ演算が適用される
2|10はany(2, 10)になる2&10 + 3はall(5, 13)になる(1&2) + (10^20)はall(one(11, 21), one(12, 22))のように組み合わさる
- Junctionsの中心的な用途は、
allやanyのようなブール式を表現することにある- junctionを分解する方法はない
- 実際に使うには、まずブール値に畳み込む必要がある
soはjunctionをブール値に強制変換するso (1&2) + (10^20) < 15はTrueso (1&2) + (10^20) > 0はFalse
- Rakuの
list.allは引数を取るメソッドではなく、リストをjunctionに変換する引数なしメソッドである<1 2 3>.allはall(1, 2, 3)になるis-prime($x)を適用するとall(False, True, True)のように各値へ適用される
- Junctionsを組み合わせると
uniqueのような動作も作れる- 「すべての要素がちょうど1つの要素と等しいか」は
{.all == .one}で表現できる <1 2 3 7>は真で、<1 2 3 7 2>は偽になる
- 「すべての要素がちょうど1つの要素と等しいか」は
Whatevers: * で作る無限範囲と無名関数
- Whatevers の
*は、Rakuで複数の役割を担うwhateverシンボルである - 範囲やシーケンスで
*は境界がないことを意味する1..*は1..Inf(2,4,8...*)[17]は262144
*を含む式は無名関数へ昇格される- この動作は whatever-priming と呼ばれる
- 結果は
WhateverCodeで、型を除けば他の関数と区別はない (* + 10)(2)は12(^10).map(* % 2)は0 1 0 1 ...パターンを作る
- 式の中に
*が2つあると、それぞれ別個の位置引数になる(2, 30, 4, 50).map(* + *)は(32, 54)を返す- この動作は扱いの難しいFibonacci定義には便利だが、各
*が同じ値になる方式より優れているとまでは言い切れない
- Rakuの多くのメソッドは関数を受け取れるため、primingは広く使われる
l[*-1]は負のインデックスのように見えるが、実際には[]がリスト長を関数へ渡して計算されたインデックスを参照している- リスト長が10なら
l[*-1]はl[10-1]、つまり最後の要素になる l.head(2)は先頭2要素、l.head(*-2)は最後の2要素を除く全体を意味する
[]に別の関数を渡せば、リングバッファのような構造も簡単に実装できる@x[95 % *]--はリスト長を使って位置を計算する
正規表現: PCRE互換を捨てた合成可能な文法
- RakuはPOSIX正規表現やPCREとは異なる新しい正規表現構文を使う
- POSIX正規表現はバックスラッシュと句読点が多い構造である
- PCREはPOSIXとの下位互換を保ちつつ、より複雑なバックスラッシュ・句読点中心の構造を受け継いだ
- Perl 6は下位互換性を壊して新しい正規表現構文を導入した
- 最も目立つ改善点は 合成可能性 である
- 多くの言語では正規表現文字列を連結して結合する
- Rakuは
/< foo >+/のように別の正規表現を埋め込み、foo正規表現を1回以上マッチさせつつ、上位の正規表現へ漏れ出さないようにする
- 複雑な正規表現を、より小さく読みやすい部分に分割できる
- この方式は、Rakuが構文解析文法を組み込み言語機能としてサポートする基盤になっている
- 下位互換性を壊す一方で、複数の利便機能も追加されている
\d+ % ','は1、1,2、1,1,4にはマッチするが、1,や12にはマッチしない- lookaheadやnon-capturing groupは難解な記号の組み合わせではない
r1 && r2はr1とr2の両方を満たす文字列にだけマッチする:でバックトラッキングを止められる- 空白はデフォルトで無視され、マッチパターン内で明示的に有効化する必要がある
- Rakuの正規表現記法は、実際の処理機能とは別に、他言語でも登場しそうな機能群として見られる
Hyperoperators: ネストしたリストにも降りていく一般化マッピング
- Hyperoperators の基本形
l>>.methodはmapに似ているが、下位リストの中へ再帰的に降りていく[1, [2, 3], 4]>>.succは[2 [3 4] 5]
- Rakuでは
f(list, *args)形式の関数呼び出しをlist.&f(* args)というメソッド形式に変換できる- そのため
>>.は一般化されたマッピング演算子のように振る舞う - Whateverと組み合わせて
[1, [2, 3], 4]>>.&(*+1)のようにも使える
- そのため
- 二項hyperoperatorである
l1 << op >> l2は、2つのリストにopを要素ごとに適用する- 短いほうのリストは、長いほうが終わるまで繰り返される
[1, 2, 3, 4, 5] <<+>> [10, 20]は[11 22 13 24 15]<< op<<または>>op>>形式では、左側または右側のリストが終わるまでだけ繰り返す
- 単一値やネストしたリストにも動作する
[1, 2, 3, 4, 5] <<+>> 10は[11 12 13 14 15][1, [2, 3], 4, 5] <<+>> [10, 20]は[11 [22 23] 14 25]
- 2つのハッシュに対するhyperoperatorには、和集合・積集合・ハッシュ差に
opを適用する別の動作もある - Hyperoperatorという名前は、すべての演算を並列評価することに由来する
- 明示的なループも
hyper接頭辞で並列化できる
- 明示的なループも
Pair Syntax: ハッシュの基本単位と簡潔なフラグ指定
- Pair はRakuのハッシュを構成する基本単位である
- ハッシュは
key => valueの組の集まりである - pairが基本型で、hashはpairのコレクションである
- ハッシュは
- Rakuにはpairを短く書くcolon syntaxが数多くある
my $x = 3; :$xはx => 3:a<$x>はa => "$x":a($x)はa => 3:3aはa => 3
- 最も重要な省略形は
:keyと:!keyである- それぞれ
key => True、key => Falseに対応する - メソッドにフラグを渡す簡潔な方法として使われる
- それぞれ
matchメソッドは複数のcolon pairをフラグのように受け取る:continue(:$c)、:pos(:$p)、:global(:$g)、:overlap(:$ov)のような形を使う- シグネチャの
:f(:$foo)は$fooパラメータを定義しつつ、:fという別名も作る - 呼び出し側は
:fまたは:fooで同じフラグを設定できる
- Colon pair引数は順序や位置の自由度が高い
"abab".match(/../, :g)はグローバルマッチを行う"abab".match(/../, :g, :ov)は重なりマッチを行う"abab".match(:g:ov, /../)のように順序を変えたり連結して書いたりできる"abab".match(:g, /../):ov:2ndのように関数呼び出しの後ろへキーワード引数を置くこともできる
- こうした構文により、
match(global=True, overlap=True)よりずっと短くメソッドオプションを指定できる
Slangs, RakuAST, Rakudo Star
- Slangs はRakuの文法を変更する機能である
- ループ構文の変更、識別子の変更、actorの追加、DNAシーケンスの追加などに使える
- 例として Slang::Otherwise、Slang::Piersing、OO::Actors、BioInfo が挙げられている
- RakuAST はRaku式をRaku内部でASTとして構文解析する機能である
Q/my $x; $x++/.ASTは変数宣言とpostfix増分演算をRakuAST::StatementListの形で示すQ/my $x; put $x/.AST.DEPARSE("NL")のように、別言語風の表記でRakuコードを出力する例もある
- Rakuには Rakudo Star というインストール版がある
- このディストリビューションは、選定されたサードパーティモジュールをあらかじめインストールして提供する
- 大きな標準ライブラリの保守負担と、ユーザーがエコシステムから適切なパッケージを自力で探さなければならない負担との折衷案に近い
1件のコメント
Hacker Newsのコメント
以前ここで話した組み合わせ可能な正規表現機能に似たものを、JavaScript向けに単独で実装したのだが、数年たった今でもほかのプロジェクトへ最も頻繁に持ち込むライブラリの一つになっている
言語の中に第一級のパーサがあるような感覚に少し近づけるし、例としてこれを使って正規表現でメディアタイプのパースを実装したものがある: https://runkit.com/tolmasky/media-type-parsing-with-template...
npmの
templated-regular-expression、GitHub: https://github.com/tolmasky/templated-regular-expressionもちろんプログラミング言語には本物のパーサがあるべきで、正規表現をパーサとして使うべきではないが、どうせ正規表現を使うなら小さな部品に分けられるのは本当に良い
Rakuの正規表現はgrammarと組み合わせるとはるかに強力で、うまく書けばどんな「本物のパーサ」よりも理解しやすい。Rakuを一般的なパーサでパースするには、パース中にパーサへ項目を追加・削除できる必要があるが、Rakuのパーサは現在のgrammarをサブクラス化し、現在のレキシカルスコープ内で項目を追加・削除し、スコープが終わると以前のgrammarへ戻す方式でこれを処理している
Rakuでは正規表現はコードを書くためのもう一つの文法であり、基本文法と挙動が少し違うだけで、パラメータや変数を持てる。正規表現文法が合わない作業なら、通常のRaku文法を差し込んで必要なことを行い、その後また正規表現文法に戻れる
例えば、1つ以上の
Aの後に同じ数のBと同じ数のCをマッチさせることもできる。多くの正規表現文法では、同じ数のAとBをマッチさせるには途中で再帰が必要になるが、そうするとCまで同じやり方で扱うのは難しく、読むのもずっと大変になる。Raku方式ならバックトラックや再帰が不要なので、かなり高速に実行できるgrammarでまとめれば完全なパースツリーが得られ、grammarなしでも可能だが、grammarを使う方が簡単だ。実際のパーサ例としてはJSON::TINY::Grammarをよく勧める: https://github.com/moritz/json/blob/master/lib/JSON/Tiny/Gra...
正直なところ、「本物のパーサ」設計のかなりの部分は、初期のコンピュータの限られたRAMに由来する副産物に見える。別個のトークン化段階は、ソースコードが占めるRAMを減らし、その後の意味解析やコンパイルのためにメモリを残すためのものだったし、それ以降の段階をそれほど大きく単純化しているとは思わない。上のJSON::Tinyモジュールは、grammarがパースしている最中にアクションクラスでネイティブのRakuデータ構造を作るので、パースしながら同時にコンパイルしているようなものだ
Rakuを本番環境で使っている。テキスト処理には最高の言語で、パーサを作る体験がとても良い
これがLLMテキストパイプラインを作る最上位の言語でないのが不思議だ
うわ。Rakuのコードベースを保守しなければならない日が来る前に業界を去りたくなる
入門向けのRaku本を見ると、PythonにC系の構文を付けたもののように感じる部分が多い。波括弧中心の構文だが、使いやすさ、組み込みデータ構造、オブジェクト指向機能はいずれも高水準だ
ほかの高水準スクリプト言語を知っていれば、似たスクリプトのRakuコードはかなり簡単に読めると思う。日常的なコードで本当に特異な機能を多くの人が使うとは思えないし、Rakuはより柔軟ではあるが、普通のコードが難解に見えることはあまりない
Rakuでキーワード引数を省略形で定義できる機能、例えば
:global(:$g)や、True値を仮定してmatch(/foo/, :g)のようにグローバル正規表現マッチを呼び出せることにも、それが表れている。Perlにもこういうものは多く、どれもより速く楽しく使うためのものだが、初心者にはやや読みづらいこれから世代差になりそうだ。年長のプログラマー世代全体が、今の世代にとっては普通の作業環境で居場所を失ったように感じることになるかもしれない
いくつかの機能は半分くらい見慣れている。Hyperはrecursion-schemes方面の何かをもっと即興的にした版のように聞こえるし、ここで示された
*はScalaの_にある程度似ている_はラムダに使いやすいので、すべての言語が似たものを採用してほしい> (2, 30, 4, 50).map(* + *) returns (32, 45)はreturns (32, 54)であるべきでは? 2番目の項は4+50なのだからもしかすると、一部の国では英語のfifty-fourの代わりにvierenvijftigのように「four and fifty」式で言うことから来た見出し翻訳の結果かもしれない
(32 54)を返す。筆者の手が滑ったようだでは、今や Perl は APL 系言語への入門ドラッグということなのか?
例えば reduce メタ演算子
[ ]を中置+と組み合わせて、「すべてを合計」できる:[+] 1, 2, 3Raku は依然として 速度が大きな問題。正規表現でログファイルをパースするのは Perl の得意分野だが、最新の Raku でも起動時間を除いて Python 3.13 より 6.5 倍時間がかかる
(2,4,8...*)[17]が262144になるのは本当に驚いたWhateverを除けば、Perl6/Raku 以降の新しさは 遅延評価のほうにありそう(1,3,9...*)[4,5]は(81 243)になり、(1,3,9...*)[(1..3)]は(3 9 27)になる入れ子にもできて、
(1,2,4...*)[(1,2,4...*)[1,2,3]]のような式も可能Raku の機能のうち興味深いものを選んで取り上げたということだろう
最も重要な Raku の機能は コマンドラインインターフェースと 文法(grammar) だと思う
コマンドラインインターフェースのサポートは珍しい機能ではなく、例えば
docoptの実装や導入例がある。しかし Raku ではこの機能が組み込まれていて使いやすい一方で文法は、プログラミング言語が文法を 第一級の構成要素として提供し、オブジェクト指向プログラミングで文法を作成・合成できるようにしている点で珍しい機能だ
このプロジェクトを何年も見てきて興味深いとは思うが、Perl 6 がこの 鏡の国への旅であまりにも迷走してしまったようで、本当に残念だ