(->> read write unlearn)

書いても書かなくても忘れる

bisql v0.4.0 - malli による自動バリデーションと式言語のサポート

Bisql

2-way-SQL のデータアクセスライブラリを Clojure で作っています。

github.com

2-way なので bi- で bisql 。 読み方は自転車と同じ「バイスクル」

2-way-SQL に限らず SQL テンプレート系ライブラリの弱点は、全ての SQL を SQL テンプレートとして書くとなると手間が大変というものだと思います。 この解決ために多くの SQL テンプレート系ライブラリではクエリビルダをサポートしています。 テンプレートでも書けるし、クエリビルダでも書ける。 しかし、これは中途半端です。 すべてのクエリ関数を SQL (SQL テンプレート)として維持すると、レビューや把握のコストが大幅に下がります。 すべてのデータベースアクセスが SQL ファイルとしての実体をもつわけなので。 それなのに、クエリビルダが混ざるとそうではなくなってしまいます。 また、単純なクエリだけをクエリビルダで書くつもりがいつの間にか複雑なクエリまでクエリビルダで構築するようになり、発行される SQL が意図したものではなくなっていることも多々あります。

bisql はこの解決のために、全てのデータベースアクセスは SQL を書かなけれいけないというルールです。 しかし、単純な CRUD 操作まで全テーブルについて手書きするのは億劫だと思うので、典型的な CRUD クエリは網羅的に膨大に自動生成する、と言うアプローチを取ります。 実DBを参照し、インデックスを考慮しインデックスを効かせられる様々なパターンのSQLをテンプレートとして作成します。 (defquery) マクロはこれら SQLテンプレートの .sql ファイルを一括で全て Clojure 関数に変換します。 面倒はありません。

Malli サポート

今回、自動生成する SQL テンプレートに :malli/in:malli/out という metadata が埋め込まれるようにしました。 ここにはクエリ関数にわたすパラメータとレスポンスデータのスキーマがそれぞれ自動で埋め込まれます(Bisql のSQL テンプレートは生成されるクエリ関数に反映される任意の metadata を設定可能)。 クエリ関数の metadata に :malli/in:malli/out が入っていると自動で malli で validation を実行します(切り替えは可能)。 また、各テーブルでベースとなる malli スキーマが schema.clj として生成されます。 :malli/in:malli/out ではそれらを参照するかたちで スキーマを定義しています。

生成されるクエリの例:

/*:name crud.get-by-id */
/*:cardinality :one */
/*:malli/in [:map {:closed true} [:id int?]] */
/*:malli/out [:maybe sql.postgresql.public.users.schema/row] */
SELECT *
FROM users
WHERE id = /*$id*/1

生成される schema の例:

(ns sql.postgresql.public.users.schema
  (:refer-clojure :exclude [update])
  (:require [bisql.schema :as bisql.schema]))

#_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]}
(def insert
  [:map
   {:closed true}
   [:id [:or int? bisql.schema/malli-default-sentinel]]
   [:email string?]
   [:display-name string?]
   [:status [:or string? bisql.schema/malli-default-sentinel]]
   [:created-at [:or [:fn bisql.schema/offset-date-time?] bisql.schema/malli-default-sentinel]]])

#_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]}
(def update
  (bisql.schema/malli-map-all-entries-optional insert))

#_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]}
(def row
  (bisql.schema/malli-map-all-entries-strip-default-sentinel insert))

つまり、典型的な CRUD クエリは SQL テンプレートとそのスキーマが自動で生成され、自動で透過的に validation を実行できるようになりました。

hatappo.github.io

clojars.org

clojureのコードをインラインで実行できるzshプラグイン

コーディングエージェントを使うようになって以前よりターミナルを酷使するようになった。ターミナル上でふと四則演算や文字列加工がしたくなったときみんなどうするだろうか?自分は電卓やブラウザなど他のアプリを開きたくはない。ターミナルからできるだけ離れたくはない。shell の算術演算でもいいが冗長だし書き方を忘れがち。ruby や python のコマンドでワンライナーを eval するのもありだが、しかしそれはそれでコマンドを書くのとクォートを書くのが少し面倒。

まったく違う解決方法として、表現力の高い Shell を採用するという選択肢もある。 Nushll, Xonsh, Elvish, YSH などどれも短く便利に色々書ける。

Lisp の世界に目を向けても Lisp を深く統合した Shell のプロジェクトというのがいくつかある。

docs.racket-lang.org

rash は Racket を深く統合した Shell 環境。 |>, |>> といった特殊なパイプ記法を導入することで素の unix コマンドと Racket コードとの間を簡潔に橋渡しすることができるのが強力かつ面白い。

github.com

closh は Clojure を統合した Shell 環境。かなり完成度が高く作り込まれていて便利だが、現在はプロジェクトが hiatus (休止)ステータスだ。

どのプロジェクトを使ったとしても大きな制約が1つある。既存 Shell 環境とその資産を捨てて移行しないといけないということだ。自分は zsh を使っているがいくつかのプラグインや設定を移行するのはかなり骨が折れるだろう。

そこで、ZSHのラインエディタであるZLEから対話処理をフックし clojureのコードなら bb に処理を自動で移譲してくれる zsh プラグイン zsh-clj-shell を作った。

zsh-clj-shell

github.com

zsh-clj-shell をインストールするとこんなふうに書ける。

 $ printf '  aaa  \n  bbb  \nccc' | (map trim %) | (map upper-case %) | cat -n
     1  AAA
     2  BBB
     3  CCC

入力行を簡易的にパースしてパイプの各ステージごとに Clojure コードかどうかを判定しその単位で変換している。 入力は、複数行文字列の場合には改行で区切った Sequence に変換された値が % に入る。 出力は同様に \n で JOIN された文字列となるので、パイプの次のステージが unix コマンドでも自然につながる。 上記はあくまで例であり、実際にはわざわざ一度 shell の世界に戻してパイプする必要はなくて Clojure 内でスレッディングマクロ ->> を使うほうがいい

 $ printf '  aaa  \n  bbb  \nccc' | (->> % (map trim) (map upper-case)) | cat -n
     1  AAA
     2  BBB
     3  CCC

また、入力を Sequence で受け取らずに単一の文字列の塊として受け取りたい場合は %%に入っている。

 $ printf '  aaa  \n  bbb  \nccc' | (trim %%)
aaa  
  bbb  
ccc

ちょっとした計算や集計もサクッと書ける。

 $ cat fee.csv                                                                                                     
Dinner,200
Lunch,150
Breakfast,100
Snacks,50

 $ cat fee.csv | (map #(let [[i u] (split % #",")] (str i "," (format "%.0f円" (* 150 (Double/parseDouble u))))) %)
Dinner,30000円
Lunch,22500円
Breakfast,15000円
Snacks,7500

インストール

インストールは、 clone して手動でzshrc などに読み込むようにする、でもいいし install スクリプトも用意している。もちろん zsh のプラグインマネージャーも使える。[^2]

zinit

zinit light hatappo/zsh-clj-shell

zplug

zplug "hatappo/zsh-clj-shell"

antigen

antigen bundle hatappo/zsh-clj-shell

sheldon

Add to ~/.config/sheldon/plugins.toml:

[plugins.zsh-clj-shell]
github = "hatappo/zsh-clj-shell"

制限

いくつか注意点がある。

1つ目は ( から始まる場合にはBabashkaを起動する。そのため ( を先頭に持ってきたサブシェルの書き方ができなくなってしまっている。1

# サブシェルではなく Clojure として認識されてしまいエラーとなる。
 $ (x=123; echo $x;)
----- Error --------------------------------------------------------------------
Type:     clojure.lang.ExceptionInfo
Message:  EOF while reading, expected ) to match ( at [1,83]
Data:     {:type :edamame/error, :line 1, :column 168, :edamame/expected-delimiter ")", :edamame/opened-delimiter "(", :edamame/opened-delimiter-loc {:row 1, :col 83}}
Location: NO_SOURCE_PATH:1:168
Phase:    parse

----- Context ------------------------------------------------------------------
1: (do (require '[clojure.string :as str :refer :all]) (let [input "" % input result (x=123; echo $x;)] (if (string? result) (println result) (println (pr-str result)))))
                                                                                                                                                                          ^--- EOF while reading, expected ) to match ( at [1,83]

----- Stack trace --------------------------------------------------------------
edamame.impl.parser/throw-reader       - <built-in>
edamame.impl.parser/parse-to-delimiter - <built-in>
edamame.impl.parser/parse-list         - <built-in>
edamame.impl.parser/dispatch           - <built-in>
edamame.impl.parser/parse-next         - <built-in>
... (run with --debug to see elided elements)
edamame.impl.parser/parse-to-delimiter - <built-in>
edamame.impl.parser/parse-list         - <built-in>
edamame.impl.parser/dispatch           - <built-in>
edamame.impl.parser/parse-next         - <built-in>
edamame.core/parse-next                - <built-in>

サブシェルは () の代わりに {} で書くなどの工夫が必要になる。

$ {x=123; echo $x;}
123

これは仕様上の問題なので実装を工夫して解決することはできないが、大きな問題ではないと思っている。

2つ目は、データは clojure 側に渡るときには文字列化していること。そのためパイプがデータをストリーム処理をしない。 unix のいいところが台無しだ。大規模なファイルを扱う上ではつらい。しかし、調べてみると xonsh も似たようなもののようだ。他の Shell はストリーム処理を頑張っているものもある。これについては頑張れば対応できるような気がするが、 InpurtStream などを単に使うようにして、あとはそれを扱いやすくしてあげれば問題ないのか、そのあたり unix のパイプについて今はまだ勉強不足なので見通しが立っていない。

まとめ

zsh-clj-shell プラグインを使うとインタラクティブ・シェル上でシームレスに Clojure (Babashka) が使えます。いくつかの制限によるトレードオフはありますが、既存のZSH資産を捨てずに Clojure を Shell に統合できるというのが最大に良さです。ぜひ試してみてフィードバックをもらえると嬉しいです。

github.com


  1. Babashka のエラーを見やすくするのも今後の課題の1つかな。

claude code で max_tokens のエラーが出続ける

Claude Code でプロンプトを送信すると何を書いても、どんなコマンドを実行してもレスポンスがこうなる。

❯ /init 
  ⎿ API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"max_tokens: 200001 > 64000,
    which is the maximum allowed number of output tokens for
    claude-opus-4-5-20251101"},"request_id":"req_XXXXXXXXXXXXXXXX"}

例外は /status コマンドなどトークンを消費しなそうなコマンドのみ。

Claude Code のバージョンを 2.1.2 までダウングレードすると事象が解消する。

原因はこの設定が環境変数に入っていたこと。この設定値が上限オーバーのようで、LLM をたたくときにエラーになる。

"MAX_THINKING_TOKENS": "200000"

自分は settings.json にいつからか入れたままあまり精査されず残っていた。

おそらく v2.1.7 で入ったこちらの修正の影響と思われる。

Fixed context window blocking limit being calculated using the full context window instead of the effective context window (which reserves space for max output tokens)

コンテキストウィンドウのブロック制限が、有効なコンテキストウィンドウ(最大出力トークンのスペースを予約する)ではなく、完全なコンテキストウィンドウを使用して計算されていた問題を修正しました。

https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md#217

Claude Code のバージョンが古いと Opus4.6 が使えないようなのでちゃんと調べたらすぐ分かった。