(rontolisp) docs

HTTPリクエスト(fetch)

rontolisp パッケージはJavaScriptの fetch APIをモデルにした外向きHTTPと、 それと自然に組み合わせられるJSON関数を提供します。いずれも Common Lisp の 一部ではないため、rontolisp: 修飾子で参照します (パッケージを参照)。rontolisp:fetch は リクエストを開始して即座に future を返します。それは rontolisp:await で解決します。future と await の仕組みそのものはHTTP固有ではなく — 非同期プログラミングガイドの主題です。このページはそれを前提と した上で、リクエストに固有の部分だけを扱います。

関数用途
rontolisp:fetchHTTPリクエストを開始する: (rontolisp:fetch url &optional options)
rontolisp:read-allレスポンスのボディストリームをひとつの文字列に読み尽くす (非同期)
rontolisp:json-parseJSON文字列をLispの値にパースする
rontolisp:json-stringifyLispの値をJSON文字列にシリアライズする

バックエンドのサポート。 インタプリタとJVMコンパイル済みクラスはJDKの java.net.http.HttpClient を使い、fetch が返った瞬間からリクエストは バックグラウンドスレッドで走ります。WASMでは fetch は代わりに通信を 行えるホストを必要とし、それは component (--component。非同期の wasi:http@0.3.0 をimportし、通常のフラグに加えて -S http=y を付けて 実行します) か、--host-fetch 付きでビルドした --no-wasi リアクタ のどちらかです。後者は同じソースを env.fetch (とレスポンスボディ用の env.readResponseBody) というimport経由でホスト自身のHTTPクライアントへ 落とします — Cloudflare Workerやnode埋め込みがfetchする仕組みがこれです (後述の節)。どちらでも ない場合、fetch はPreview 1 (コアモジュール) モードではコンパイルエラーに なります。ブラウザプレイグラウンド では本物のブラウザの fetch() が実行され (CORSの制約を受けます)、その間プログラムは続行 します。JSON関数は すべての バックエンド・すべてのWASMモードで動作 します。制限があるのは fetch 自体だけです。awaitfuturep、future コンビネータは非同期ガイドで扱います。

最初のリクエスト

fetch はリクエストが飛び始めたらすぐに返ります。future を rontolisp:await に渡すとレスポンスの到着までサスペンドし、結果の プロパティリスト (:status <integer> :headers <alist> :body <stream>) が 得られます — どのバックエンドでも :body はボディのオクテットの 非同期ストリーム (各チャンクは (unsigned-byte 8) ベクタ) で、 rontolisp:read-all でデコード済みの 1 つの文字列に読み尽くします:

個々のフィールドの読み取り:

fetch が返った時点でリクエストは既に走っているので、複数のリクエストは オーバーラップします — 全部開始してからそれぞれを (どの順番でも) await します。これは future の一般的なオーバーラップの 挙動そのものです:

リクエストのオプション

省略可能な第2引数はオプションのプロパティリストで、:method (文字列、デフォルト "GET")、:headers ((name . value) の文字列ペアの alist)、:body (文字列) を指定できます:

サポートされるメソッドは GETHEADPOSTPUTDELETEOPTIONSPATCH です。バックエンドごとのバリデーションのタイミングとエラー時の 挙動 (リクエストの失敗は fetch 時ではなく await 時に表面化します — どのバックエンドもそこでエラーをシグナルし、nil が返るのはリクエストを 開始すらできなかった場合だけです) は fetch のリファレンスページを参照してください。

JSONの扱い

rontolisp:json-parse はJSONドキュメントをLispの値に変換します。挙動は com.inuoe.jzon のデフォルトに従います: JSONオブジェクトは 文字列をキーとするハッシュテーブルに、配列はベクタになり、true/false/null はそれぞれ t/nil/シンボル null になります:

rontolisp:json-stringify はその逆です: ハッシュテーブルはオブジェクトに、 ベクタまたはリストは配列になり、nil/t/シンボル null はそれぞれ false/true/null になります:

どちらの関数もrontolisp自身で書かれていて、すべてのバックエンドで使われた ときにプログラムへコンパイルされます。またjzonの軽量なサブセットなので — プログラムはそのままjzonへ切り替えられます。値の対応表の全体とエッジケース (整数の桁数、キーの順序) は json-parsejson-stringify のリファレンスページにあります。

ハッシュテーブルを手で組み立てる — make-hash-table してからキーごとに setf gethash する — のが面倒なときは、よくあるリスト形式との相互変換を行う 4つのユーティリティが使えます。 rontolisp:plist-hash-tablerontolisp:alist-hash-table はプロパティリストや連想リストからハッシュテーブルを作り (:name のような キーワードキーは "name" に小文字化されます)、JSONオブジェクトをクオート リテラルから1つの式で書けます:

逆変換の rontolisp:hash-table-plistrontolisp:hash-table-alist は、パースしたオブジェクトを getfassoc で辿れるリストに平坦化します (パース結果のキーは文字列なので、assoc には :test 'equal を指定します):

いずれも同名の alexandria 関数の軽量なサブセットで、JSON関数と同様に すべてのバックエンドでプログラムにコンパイルされます。

完全なプログラム

これらの部品を組み合わせると、JSON APIの典型的な往復になります: json-stringify でリクエストボディを作り、POSTし、レスポンスをawaitして ボディを json-parse でパースします。以下を fetch-post.lisp として保存 してください:

200
{"name":"rontolisp","stars":1}

実行方法

インタプリタで:

rontolisp fetch-post.lisp

JVMクラスにコンパイルして (クラス名は出力ファイル名から付きます):

rontolisp fetch-post.lisp -o FetchPost.class
java FetchPost

WASM componentにコンパイルして (wasmtime 46+。外向きHTTPを許可する -S http=y に注意 — これがないと wasi:http のimportが提供されず、 インスタンス化に失敗します):

rontolisp fetch-post.lisp -o fetch-post.wasm --component
wasmtime run -S http=y fetch-post.wasm

リアクタからのfetch (--no-wasi --host-fetch)

--no-wasi リアクタはWASIを一切 importしないため、fetchを通す wasi:http を持ちません — しかしリアクタを 駆動するホスト (Cloudflare Worker、node、ブラウザページ) は自前のHTTP クライアントを持っています。--host-fetchrontolisp:fetch をそこへ、 2つの注入importとして経路付けします — リクエストとレスポンスのヘッドを運ぶ env.fetch(request-json) -> response-head-json と、ボディを運ぶ env.readResponseBody(ptr, cap) -> i32 です:

rontolisp worker.lisp -o worker.wasm --no-wasi --host-fetch --optimize=size

Lisp側は何も変わりません — オプションも (:status :headers :body) の答えも 同一で、:body は他と同じく非同期ストリームです — が、このバックエンドに 固有な点が3つあります:

  • fetchはトップレベルではなくエクスポートの中に置く。 リアクタには _start がありません: ホストがインスタンス化し、エクスポートされた関数を 呼びます。JavaScriptホストは env.fetchWebAssembly.Suspending (JSPI) で実装し、これはpromiseが確定するまでwasmスタック全体を停止させますが、 _initialize だけは停止できない唯一のスタックです — したがって ロードパス が到達するfetchはそこで拒否されます。ビルドはそれを名指しで 警告します。
  • ボディはヘッドの後から、1チャンクずつ届く。 env.fetch が答えるのは ステータスとヘッダで、オクテットは読み尽くしが要求するたびに env.readResponseBody 経由で、モジュールが渡すバッファへ引き込まれます (ホストは書き込んだ個数を答え、0 がストリーム終端です)。したがって大きな レスポンスがJSON文字列になることはなく、バイナリのレスポンスもオクテット のまま渡り、Workerは上流のストリーミングレスポンスをそのまま自分の クライアントへ転送できます。
  • 開始 == 確定。ただし確定するのはヘッド。 future は fetch が返った 時点で確定済みです (ヘッダまでの往復の間スタック全体が停止していたため)。 したがって await はサスペンドせず、2つのfetchが重なることもありません — Preview 1 が一貫して持つ退化した非同期の 形です。よって ヘッドよりの通信失敗は fetch の呼び出しで signal され、ボディ途中の 失敗は他のすべてのバックエンドと同様に読み尽くしで signal されます。生きて いるレスポンスボディは同時にひとつだけです: 前のボディを読み尽くす前に次の fetch を始めると、その読み尽くしは新しいレスポンスのオクテットを返す 代わりに signal します。 (--reentrant の下 では各レスポンスヘッドが自分の "body-id" を運び、すべての pull がそれを 名乗るため、レスポンスは独立に読み尽くされ、何も上書きされません。)

引き換えにホスト側がひとつ義務を負い、これもビルドが表示します: すべてのエクスポートを WebAssembly.promising 経由で呼び、呼び出しを直列化(または --reentrant でコンパイルして 1 インスタンス上でオーバーラップ) すること。サスペンドしたハンドラはイベントループに制御を返すため、2つ目の リクエストが同じインスタンスに入るとグローバルとアロケータを共有してしまい ます — モジュールは両方の呼び出しを壊す代わりに、その再入をトラップで拒否 します。同期的な env.fetch (JSPIのないnode、テストスタブ) にはこの義務は 不要で、それも同様に有効です。 --emit-js-glue を付ければ、その義務が JavaScript としてモジュールの隣に書き出されます — 2つのインポート、promising エントリ、キューまで — ホストに残るのは自分の fetch が何をするかだけです。

典型的な形は served なリアクタです: http-handlerClackアプリケーションを これらのフラグでコンパイルすると handle-request がエクスポートされ、その ハンドラがfetchします。 examples/cloudflare-workers/dog-fetcher がまさにそれで、JavaScript側も含まれています — インタプリタ・JVM・ wasi:http component でも動く、ひとつのソースです。

HTTPではなく素のTCPを使う場合 — あるいは サーバー 側を実装する場合 — は TCPソケットガイドを参照してください。