# 計算できる設計書を作りたくて新しいLisp処理系を作った (allisp)

mizchi さんのこのポストを読んだ。

自然言語の仕様書は、実装が変わっても自動では更新されない。 解釈の違いや実装との差分も機械的に検出できない。 そのため、自然言語そのものを正本にするのは難しい。

そこで Lean ではなく Lisp を使い、S 式を正本にする処理系 allisp を作った。

allisp について

allisp.pwn.jp

github.com

allisp は、マクロや SBCL で計算できる部分を評価し、未定義の部分だけを LLM に渡す。 この仕組みにより、要件や設計の抜け漏れ検査から、設計に基づくコード生成までを allisp 上で実行できる。

(allispの特徴)未定義のフォームだけを LLM に渡す

allisp は普通の Lisp と同じように、定義済みのフォームを決定的に評価する。

(defun average (numbers)
  (/ (reduce (lambda (total number) (+ total number))
             numbers :initial-value 0)
     (length numbers)))

(def samples '(12 18 15))

(average samples)                    ; 定義済みなので 15
(summarize-metrics :samples samples) ; 未定義なので LLM に渡す

違いは最後の行にある。 未定義のフォームは即座にはエラーにならない。 フォームごと LLM に渡される。

ただし、LLM が返すのは結果の説明ではなく Lisp コードだ。 生成されたコードは評価器が実行する。 実行できないコードなら結果も得られないため、LLM の主張だけで処理が成功したことにはならない。

(allispの特徴)定義を足すほど推測が減る

pure で囲んだ式では LLM を呼び出さない。 未定義の名前があれば、その場でエラーになる。

(pure
  (average samples))

;; oracle: 0 calls (0 misses, 0 hits), errors: 0

すべてを定義すれば普通の Lisp と同じになる。 未定義の部分は LLM に委ねる。 allisp は、その中間を選べるようにした。

仕様の検査も同じである。 今の評価器で判定できない条件があれば、check-spec は検査を飛ばした条件と、追加すべき定義を返す。 形式化の進捗が「通ったか」だけでなく、「何が足りないか」として見える。

要件からコードまで残差だけを任せる

allisp では、要件 -> 設計 -> コードを同じ構造でつなげられる。 要件の語彙や出力先の選択は、Lisp のマクロとして決定的に展開される。

(defmacro make-code-file (path language &body parts)
  `(generate-file ,path
     (render-code :language ,language
                  :design (append ,@parts))))

このマクロで未定義なのは render-code だけだ。 設計からソースコードを合成する部分だけが LLM に渡る。

要件から設計を作る場合も同じで、構造の変換はマクロが担い、要件をどのデータ型や関数へ対応させるかだけを LLM が決める。 生成された設計は、次のコード生成でそのまま正本として使える。

決定できる構造まで毎回 LLM に考え直させる必要はない。 LLM が担当するのは、マクロで計算できなかった残差だけでよい。

今後の展望

allisp はまだ試験的な実装で、仕様を表す語彙や決定的評価器が扱える処理は限られている。 要件と設計の検査、コード生成は動き始めたので、次は実際のプロジェクトで一つの流れとして使い、どこに未定義のフォームが残るかを調べたい。

繰り返し LLM へ渡している処理は、関数やマクロとして allisp 側へ取り込む。 使うほど定義が増え、LLM 呼び出しが減り、同じ仕様を再現しやすくなる状態が理想だ。

すべてを最初から形式化するのではなく、必要になった部分から決定的な定義へ移す。 allisp を、その移行を続けられる処理系に育てていきたい。


(補足) この文章はGPT-5.6-sol を利用して作成しています

ChatGPTとGoogleBardを利用したメタプログラミング構文の提案

まとめ

ChatGPTとGoogleBardを利用して新しい構文を考えてもらう 新しい構文はChatGPTを利用してS式を展開することを期待する

メタプログラミングするために必要なlispの新しい構文を3つ創造してください. 生成AIの入出力に利用できるメタプログラミング構文を考えてください 以下に事例を3つ共有します。ai-eval:
目的: 生成AIのモデルを直接呼び出し、与えられた入力に対する応答を評価する。
構文: (ai-eval model input)
説明: model はAIモデルを指定し、input にはモデルに送る入力が含まれる。この関数はモデルからの出力を返す。
ai-define:
目的: 生成AIの応答を利用して、新しい関数やマクロを動的に定義する。
構文: (ai-define function-name input-pattern output-model)
説明: function-name は新しく定義される関数またはマクロの名前、input-pattern はこの関数に渡される入力のパターン、output-model は出力を生成するために使われるAIモデル。この構文は、与えられたパターンに基づいて入力を解析し、対応するAIモデルを用いて出力を生成する関数またはマクロを作成する。
ai-transform:
目的: 既存のコードに対してAIモデルを使用して変換を適用する。
構文: (ai-transform code-block transformation-model)
説明: code-block は変換されるLispコードのブロック、transformation-model はこの変換に使用されるAIモデル。この関数は、与えられたコードに対してAIモデルを使用して変換を行い、新しいコードブロックを生成する。
 上記の他によりよい構文を考えてみてください

Bardからの返答

以下に、生成AIの入出力に利用できるメタプログラミング構文を3つ考えました。

1. ai-generate

目的: 生成AIのモデルを使用して、新しいコードを生成します。

構文:

(ai-generate model input-pattern)

説明:

  • model は生成AIのモデルを指定します。
  • input-pattern は、生成AIに渡す入力のパターンを指定します。

この構文は、与えられたパターンに基づいて入力を解析し、対応する生成AIモデルを用いて新しいコードを生成します。

例えば、次のコードは、与えられた入力に基づいて、ランダムな文字列を生成する関数を生成します。

(defmacro ai-generate-random-string (model input-pattern)
  `(lambda ()
    (ai-generate model ,input-pattern)))

(defvar random-string-model (make-instance 'ai-random-string-model))

(defmacro generate-random-string ()
  (ai-generate-random-string random-string-model `(length ,(read))))

2. ai-rewrite

目的: 生成AIのモデルを使用して、既存のコードに変更を加えます。

構文:

(ai-rewrite code-block transformation-model)

説明:

  • code-block は変更されるLispコードのブロックを指定します。
  • transformation-model はこの変更に使用される生成AIモデルを指定します。

この構文は、与えられたコードに対して生成AIモデルを使用して変更を行い、新しいコードブロックを生成し、それを返します。

例えば、次のコードは、与えられた入力に基づいて、コード内のすべての関数名を大文字に変換します。

(defmacro ai-rewrite-uppercase (model input-pattern)
  `(lambda (code)
    (ai-rewrite code model ,input-pattern)))

(defvar uppercase-function-model (make-instance 'ai-uppercase-function-model))

(defmacro rewrite-uppercase (code)
  (ai-rewrite-uppercase uppercase-function-model `(code ,code)))

3. ai-generate-and-rewrite

目的: 生成AIのモデルを使用して、新しいコードを生成した後、そのコードを変更します。

構文:

(ai-generate-and-rewrite model input-pattern transformation-model)

説明:

  • model は生成AIのモデルを指定します。
  • input-pattern は、生成AIに渡す入力のパターンを指定します。
  • transformation-model はこの変更に使用される生成AIモデルを指定します。

この構文は、まず与えられたパターンに基づいて入力を解析し、対応する生成AIモデルを用いて新しいコードを生成します。次に、生成されたコードに対して、2番目の生成AIモデルを使用して変更を行い、新しいコードブロックを生成し、それを返します。

例えば、次のコードは、与えられた入力に基づいて、ランダムな文字列を生成し、その文字列をすべて大文字に変換します。

(defmacro ai-generate-and-rewrite-uppercase (model input-pattern)
  `(lambda ()
    (let ((code (ai-generate model ,input-pattern)))
      (ai-rewrite code transformation-model `(code ,code)))))

(defvar generate-and-rewrite-uppercase-model (make-instance 'ai-generate-and-rewrite-uppercase-model))

(defmacro generate-and-rewrite-uppercase ()
  (ai-generate-and-rewrite-uppercase generate-and-rewrite-uppercase-model `(length ,(read))))

これらの構文は、生成AIの入出力に利用することで、より柔軟で強力なメタプログラミングを実現することができます。

なぜ自分はLISPが好きなのか

tl;dr

S式ならばこの心配はありません。なぜならどんな問題であれ、 それが計算機で処理可能ならば何らかの内部表現を持つはずなので、 新たな問題に対応する内部表現を考えたらそれをそのまま書き下せば良いだけ だからです。

これに尽きる

なぜ自分はLISPが好きなのか

これまでLISPをなんとなく学んでいたが、S式の理由 を読んで言語化することができた。

終わりに

この文章はWIPなので定期的に更新する

ChatGPTの入力をLISPのS式にするメリデメ

tl;dr

chatGPT内のLLMは自然言語テキストから多く学習しているため、S式入力の理解・解釈が制約される可能性あり。

ChatGPTへの入力

ChatGPTへの入力文にて 自然言語よりも LISPのS式を選択する事によるメリデメを教えてください

ChatGPTへの出力

LISPのS式(S-expressions)をChatGPTへの入力文として使用することは、ある程度のメリットとデメリットがあります。

まず、メリットについては以下のとおりです:

  • 構造的明瞭さ:S式は、プログラムやデータの構造を明確に表現することができます。これにより、複雑な入力を構造的に表現することが可能となります。

  • エラーの発生が少ない:適切なS式が提供されると、一般的には文法的なエラーが少なくなる傾向にあります。これは、S式が事前に定義された明確な構造を持つためです。

  • 処理の再現性:同じS式を与えれば、その実行結果は一定であり、再現性があります。

一方で、デメリットも存在します:

  • 可読性と書きやすさ:自然言語に比べてS式は人間にとって直感的に理解しにくい場合があります。これは特に、LISPやS式に不慣れな人々にとっては大きな問題となる可能性があります。

  • 表現の自由度:S式は構造的である一方で、自由な表現を制約する場合があります。自然言語はその表現力が非常に高く、あらゆる種類の思考やアイデアを表現することが可能です。

  • ChatGPTの理解能力:GPT-4の訓練データは主に自然言語から構成されており、LISPやS式のような特定のプログラミング言語の理解や解釈は、自然言語に比べて制約される可能性があります。

以上が、LISPのS式をChatGPTへの入力文として使用することのメリットとデメリットです。

commonlispでFizzbuzz問題を解いてみる

最初の実装

まずはシンプルなFizzBuzz実装から始める。1から100までの数字を列挙し以下の条件に基づいて出力する。

  • 3で割り切れる場合は "Fizz"
  • 5で割り切れる場合は "Buzz"
(dotimes (i 100)
  (let* ((n (1+ i))
         (fizz (if (zerop (mod n 3)) "Fizz" ""))
         (buzz (if (zerop (mod n 5)) "Buzz" ""))
         (fizzbuzz (concatenate 'string fizz buzz)))
    (format t "~a~%" (if (string= fizzbuzz "") n fizzbuzz))))

これでFizzBuzzの基本的な実装が完成した。ここからコードを拡張する。

拡張版実装

こちらではルールを柔軟に追加できるようにした。

(defun print_fizzbuzz (i rules)
  (let* ((n (1+ i))
         (result (reduce (lambda (acc rule)
                           (let ((condition (first rule))
                                 (output (second rule)))
                             (if (funcall condition n)
                                 (concatenate 'string acc output)
                                 acc)))
                         rules
                         :initial-value "")))
    (if (string= result "")
        n
        result)))

(defun print_fizzbuzz_with_rules (limit rules)
  (dotimes (i limit)
    (let ((result (print_fizzbuzz i rules)))
      (format t "~a~%" result))))

(let ((rules (list (list #'(lambda (n) (zerop (mod n 3))) "Fizz")
                   (list #'(lambda (n) (zerop (mod n 5))) "Buzz"))))
  (print_fizzbuzz_with_rules 100 rules))

拡張版では rules 変数にルールを追加することで、例えば「7で割り切れる時は "hoge" を返す」といった機能を簡単に追加できるようになった。 また表示とルールを分離することでテストしやすいコードになった。

ChatGPTにLISPの新しい構文を考えてもらった (要求定義編)

以下は、要求定義を表現するために使用できる新しい構文の一連の具体的な例です。これらはLISPの構文を拡張し、より高い抽象度で情報処理システムの要求を表現するために使用できます。

1. `(def-case CASENAME CONDITIONS ACTIONS)` - 特定の条件が満たされた場合に実行されるアクションを定義する。例えば、`(def-case "ログイン" (user-exists? user-name) (login user-name))` は、ユーザーが存在する場合にログインするアクションを定義します。

2. `(def-entity ENTITY ATTRIBUTES)` - エンティティの属性を定義する。例えば、`(def-entity "ユーザー" (name age email))` は、ユーザーエンティティの名前、年齢、メールアドレスの属性を定義します。

3. `(def-function FUNCTIONNAME PARAMETERS BODY)` - 関数を定義する。例えば、`(def-function "加算" (a b) (+ a b))` は、2つの引数を加算する関数を定義します。

4. `(def-module MODULENAME FUNCTIONS)` - モジュール内の関数のグループを定義する。例えば、`(def-module "計算" *1 (def-function "減算" (a b) (- a b))))` は、計算モジュール内の加算と減算の関数を定義します。

5. `(def-requirement REQUIREMENT)` - システムの要求を定義する。例えば、`(def-requirement "ログイン機能")` は、ログイン機能の要求を定義します。

6. `(def-component COMPONENTNAME REQUIREMENTS)` - システムのコンポーネントとその要求を定義する。例えば、`(def-component "ユーザーサービス" ("ログイン機能"))` は、ユーザーサービスコンポーネントとそのログイン機能の要求を定義します。

7. `(def-interface INTERFACENAME METHODS)` - インタフェースとそのメソッドを定義する。例えば、`(def-interface "データベース接続" ("接続する" "切断する"))` は、データベース接続インタフェースとその接続と切断のメソッドを定義します。

8. `(def-class CLASSNAME ATTRIBUTES METHODS)` - クラスを定義し、その属性とメソッドを指定する。例えば、`(def-class "ユーザー" (name age) *2 ("情報を更新する" (new-name new-age) (set-name new-name) (set-age new

*1:def-function "加算" (a b) (+ a b

*2:"情報を表示する" () (display-info

GPT隆盛の時代にLispを勉強する

思いつきで上記文章書いたけど、「拾う」「書く」という既存の言語にはない概念をdefmacroしてそれぞれの要素(人間、鉛筆、ノート) に適用させてた。

きちんと理解できているみたい。すごい

 

```lisp

;; シミュレーション世界の定義
(defvar *world* (list (list 'pencil '(0 0))
                      (list 'notebook '(1 1))
                      (list 'person '(0 1))))

;; マクロ定義
(defmacro with-object *1
     result))

;; 出来事のシミュレーション
(with-object (pencil 'pencil '(0 0))
  (with-object (person 'person '(0 1))
    (with-object (notebook 'notebook '(1 1))
      (setf pencil (list 'pencil nil))
      (setf person (list 'person '(0 0)))
      (setf notebook (list 'notebook '(1 0)))
      (with-object (pencil 'pencil '(0 0))
        (with-object (person 'person '(0 1))
          (pick-up pencil person)
          (with-object (notebook 'notebook '(1 1))
            (write-on-notebook person notebook "かきくけこ"))))))))

;; 出来事の定義
(defun pick-up (pencil person)
  (let *2
    (setf pencil nil)
    result))

(defun write-on-notebook (person notebook text)
  (with-open-file (stream "note.txt" :direction :output :if-exists :supersede)
    (format stream "~aがノートに「~a」と書いた。"
            '彼 text)))

```

*1:var class position) &body body)
  `(let* ((,var (find-if (lambda (obj) (and (eq (car obj) ',class)
                                             (equal (cadr obj) ',position)))
                         *world*)))
          (result (progn ,@body)))
     (setf (cadr ,var) (position result

*2:result pencil