プログラマ向け圏論入門 1を見てからZodで考えてみた
この動画を全部見ました!ありがとうございます!!
止めずに最後まで見てしまいました。面白かったです。
5月9日のフロントエンドカンファレンスでZodの話をするので、視聴後にZodのコードを眺めていたら、「Zodって圏における何だろう」と気になりました。
私自身この動画で初めて圏を知ったレベルなので、間違っているところは多々あると思いますが、考えてみることにしました。Haskellやモナドはまだ勉強していません。
この記事は、tsが好きかつ、動画を見て圏論について一緒に勉強したい人向けに書いています。
まずは集合と対象の違いをはっきりさせてみる?
一旦、集合について考えてみました。例えば、z.string()は文字列の集合です。
具体的には、こんな感じでしょうか。
- "apple", "orange", "" ... といった無限個の要素を認める
- Zodの
parse()メソッドである値がその集合に「属しているか」を判定する
次に、z.string()が対象として扱っていいかを考えようとしました。でもその前に、対象は圏があるから対象なわけであって...と考えてしまいました。
結局、成り立つか分かりませんが、z.string()が対象である圏を考えて、成り立たつかどうかを考えてみることにしました
z.string()が対象である圏を考えるために、射を考える
動画の後半では、「人間関係」や「プログラミング経験が長い」かどうかという射を仮置きして、圏であるかどうかを判断しています。
自分もやってみました。ただ、途中から普通の集合の話になってきていそう...。
射①:スキーマ (A) でパース可能な値は、必ずスキーマ (B) でもパース可能である
簡単そうなので、ここから考えてみます。
例えば、
-
z.email()でパース可能な値はz.string()でパース可能 -
z.object({ name: z.string(), age: z.number() })でパース可能なオブジェクトはz.object({ name: z.string() })でもパース可能。
といった感じです。対象と射は上に書きましたね!
合成と結合律
z.literal("admin@x.com") z.email() z.string() z.any()
包含関係は順序を問わないので、
コードで、AからBへの射を定義し、BからCへの射を定義してから合成を定義するとこう。
const composeSchema = <T, U, V>(f: z.ZodType<U, any, T>, g: z.ZodType<V, any, U>) => {
return f.pipe(g);
};
const adminToEmail = z.literal("admin@x.com").pipe(z.string().email());
const emailToString = z.string().email().pipe(z.string());
const strictComposed = composeSchema(adminToEmail, emailToString);
ちなみに最初はpipe()で定義し、それ違うよと言われました。こちらは単にデータを流しているだけ。
const A = z.literal("admin@x.com");
const B = z.email();
const C = z.string();
const composed = A.pipe(B).pipe(C);
composed.parse("admin@x.com");
恒等射
z.string()でパース可能な値はz.string()でパースできる。あんまり意味があるのかはわかりませんが...
じゃあこれは圏でいいですか?
これが圏であるかどうかを確定する前に、他のケースについても考えてみます。z.any、z.unknown、z.transformです。
z.any()とz.unknown()
どちらもあらゆる値を受け入れるスキーマなので、
射で考えると、
- 任意の
に対してA なので、射V_A \subseteq V_{\text{any}} は常に存在するA \to \text{z.any()} -
も常に存在するA \to \text{z.unknown()}
両方の射が存在するのですが、これはいいんですかね?
よく見ると射①の世界から見れば同じになりそう?でした。
TypeScriptの推論型は違っても、Zod圏のレベルでは同じ万能スキーマで、互いに射が双方向に伸びている感じです。
そんでAIに聞いたら「終対象(terminal object)の匂いがする😁」と言われました
z.transform()は射①に入らない
例えば、z.string().transform((s) => s.length)は、文字列を受けて数値を返すスキーマです。ということで、射①にはパース可能な値の包含関係でないので、捉えられません。
ただこのz.transform()を無理やり射にしたいのなら、こんな定義になるでしょうか?
射②:
射②を一緒にするとどうなるか?そもそも射②だけで別の圏が建つのか?transformの合成は明らかに関数合成っぽいので...
この辺りは一旦別で考えてみようかなと思います(まだ考えがまとまっていません)
このへんはまだ全然整理できていないので、続きはまた別の記事で考えてみます。圏論の入り口で詰まりながらも、Zodを通して眺めるとちょっとだけ楽しい時間でした。
Discussion