- Janet は小さな Lisp 方言だが、命令型言語・第一級関数・単一識別子名前空間・レキシカルなブロックスコープを備えており、シンプルに始められる
- コア言語 は
do, def, var, set, if, while, break, fn の8つの命令だけで構成され、マクロがより強力または便利な制御フローラッパーを作ってくれる
- 配布性 は、Janet プログラムを Janet ランタイムと静的リンクしたネイティブ実行ファイルにコンパイルし、利用者に Janet や依存関係のインストールを求めない形で確保される
- Parsing Expression Grammar (PEG) は正規表現より単純で強力かつ予測可能で、sh はパイプやリダイレクトを Janet 内で表現できるようにして CLI 作成の幅を広げる
- コンパイル時実行 はトップレベル命令を先に実行した後、プログラム状態のスナップショットをディスクに保存することで、マクロなしでも実行時へ値・共有参照・ジェネレーター・クロージャ状態を渡せるようにする
シンプルなコア
- Janet は命令型言語であり、第一級関数、単一識別子名前空間、レキシカルなブロックスコープを持つ
- 言語コアは
do, def, var, set, if, while, break, fn の8つの命令で小さく保たれている
- マクロによって、より強力または便利な高水準の制御フローラッパーが可能になる
- ランタイムの意味論はなじみやすく、標準ライブラリ 全体が1ページに収まるほど、言語の残りの部分も小さい
ネイティブ配布と埋め込み
- Janet プログラムは、Janet ランタイムを静的リンクしたネイティブ実行ファイルへ簡単にコンパイルできる
- 配布を受ける利用者は、Janet、プロジェクト依存関係、そのほかの追加コンポーネントをインストールする必要がない
- Janet は自身をバイトコードへコンパイルした後、そのバイトコードを Janet ランタイムを起動する
.c ファイル内に書き込み、システムの C コンパイラでその C ファイルをコンパイルする
- シンプルな “hello world” ネイティブバイナリは 1MB より小さく、Janet 1.27.0 の aarch64 macOS では 784K だった
- このバイナリには Janet ランタイム全体、ガベージコレクタ、バイトコードコンパイラまで含まれるため、実行時に Janet コードを評価するプログラムも作れる
- Janet ランタイムは小さな C ライブラリなので、リンクした後に通常の C 関数を呼び出して Janet の値を操作できる
- Web サイトにも埋め込め、Toodle のようにユーザー定義のプログラマブル DSL を持つ静的サイトを作れる
テキスト解析とサブプロセス DSL
- Janet のテキスト処理は正規表現ではなく Parsing Expression Grammar を基盤としている
- Parsing Expression Grammar は正規表現より単純で強力かつ予測可能で、行単位に縛られないため複数行テキストも解析できる
- HTML、JSON、そのほかの非正規言語を解析でき、任意の null バイトを含むバイナリファイル形式も扱える
- sh は、パイプやリダイレクトを Janet コード内で直接表現できるサードパーティ製シェルスクリプティング DSL である
($ find . -name *.janet | say)
- この DSL は Janet を、Perl の妥当な代替から、かなり広い範囲のプログラムに対する Bash の妥当な代替へと押し上げる
コレクションと構文感覚
- Janet のコレクション型には可変と不変の両方の形がある
- 不変コレクションは値セマンティクスを持つため、不変ベクタ
[1 2] はメモリアドレスが異なっても (take 2 [1 2 3]) と区別されない
- 可変コレクションは参照セマンティクスを持つため、ハッシュテーブル
@{:x 1 :y 2} は自分自身とのみ等しく、同じキーと値を持つ別のハッシュテーブルは別オブジェクトである
- 構文では括弧を広く使うが、リストには
[]、テーブルには {} を使って形を分けている
- 可変リテラルには
@"mutable string" のように常に @ 接頭辞を付ける
- 匿名関数は
(fn [x] (+ 1 x)) と書き、|(+ 1 $) のように | で式を関数へ持ち上げる短縮記法もある
- スプラットまたはスプレッドは
; を使って (+ ;args) のように表す
- バッククォート文字列は任意の数のバッククォートで開き、同じ数で閉じられ、バッククォート文字列内では
\n のようなエスケープシーケンスは適用されない
- 残余引数は
. ではなく & を使って (defn foo [first & rest] ...) のように書く
- Janet は reader macro をサポートしないため構文自体が固定されており、Janet を読めればすべての Janet プログラムを読める
マクロとコンパイル時状態
- Janet マクロはコードを書くコードであり、コンパイル時に値と抽象構文木を操作する現在の実行フローと、将来実行されるアプリケーションコードのフローを同時に扱うことになる
- Janet マクロは hygienic ではなく、関数用の別名前空間もない
- ただしリテラル関数を unquote できるため、完全に参照透過なマクロを書ける
- Janet プログラムをコンパイルすると、トップレベル命令、通常の文、関数宣言などを先に実行した後、プログラム状態のスナップショットをディスクへ記録する
- このスナップショットは共有参照を保持するため、再起動後も可変値を継続して変更できる
- ジェネレーターは次に再開したときに実行する命令を記憶し、クロージャも閉包した値を保持する
- マクロはコンパイル時コード実行の特殊な形だが、マクロなしでもこの能力を使える
- ゲームではスプラインを事前処理でき、コンパイル時にファイルを読んで最終バイナリへアセットを入れられ、任意の副作用も実行できる
- Janet for Mortals は SQL スキーマファイルを基にデータベースバインディングを自動生成する例を示しており、この種の作業は多くの言語ではかなり難しいと評価している
Lisp 伝統よりも快適さ
- Janet は古い Lisp の慣習をそのまま踏襲していない
CAR は first、PROGN は do、LAMBDA は fn、SETQ は def と名付けられている
nil は空リストではなく独立した型であり、ブール値は第一級の値である
EQ, EQL, EQUAL, EQUALP 系を避けており、連結リストもほとんど見かけない
2件のコメント
Hacker Newsのコメント
Janetには物足りない点がある。主にパッケージ管理でのバージョン指定が弱く、高度なHTTPルーティングのようなライブラリ群も不足している。
それでも、JPMでバイナリやスクリプトを作れて移植性が高いのは本当に気に入っている。以前、概念実証としてPlaydateゲーム機にJanetプログラミング言語を載せてみたこともある。
Janetでコードを書くのは楽しいが、そのたびに人から自分がこの言語の作者だと思われるのはちょっと困る
「JanetがJanetを書く」版もやってみると良さそう
依存関係をベンダリングし、jpmなしでもモダンなJanetバンドルを簡単にインストールできるようにしてくれる
LLM活用に前向きなら、ラッパーはLLMに書かせて、実際のロジックはJanetで書ける
同じ開発者がもっと前に作った似た言語として Fennel もある。Luaにコンパイルされ、実装もすべてLuaで書かれている。
独自の標準ライブラリがないため、Janetのパーサライブラリのような優れたものが多く欠けているが、Luaを組み込んだ環境でスクリプトを書くには向いている。
https://fennel-lang.org/
FennelとLua VMの間の接続は非常に脆く、JanetのデバッガやREPLの品質には到底及ばない。Fennelは移植性がはるかに高く、LuaJITのおかげでSBCLを圧倒できる可能性すらあるだけに残念だ。
ただ、トランスパイル体験が完全に足を引っ張っていると思う。回避策はあるが、
debug.setinfoを実装してもmatchブロックのような厄介な境界ケースに出くわす。LuaJIT2をforkして、デバッグとエラー構造を言語透過性にもっと合うよう修正することには大きな価値があると思う。そうなればFennelのような言語はずっと魅力的に見えるはずだ
記事の筆者は、以前Hacker Newsでも取り上げられたこれらのツールを Janet で作っている。
https://bauble.studio
https://toodle.studio
この2つの興味深いアートツールのおかげで、しばらくJanetにかなり期待していた
Janetが注目されるのを見ると、いつも嬉しくなる。モダンな機能の1つとして sandbox を挙げたい。
「インタープリタが特定のシステムリソースを利用できないように機能セットを無効化する。いったん無効化した機能を再び有効にする方法はない。」
https://janet-lang.org/api/misc.html#sandbox
「SETQ is def」を見て、最初は思わず声に出して「え?」と言ってしまった。SETQは束縛を作らず、更新しかしないからだ。
ドキュメント(https://janet-lang.org/docs/bindings.html)を読むと、実際に筆者のほうが間違っていて、「defで作られた束縛は不変」と書かれている。たぶん「SETQ is set」と言いたかったのだろう。
JanetはGuile、Tcl、CLのちょうど良い中間点のように見えて、本当に好きになりたいのだが、ラムダや制御フロー演算子に角括弧ベクタを使うことに本能的な抵抗がある。Clojureも同様で、どうしても受け入れにくいが、十分に慣れればいけるのかもしれない。
それと、現在のLSP/SLIMEの状況がどうなっているのかも気になる。最近ではかなり重要だ
丸括弧を使うと、リストの先頭要素が残りのリストをどう解釈するかを決める。たとえば
(func a b c)は関数実行、(macro x y z)はマクロ展開、([p q r] …)は引数ベクタで始まり、その後に評価式が続く「裸の」関数本体になる。角括弧は、要素が同じ「種類」で、先頭要素が特別ではないときに使う。たとえば
(defn f [a b c] …)は同種の引数の集まりで、先頭の引数が特別ではないし、(let [a 1 b 2] …)も束縛の集まりで、先頭の束縛が特別ではない。思いつく唯一の例外は、
caseで複数のマッチ要素をまとめる場合だが、これは利便性のためだ。この理屈を理解してから考えが変わり、その後は美しいと感じるようになった[1 2 3]の代わりに(array 1 2 3)を使えばいいし、(fn [x] (+ 1 x))の代わりに(f (x) (+ 1 x))を使えばいい。必須ではない
一定の長さを超えるシステムスクリプトでは、私にとって Janet が sh, Python, awk などの代わりになっている
スクリプトの起動時間が非常に速く、私のシステムでは hyperfine で 1.4ms であり、dash の 1ms に近い。コンパイル済み実行ファイルではなく、スクリプトとしての話だ
sh-dslモジュールのおかげで、($ cmda w x | cmdb y z)のようにシェルコマンドをとてもエレガントに書ける。デバッグのためにイメージをロードできる機能も大いに役立つごく最近使い始めたばかりだが、すでに一番好きな言語の一つになりそうで、これまで使った他の Lisp は SICP 用の MIT Scheme だけだ
この記事は新鮮だ。インターネットの AI 以前の議論 の匂いが残っている
新しい言語、新しい文法、何年もコードを書いてきた人たちの激しい議論がある。誰か AI が許可されないオンラインコミュニティを始めてくれたらいいのにと思う
実際にはその一つ前のリローンチと言うべきかもしれず、今はまた新しいホームページがあるようだ。オンラインコミュニティで AI を安定して防ぐ方法を最初に見つけた人は、おそらく大金持ちになる可能性が高い
https://www.techspot.com/news/111698-digg-relaunch-fails-two...
ある種の「人間性の証明」は難問だ
運営が定めた正確なルールは「意味のある人間の著作」だったが、惑わされてはいけない。lobsters には LLM にイデオロギー的に反対している人が多い。技術がどれだけ「意味のある形で」使われたかはあまり重要ではない
私の作業は AI が触れたというだけでゴミ扱いされ、AI を使っていると話したとき、露出狂やフェティシスト呼ばわりする人までいた。参加を考えている人には前もって伝えておきたい
「リテラル関数をアンコートできるようにして、Janet は完全に参照透過なマクロを書けるようにする」といった文章に、Lisp の人たちは本当に抽象的なものに興奮するようだ
街の普通の人にこんなことを言ったら、たぶん逃げ出したくなるだろう
#define MULTIPLY(x, y) x * yint result = MULTIPLY(2 + 3, 4); // 14ある言葉の意味を知らないからといって、それが悪いわけではない。文脈を見ると、たぶんそういう意味で言ったのだと思う
プログラミングで繰り返し現れるパターンや問題について共有言語を持つのは良いことだ。「あのプログラマー集団は変だ」みたいに用語を揶揄するのは無意味で、逆効果でしかない
再開しようか考えているが、ニッチな機能が実装する価値のあるものか悩んでいる。私には実装も難しい。
dynamic-unwindを飛ばし、もしかするとcall/ccも外して、その代わりデバッグ性、エコシステム、性能、パッケージ管理に集中したほうがいいのかもしれないだから「コンピュータ関係の仕事をしています」くらいにかなり曖昧に言うか、「あまり面白い仕事じゃないんです」と言って話題を変えようとする。もう少し具体的に言うだけで、人は出口を探し始める
正直、Lisp 系コミュニティは小さかったからこそ、かえって得をした面があると思う。たとえば大昔の Design Patterns ですら継承より合成を好むべきだと警告していたのに、オブジェクト指向プログラマーたちは今でも 15 階層の深い階層構造を作っていた
Janet に初めて触れたとき、これらのドキュメントが本当に役立った
https://janetdocs.org/tutorials
https://janet.guide/ 筆者が作ったものだ
HN にたまに上がる Janet の記事に惹かれていたが、みんなが高く評価する Janet for Mortals はまったく mortals 向けの本には感じられなかった
他の言語と比べても Janet は本当に学びやすい部類なので、その本が難しいというのは驚きだ。本自体は読んでいないが、言語にはある程度慣れていて、正直褒めることしかない
Janet は Lisp 2.0 のように見えるので、文法も Lisp 的だ
Lobste.rs の意見
Janet を使い始めて 10か月で、APL 系言語以外はほとんど忘れてしまうほど夢中になり、コミュニティのドキュメントサイトを運営しながらチュートリアルも書いている
始めて 3 週間以内に個人用スクリプトをすべて書き直し、新しく作る運用ソフトウェアも Janet で書いている
Janet は実装上、言語全体がほぼ ハッシュマップのように動作するので、
(keys (curenv))でローカルシンボルを、(keys (getproto (curenv)))でコアシンボルを確認できるし、その気になればハッシュマップベースで CLOS に似たものも作れ、実装例もあるJoy Web Framework で 20 個前後の Web サイトと複数のサービスを 512MB の無料 VPS 1 台で動かしており、関連するチュートリアルも書いている
ただし「不変コレクション」という表現は実態とはやや異なり、標準ライブラリは概して可変値を返すため、現時点では不変性にこだわる大きな理由はない
コンパイル時の値を実行時に渡す機能が特に強力だった。たとえば聖書の
.tsvをコンパイル時にハッシュマップとしてバイナリへ埋め込んでおけば、実行時は参照だけで済み、embedを使った Go 版より2 倍高速という結果も出たGo で同じものを手作業でハッシュマップとして実装するとずっと長くなるが、Janet なら Lisp to Go コンパイラも 46 行で作れた
Ian Henry が挙げたさらに興味深い点は、Janet が クロージャの状態をイメージ/セッション間で保持することだ。
(curenv)ハッシュマップに関連環境を保存しておき、新しい REPL セッションで復元してもクロージャ内部の状態が継続するLisp ベースの音楽 DSL である https://lisp.trane.studio/ もあり、論文 https://dl.acm.org/doi/abs/10.1145/3677996.3678285 と結果例 https://x.com/greg_ash/status/1824218993118388708 も見る価値がある
自作ライブラリもあり、複数のデータ構造上で SQL ライクなクエリ構文を提供している
データフレームの挿入・更新・CSV 保存/読み込みをサポートし、Datalog と miniKanren も含み、APL のようなベクトル化演算も可能だ
Janet 上で J を直接使う jnj もあり、Joy Web Framework には
(var account (db/find-by :account :where {:login (auth-result :login)}))のような DB クエリ DSL があり、実際の Web サイトの認証コードでも使われている「不変コレクション」と言うと永続データ構造を連想するが、それは有用だとしても Janet の基本機能ではない
実際に気に入っているのは値型と参照型の対称性で、記事の最後に「immutable composite values」とは書いてあったものの、自分が書いたのでなければ意味をすぐには汲み取れなかったと思う
Janet は新鮮で 埋め込み可能な言語 なので、ゲームエンジンのようなプロジェクトの組み込みスクリプティングに使ってみたい
DLL を差し替えずに高速反復のためのホットリロードが必要な場面によく合いそうで、Lua も優れているが Janet のほうがある面ではより表現力が高そうだ
Janet は言語として本当に魅力的で、いつか Zig プロジェクトの スクリプト言語 として使ってみたい。より多くの人が Janet に言及しているのはうれしい
良さそうではあるが、すでに babashka で Clojure スクリプティングに慣れてきているので、似た印象を受ける。埋め込み可能性 以外に見落としている大きな利点があるのか気になる
「残りの引数を含む配列の分割代入が潜在的に高コストなコピーを引き起こす」といった点は、全体的にあまり関数型らしくなく見えて好みではない
他の言語でテキストをパースするたびに恋しくなる機能だ
Janet は小さくて埋め込み可能な Clojure というより、関数型プログラミング支援がより良い Lua に近い