AI ポン出しスライド品評会

最近[いつ?]は AI に登壇用のスライドをポン出しさせたという話や, それに対する賛否両論を各所で聞くようになってきました.

そこで私も試しに AI にスライドをポン出しさせてみて, 実際に登壇に耐える品質なのかレビューしてみました. 条件設定は以下の通りです:

  • Claude Code を使用
  • 題材は tsc.rip の実装リポジトリ
  • 指示は tsc.rip を題材に 10 分間の登壇を行うことになりました. 登壇用のスライドを作成してください. 出力形式は PDF でお願いします. のみ
    • そのため発表のストーリーが意に沿うか・人間が作った時と同じかどうかは評価対象外
  • モデルは Opus 5 と Fable 5 (effort は共に high) でそれぞれ出してみて比較

というわけで早速見ていきましょう.

Opus 5

  • 表紙はまあ悪くないんじゃないですか

  • 見たことある感じの見た目だ! 特にスタイルを指示しないとやっぱりこういう感じになるんだ
  • キャプション部分, 文字も小さいしコントラスト比も低くないですか? 後ろの方の人とかちょっと見づらいんじゃないかな
  • 「です。」: 他のスライドも含めて一貫して常体なのに, ここだけなぜか敬体が混ざっていますね

  • 「The tsc is dead, long live the tsc!」のスタイルはブラウザでサイトを表示した時のものなのだけれど, curl の結果しか紹介していないと何でこんなスタイルなのか伝わりづらそう
    • さっきのスライドと同時にブラウザのデモも見せるとか, 口頭でブラウザで見るとこういう感じと補足できていれば良さそう
  • 引き続き小さい文字は読みづらいかも
  • 「.rip ドメインが取れたので」: 正確な経緯 (script のアナグラムで取っていて持て余していたのを転用した) は伝えていないのでわからないのは仕方ないですが, 捏造はしないでください
  • 「せっかくなので」: こういう変な論理の飛躍は自分もあえてやることがあるので良いでしょう

  • コード例は最後の 1 行だけで十分なんじゃないかな

  • この横並びの図よく見ますよね. 5 つも並ぶと文字も小さくなってかなり読みづらいですし, 適度に折り返すか, 横長のものは縦に並べるのが良さそう
  • 「typeeval」はサーバー内部のモジュールの名称なのだけれど, ここでいきなり名前だけ書いても何のことか伝わりづらそう
  • typeeval と typescript-go だけ色が違うのはどういった意図があるのかわからなかった
  • 肝心のアプリケーション本体が図の中に登場していなくて, 全体像なのに全体の流れがわかりづらい
    • リクエストの流れ (CloudFront → Lambda → サーバー → アプリ) と, サーバー内部の型の評価の流れ (サーバー → typeeval モジュール → typescript-go) の二つを同時に説明しようとしてしまっていそう

  • 「Program」が何なのかは簡単に説明した方が良い気がします. まあ口頭でも良いか

  • 「契約違反」「型エラー」がそれぞれ何を指しているのかわかりづらいかも
    • 直すなら「型の評価や読み取りに失敗した時は 500 にする」とか?
    • 「ContractError」「TypeError」みたいな内部のエラーの識別子をわざわざ持ち出す必要もなさそう
    • そもそも聴衆はこれらのエラーを 500 としていることについてそれほど興味ない気もする

  • 特筆すべきことはなさそう

  • なんかこの「xxxを、xxxする」みたいな構文もよく見ますね. 洒落臭いのでやめた方がいいです

  • q 値の比較を型レベルでやっているのは特筆すべきだとは思うけど, あとの話題は割とどうでも良いので省いてもいいんじゃないかな

  • 「ついでに計算もさせる」: ここまでもだいぶ計算させてただろという気がする
    • /brainfuck と比べても Accept ヘッダの解釈の方がよほど複雑
    • 単に面白エンドポイントもありますくらいの紹介で良いのでは

  • このスライドで伝えたいことがわからないです. 遅いの? 十分に速いの?

  • 「テストで殴る」: 殴らないでください. 暴力反対

  • 「インターネットに晒す」: 晒さないでください. 必要もないのに攻撃的な言葉を使わない
    • 単に「公開する」に直すだけだとまだ違和感があって, 直すとしたら「インターネットに公開する上での安全性」とかかな
  • こういう二段組のレイアウトもよく見るけど, 個人的にはスライドの情報量が多くて追いつけなかったり, どこを見たら良いのかわからなくなることが多いです. ちゃんとスライド二枚分の時間をかけて説明するか, そもそもスライドを二枚に分けてほしい

  • 「本物の純粋関数型言語」: う〜ん, 意図はわからないでもないけど正確さに欠けるので「れっきとした宣言的プログラミング言語」くらいが無難かなあ
  • 「HTTP サーバーくらいは書ける」: Web アプリは書いたけど, HTTP サーバー全体は書いてないので言い過ぎでは?
    • IO がないのはもちろんだし, それを除いても HTTP サーバーの全てを型で実装するのは難しい
  • 「tsc が Go になったことで (中略) 間に合う速度になった」: TypeScript 実装と速度比較していないのに適当なこと言わないでください
  • 「こんなことに使われても壊れない」: 確かに typescript-go はよくできているんだけど, よくできているかの評価の軸としてこれを使うのはおかしいのでは

  • https:// 書くなら書くので一貫させましょう
  • 最後の curl の例は何を見せたいのだろう? HTML ならブラウザで見せた方が良さそう

総評: 全体を通して視覚的・言語的に読みづらいところが多いし, まとめのスライドに関してはほぼ間違っていて, このまま登壇はできなさそう.

Fable 5

  • Opus とは打って変わってシンプル. まあ良いんじゃないですか

  • 文字がデカくて好感が持てます

  • 「JavaScript 実装」: 実装言語としては「TypeScript 実装」の方が正確そう
    • わかりづらそうなら「TypeScript セルフホスト」とかで
  • 「置き換わる」: 7.0 はリリース済みなので「置き換わった」でも良いかな
  • 「当然」: こういう変な論理の飛躍は自分もあえてやるので一旦良いでしょう
  • 小さい文字のコントラスト比はちょっと際どい感じはする (読みやすいとは言えない)
    • Opus のものよりはマシだけど

  • 横並びも 3 つくらいならまだ読めますね
  • 「アプリ」と「サーバー」についてはここでちゃんと説明した方が良さそう
    • この後のスライドではアプリとサーバーの存在を前提として話が進んでいる
    • 補足的に書かれている説明は読み飛ばしそうだし, そもそも Apache と CGI でピンと来る人にしか伝わらなさそう

  • 特筆すべきことはなさそう

  • 「__eval.ts」というファイル名は重要ではないので「評価用のファイル」くらいでも良いかも
  • 「型を文字列に印字してパースはしない」: 理由が説明されないなら書かないほうがわかりやすそう

  • 超重箱の隅だけど, 一行目は「、」のあとで改行した方が読みやすそう
  • 「アプリの型エラーが (中略) Internal Server Error になる世界」: 大体そうじゃない?

  • ちょっと情報がごちゃついているので, 自分だったら上 4 行は 2 行にまとめるかな

  • 「プログラム」が何なのかは簡単に説明した方が良い気がするけど口頭でも良いか
  • 「Go 実装だから許される贅沢」: TypeScript 実装と速度を比較したことないのに言い過ぎです
  • 停止しないリクエストが作れるのは, 型システムがチューリング完全かどうかというよりは, /brainfuck みたいなふざけたエンドポイントを用意してるのが悪いのでは

  • 特筆すべきことはなさそう

  • 「何が大変か」と言いつつ「HTML を返す」「Brainfuck」あたりはやるだけみたいな感じで, 結局何が大変なのかわかりづらいです
  • 「node_modules に JS が無くても普通に動く」: まあそうなのだけれど, このスライドの話との関連性がよくわからなかった

  • 「tsString」みたいな内部の関数名はわざわざ言っても意味なさそう
  • タイムアウトや上限の話題はさっきも登場したけど, ここで詳しく説明されるならここだけでも良いかも
  • 「信頼境界」: これだけ Lambda 一般の話題なので浮いてそう. この発表で説明する必要性は薄いかも

  • 「ネタだけど」の後, Accept ヘッダの解釈とかの型の実装で本気出している部分を紹介した方が良くないですか?
    • そもそもネタだったら入力検証はサボって良いみたいな言い草なのも気になるけれど
  • GitHub リポジトリ https://github.com/susisu/tsc.rip は private なので, ここに書いても見えないですね

総評: ちょいちょいずれているところはあるけど, まあ最悪このままでも登壇できなくもないか.

まとめ

ポン出しを比較しただけで, 二回目では結果は変わるかもしれないという前提は置いた上で,

  • Opus と比べると, Fable はこの手の登壇の文脈をある程度わかっている感じがあっていくらかマシ
  • Opus は文字の大きさやコントラスト比, スライドあたりの情報量などの配慮が足りていない上, 言語的なセンスもあまり良くない
  • Opus も Fable も共通してちょいちょい間違ったことやずれたことを言っているので, まともな品質を目指すならちゃんとレビューはした方が良いです. してくれ
  • 実装途中に人間が考えていたこと (型の評価の実装方針検討など) は AI に伝えていないので, 当然スライドにも全く含まれていないません. もしそれが登壇で伝えたいことなのであればちゃんと AI にも伝えましょう

比較用: 人間

tsc.rip を支える技術 または 型レベル Web アプリケーション

先週 8/22 に Kyoto.なんか #8 を開催しました. 名前の通り何でもありのイベントですが, 今年も多彩な登壇・参加者が集まって面白い会になったと思います. いつもありがとうございます.

私は「tsc.rip を支える技術」というタイトルで登壇しました. 以下は発表の記事版です. 当日のスライドは末尾にあります.

TypeScript 7.0

先月 7/8 に TypeScript 7.0 がリリースされました. コンパイラ (tsc) の実装言語が TypeScript 自体から Go になり, パフォーマンスが向上しています. めでたいですね.

devblogs.microsoft.com

GitHub 上のリポジトリについても, 元々 Go 実装のコンパイラは別リポジトリ microsoft/typescript-go で開発されていましたが, 8/20 に microsoft/TypeScript に取り込まれました. *1

tsc.rip

あまりにめでたいので, これを記念するサイト tsc.rip を作りました. 実のところは持て余していたドメインが使えそうだったので使ったというだけなんですが, まあそれはそれとして...

tsc.rip のスクリーンショット
The tsc is dead, long live the tsc! *2

tsc.rip を支える技術

さてこの tsc.rip は AWS 上に以下のような静的サイト激安構成で構築しています.

あれ? 静的サイト激安構成なら CloudFront のオリジンは Lambda ではなく S3 の方が無難なはずですよね?

実は単にペライチの HTML を配信しているだけではなく, ブラウザでアクセスした時と curl でリクエストした時でレスポンスを変えているんです.

$ curl https://tsc.rip
The tsc is dead, long live the tsc!

あれ? それでも CloudFront Functions を使えばヘッダを見て向き先の S3 オブジェクトを変えられますよね?

実はリクエストボディを処理するエンドポイントもあるんです. CloudFront Functions ではリクエストボディは扱えないですからね.

$ curl https://tsc.rip/echo -XPOST -dHello
Hello%
$ curl https://tsc.rip/brainfuck -XPOST -d'>,[>,]<[.<]!Hello'
olleH%

あれ? それって静的サイトではないのでは?

tsc.rip を支える技術 (裏)

(当日はここでウミガメのスープが挟まりましたが省略)

ここでレスポンスヘッダを見てみましょう.

$ curl https://tsc.rip -I
HTTP/2 200
content-type: text/plain; charset=utf-8
content-length: 36
(中略)
x-powered-by: typescript-go/7.0.2
x-type-instantiations: 3083

見慣れない変なヘッダ x-type-instantiations がありますね. TypeScript で複雑な型レベルの計算をすると Type instantiation is excessively deep and possibly infinite. というエラーメッセージをよく目にしますが, このヘッダはまるで型レベルの計算が行われているとでも言っているかのようです.

そう, 実は tsc.rip は型レベル Web アプリケーションだったのです. ここでいう"静的"サイトとは, リクエストの処理が静的な検査 = 型レベルの計算で行われているという意味だったんですね〜.

詭弁では? はい.

型の評価

tsc.rip の裏側にある Go 製 Web サーバーには, 先ほど紹介した Go 実装の TypeScript コンパイラを組み込んでいます. このサーバーがやってきた HTTP リクエストを TypeScript の型に変換し, コンパイラを利用して型レベル Web アプリケーションを実行して, やはり TypeScript の型として出力された結果を HTTP のレスポンスにして返却しています. ここで言っている「型レベル Web アプリケーション」の実体は, 以下のようなシンプルな型エイリアスです. (ストリームや IO はサポートしていません.)

export type Handle<Req> = Res;

型を評価する方法としては以下のような選択肢が考えられます:

  • tsc が出力するコンパイルエラーを解析する
  • Language Server を使う
  • Compiler API を使う
  • (自前で型インタプリタを実装する...のは今回は除外)

tsc が出力するコンパイルエラーを解析する方法は, リクエストごとに以下のような意図的に型検査に失敗するようなファイルを作成し, tsc を実行した時のエラーメッセージからレスポンスを抽出するというものです. デフォルトの設定ではエラーメッセージに含まれる型情報は一定の長さで省略されますが, --noErrorTruncation オプションを付与することで回避できます.

type __Req = { /* ... */ };
declare const __res: Handle<__Req>;
const _: "__TSCRIP_RESPONSE__" = __res;
$ tsc -p tsconfig.json --noEmit --noErrorTruncation --pretty false
__eval.ts(3,7): error TS2322: Type '"Content-Type: text/plain; charset=utf-8\n\nThe tsc is dead, long live the tsc!\n"' is not assignable to type '"__TSCRIP_RESPONSE__"'.

tsc が常にコンパイルエラーを出してコケるという点では tsc.rip というドメインには最適なんですが, CGI みたいにリクエストのたびにコンパイラのプロセスを起動することになるので, 実行速度の面で懸念があります. この期に及んで速度を気にする必要はあるのか? というのはさておき...

サブプロセスで起動している Language Server と通信し, 型情報を手に入れるという方法も考えられます. TypeScript 7.0 では従来の独自プロトコルを使う tsserver に代わって, 正式に LSP プロトコルを話すサーバーが実装されています. 結局は次の Compiler API を使うことにしたので詳細な実現方法は調べていないのですが, 利点はコンパイラから正式に公開されているインターフェースのみを使うということ, 欠点は基本的にはテキストエディタ上でのヒント表示などを想定した機能なので詳細な型情報の取得には必ずしも適してはいない (やはりデフォルトでは省略されるらしい) というあたりです.

さて最終的に使うことにした Compiler API はコンパイラ自体をライブラリとして利用するための API のことで, これを使えば詳細に型情報を取得・解析できます. ただし最大の欠点として, TypeScript 6.0 までは利用できていた Compiler API は, Go 実装に切り替わった 7.0 では利用できなくなっています. 次期バージョンである 7.1 では (6.0 以前とは互換性がない形ではあるものの) 再度実装されることが予告されていますが, 少なくとも現時点ではまだ提供されていません. また Go の実装も Compiler API に相当する部分は internal (非公開) ディレクトリ以下にあり, 通常の方法では外部から使用することができません. かといってここで 6.0 を使うのもなんか負けた気がします.

ということで, ここで internal ディレクトリ以下の API を使うためにちょっとしたハックをしましょう. tsgolint では internal ディレクトリ以下にあるのものを公開するために割と手の込んだことをやっているようです*3が, 公開まではせず単に API を使用するだけであればそこまで難しくはありません.

まずはサーバーの実装を, 以下のようにメインのモジュールと, typeeval/ 以下のサブのモジュールの 2 つに分割します.

server/
|-- typeeval/
|   |-- go.mod
|   \-- typeeval.go
|-- go.mod
\-- main.go

typeeval/go.mod では, このモジュールが github.com/microsoft/typescript-go*4 以下の github.com/microsoft/typescript-go/_tscrip/typeeval であるというように勝手に名乗ります. これだけで typeeval/ 以下では github.com/microsoft/typescript-go の internal ディレクトリ以下にある非公開 API が使えるようになるので, ここで型を評価するコードを実装します.

// server/typeeval/go.mod
module github.com/microsoft/typescript-go/_tscrip/typeeval

require github.com/microsoft/typescript-go v0.0.0-20260708042240-2bd066d87f5b // typescript/v7.0.2

メイン側の go.mod では replace を使い, typeeval/ 以下の実装を github.com/microsoft/typescript-go/_tscrip/typeeval として参照できるようにします. これでメインのサーバー実装から型の評価を実行できるようになりました.

// server/go.mod
module github.com/susisu/tsc.rip/server

replace github.com/microsoft/typescript-go/_tscrip/typeeval => ./typeeval

ところで非公開 API が使えるようになったところで, 使い方に関する資料が豊富にあるわけではありません*5し, 今後のバージョンアップで大きく変更される可能性もあるのでそこまで深入りしたいわけでもありません. というわけで型の評価部分は Claude Code に調査と実装を任せて, なんかいい感じのものが出てきたので大体そのまま採用しています. 退屈 (でもないが) なことは AI にやらせよう.

こうして完成したサーバーの実装は単体で以下のリポジトリに置いてあるので, 具体的な評価の仕組みなどに興味がある方は覗いてみてください. (tsc.rip のインフラ構成に固有の実装は除いています.)

github.com

型レベル Web アプリケーションの実装

TypeScript の型システムはチューリング完全なので, 型レベル Web アプリケーションの実装はやるだけです. 以上.

と言うだけで終わっても面白くないのでちょっとだけ雰囲気を紹介しておくと, まずルートは以下のように定義しています. Accept ヘッダの解釈*6やレスポンスヘッダの付与などはいい感じにライブラリ化して使えるようにしました.

export type Routes<Req extends req.Request> = {
  "/": {
    GET: ((
      Req["headers"] extends { accept: infer Accept extends string }
        ? accept.Negotiate<Accept, ["text/plain", "text/html"]>
        : "text/plain"
    ) extends "text/html"
      ? res.HTML<200, `<!DOCTYPE html>(略)`>
      : res.Text<200, "The tsc is dead, long live the tsc!\n">) &
      hs.Link<`<https://susisu.ch>; rel="author"`> &
      hs.CacheControl<"public, max-age=3600"> &
      hs.Vary<"accept">;
  };
  // ...
};

ルーティングを行うサーバー本体の実装はこういう感じ. パスパラメータには対応していませんが, ひとまずこれで十分です.

export type Handle<Req extends req.Request> =
  Routes<Req> extends { [_ in Req["path"]]: infer Handlers }
    ? Handlers extends { [_ in Req["method"]]: infer Res }
      ? Res
      : Req["method"] extends "HEAD"
        ? Handlers extends { GET: infer Res }
          ? Res
          : res.Error<405, "Method Not Allowed\n"> & hs.Allow<AllowedMethods<Handlers>>
        : res.Error<405, "Method Not Allowed\n"> & hs.Allow<AllowedMethods<Handlers>>
    : res.Error<404, "Not Found\n">;

レスポンスへの Content-Length ヘッダの付与 (つまりは文字列の UTF-8 でのバイト数カウント) は TypeScript の型レベルでは一般的な実装が困難なので, Go 側の Web サーバーが勝手に付与してくれるのに任せています. 他にも上記の Accept ヘッダの正しい解釈に必要な値のパースや数値の大小比較, Allow ヘッダを付与するためにユニオンを直列化*7したりなど, 型レベルではちょっとしたことをやるのにも大変な実装が要求されてしまうので, 一般に TypeScript の型で Web アプリケーションを書くことはお勧めしません. 知ってた?

まとめ

tsc.rip の本体は型レベル Web アプリケーションだったのだ! TypeScript 万歳!

当日のスライドはこちら.

*1:https://github.com/microsoft/TypeScript/pull/63763

*2:https://ja.wikipedia.org/wiki/%E5%9B%BD%E7%8E%8B%E5%B4%A9%E5%BE%A1%E3%80%81%E5%9B%BD%E7%8E%8B%E4%B8%87%E6%AD%B3!

*3:参考: https://speakerdeck.com/syumai/how-tsgolint-exposes-typescript-gos-private-apis

*4:当時は実装が https://github.com/microsoft/TypeScript へ取り込まれる前でした.

*5:とはいえ従来の Compiler API と似た雰囲気で, 全くわからないという感じでもないですが.

*6:RFC 9110 に従ってステートマシンを書いたりするとかなり大変だったので, 丸ごと Claude に実装させました https://gist.github.com/susisu/2145a0035263b340ee8611c5bd767397

*7:参考: https://github.com/type-challenges/type-challenges/blob/main/questions/00730-hard-union-to-tuple/README.md