1 ポイント 投稿者 GN⁺ 2024-11-14 | 1件のコメント | WhatsAppで共有
  • Rakuは完成度の高い単一設計というより、複数のアイデアを直接試す言語機能の実験室に近く、他の言語が採用し得る実験を一か所で見られる
  • Junctions は可能な値の集合に演算を伝播させたあと、allanyone のようなブール量化子に畳み込むことで、複雑な条件を短く表現する
  • Whatevers* は無限範囲、無名関数、リスト長ベースのインデックス指定に使われ、簡潔だが見慣れないコードスタイルを生み出す
  • Rakuの正規表現はPCRE互換を捨てる代わりに、合成可能な文法、値区切り子、読みやすいlookahead、デフォルトでの空白無視といった利便機能を提供する
  • Hyperoperators、Pair Syntax、Slangs、RakuAST、Rakudo Starまで見ると、Rakuは文法実験から配布方式まで幅広く実験する言語だと分かる

Rakuが言語機能の実験室のように見える理由

  • 1年以上実際にRakuを使ってみた結果、Rakuは「言語機能のための実験室」に近い言語に見える
  • 5つの並行性モデルと、ある処理を行う複数の方法は、「何が起こるかを見るため」の実験的性格を示している
  • 多くの機能が奇妙に相互作用し、エッジケースが多い理由も、100個の実験を実行することより、それらを磨き上げて一緒に動作させることのほうがはるかに難しいためだ
  • ここで扱う機能は、他のプログラミング言語でもいつか見かけそうなRaku流の実験である

Junctions: 可能な値の重なりとブール量化子

  • Junctions可能な値の重なりを表し、junctionに演算を適用すると内部の値すべてに同じ演算が適用される
    • 2|10any(2, 10) になる
    • 2&10 + 3all(5, 13) になる
    • (1&2) + (10^20)all(one(11, 21), one(12, 22)) のように組み合わさる
  • Junctionsの中心的な用途は、allany のようなブール式を表現することにある
    • junctionを分解する方法はない
    • 実際に使うには、まずブール値に畳み込む必要がある
  • so はjunctionをブール値に強制変換する
    • so (1&2) + (10^20) < 15True
    • so (1&2) + (10^20) > 0False
  • Rakuの list.all は引数を取るメソッドではなく、リストをjunctionに変換する引数なしメソッドである
    • <1 2 3>.allall(1, 2, 3) になる
    • is-prime($x) を適用すると all(False, True, True) のように各値へ適用される
  • Junctionsを組み合わせると unique のような動作も作れる
    • 「すべての要素がちょうど1つの要素と等しいか」は {.all == .one} で表現できる
    • <1 2 3 7> は真で、<1 2 3 7 2> は偽になる

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+ % ','11,21,1,4 にはマッチするが、1,12 にはマッチしない
    • lookaheadやnon-capturing groupは難解な記号の組み合わせではない
    • r1 && r2r1r2 の両方を満たす文字列にだけマッチする
    • : でバックトラッキングを止められる
    • 空白はデフォルトで無視され、マッチパターン内で明示的に有効化する必要がある
  • Rakuの正規表現記法は、実際の処理機能とは別に、他言語でも登場しそうな機能群として見られる

Hyperoperators: ネストしたリストにも降りていく一般化マッピング

  • Hyperoperators の基本形 l>>.methodmap に似ているが、下位リストの中へ再帰的に降りていく
    • [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; :$xx => 3
    • :a<$x>a => "$x"
    • :a($x)a => 3
    • :3aa => 3
  • 最も重要な省略形は :key:!key である
    • それぞれ key => Truekey => 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の文法を変更する機能である
  • 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件のコメント

 
GN⁺ 2024-11-14
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をパースするには十分強力ではない
      Rakuの正規表現はgrammarと組み合わせるとはるかに強力で、うまく書けばどんな「本物のパーサ」よりも理解しやすい。Rakuを一般的なパーサでパースするには、パース中にパーサへ項目を追加・削除できる必要があるが、Rakuのパーサは現在のgrammarをサブクラス化し、現在のレキシカルスコープ内で項目を追加・削除し、スコープが終わると以前のgrammarへ戻す方式でこれを処理している
      Rakuでは正規表現はコードを書くためのもう一つの文法であり、基本文法と挙動が少し違うだけで、パラメータや変数を持てる。正規表現文法が合わない作業なら、通常のRaku文法を差し込んで必要なことを行い、その後また正規表現文法に戻れる
      例えば、1つ以上のAの後に同じ数のBと同じ数のCをマッチさせることもできる。多くの正規表現文法では、同じ数のABをマッチさせるには途中で再帰が必要になるが、そうすると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でHTTPS APIを呼び出し、特定の値を返すまでポーリングする良い例を知っているか気になっている
    • RakuのLLMパッケージを使っているのか、使っているならどれを使っているのか気になる
  • うわ。Rakuのコードベースを保守しなければならない日が来る前に業界を去りたくなる

    • 面白いことに、そのブログ記事を読んだら仕事を辞めてRakuチームを探して働きたくなった。まだあまりに世間知らずなのかも :)
    • 一般的なRakuコードを見たことがないなら、記事を見てそういう反応になるのも分かる
      入門向けのRaku本を見ると、PythonにC系の構文を付けたもののように感じる部分が多い。波括弧中心の構文だが、使いやすさ、組み込みデータ構造、オブジェクト指向機能はいずれも高水準だ
      ほかの高水準スクリプト言語を知っていれば、似たスクリプトのRakuコードはかなり簡単に読めると思う。日常的なコードで本当に特異な機能を多くの人が使うとは思えないし、Rakuはより柔軟ではあるが、普通のコードが難解に見えることはあまりない
    • Perlに似ている。誰も保守したがらないが、使うのは本当に楽しい。表現力が非常に高い
      Rakuでキーワード引数を省略形で定義できる機能、例えば:global(:$g)や、True値を仮定してmatch(/foo/, :g)のようにグローバル正規表現マッチを呼び出せることにも、それが表れている。Perlにもこういうものは多く、どれもより速く楽しく使うためのものだが、初心者にはやや読みづらい
    • 人々がAI支援で書かれたコードベースの保守についても似たようなことを言っているのは奇妙だ
      これから世代差になりそうだ。年長のプログラマー世代全体が、今の世代にとっては普通の作業環境で居場所を失ったように感じることになるかもしれない
    • Rakuが「業界」のために意図された言語だとは思わない
  • いくつかの機能は半分くらい見慣れている。Hyperはrecursion-schemes方面の何かをもっと即興的にした版のように聞こえるし、ここで示された*はScalaの_にある程度似ている
    _はラムダに使いやすいので、すべての言語が似たものを採用してほしい

  • > (2, 30, 4, 50).map(* + *) returns (32, 45)returns (32, 54)であるべきでは? 2番目の項は4+50なのだから
    もしかすると、一部の国では英語のfifty-fourの代わりにvierenvijftigのように「four and fifty」式で言うことから来た見出し翻訳の結果かもしれない

    • rakudoで確認してみたところ、実際には(32 54)を返す。筆者の手が滑ったようだ
  • では、今や Perl は APL 系言語への入門ドラッグということなのか?

    • その通り。しかも記事では メタ演算子にも触れていない
      例えば reduce メタ演算子 [ ] を中置 + と組み合わせて、「すべてを合計」できる: [+] 1, 2, 3
  • Raku は依然として 速度が大きな問題。正規表現でログファイルをパースするのは Perl の得意分野だが、最新の Raku でも起動時間を除いて Python 3.13 より 6.5 倍時間がかかる

    • 例も一緒に示すべき。自分の経験では Raku のほうが速いものも遅いものもあるので、何で遅いのかを見ずに「Raku は Python 3.13 より 6.5 倍時間がかかる」とだけ言っても、あまり意味がない
  • (2,4,8...*)[17]262144 になるのは本当に驚いた

    • 自分の頭にはすぐに「ひどい」という言葉が浮かび、続いて「ひどい結果」と 停止性問題が思い浮かんだが、好みは人それぞれだろう
    • 増分の検出は Perl6 には昔からあったので新機能ではない
      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 がこの 鏡の国への旅であまりにも迷走してしまったようで、本当に残念だ

    • Patrick Michaud が最近どのように、何をしているのか気になる