Skip to content
Go back

AIに作らせたコードからElispを学ぶ

きっかけ

私は大学3年生のときからEmacsを使っています。当時所属していた研究室の指導教員のすすめで、「なんだかかっこよさそう」と思って使い始めたのを覚えています。

長年Emacsを使ってきたとはいえ、まだまだ使いこなせていません。インターネットで便利な機能を検索して自分のEmacsにインストールし、少し設定を変えるというのが、私の主なEmacsの使い方です。

これまで、Elispを学ぶためにいくつかの本や既存のコードを読んでみましたが、あまり身につきませんでした。関数型言語に慣れていないことや、Elisp (Emacs Lisp) のデバッグ方法がわからなかったこともあり、次第にモチベーションが下がっていったのだと思います。

そんな中、AIを活用した新しい学習方法を試してみることにしました。教科書やマニュアルを読むのは個人的に楽しめなかったので、どうせなら自分が欲しいものを題材にして、実践的に学んでみようと考えたのです。

この記事では、Claude Codeに生成させたコードを題材にElispを学んだ過程について書きます。

何を作ったか

Claude Codeに、日本語文章を添削してくれるシステム (sakubun-checker) を作ってもらいました。Geminiの無料API (2.5 Flash) を使用して、日本語の作文技術のルールに基づいて日本語文章を評価してくれます。

sakubun-checkerでこの記事を添削する様子

Elisp, Pythonスクリプト, 日本語作文技術のルールをまとめたプロンプトの3つで構成されています。Elispは入出力を担当し、PythonスクリプトはGeminiへのAPIリクエストを担当しています。プロンプトには「修飾語・被修飾語を近付ける」といったルールを記述しつつ、マークダウン形式で添削結果を出力するようにしています。

コードを読み解く

Claude Codeが生成したコードを読み解きながら、Elisp特有の概念や慣習を学んでいきました。開発途中のコードも含めて、特にためになった学びをいくつか紹介します。

なお、以下のコード例は学習のために簡略化しています。実際のコードにはエラーハンドリングやバッファ管理などの処理が含まれます。

外部プロセスの実行

ElispからPythonスクリプトを呼び出す方法を学びました。

最初は call-process で同期的に実行していましたが、Gemini APIからのレスポンスに数秒かかってEmacsが止まるので、make-process を使って非同期処理に変更しました。

;; 同期処理版
(call-process "python3"                 ; 実行するプログラム
              nil nil nil               ; 入出力設定
              "check.py" text prompt)   ; スクリプトと引数

;; 非同期処理版
(make-process
 :name "sakubun-check"
 :buffer buffer
 :command (list "python3" "check.py" text prompt)
 :sentinel (lambda (proc event)
             ;; Pythonスクリプトの実行が終わったときにこの関数が呼ばれる
             (message "チェック完了!")))

call-processの引数は、第1引数がプログラム名、第2〜4引数が入出力設定、第5引数以降がプログラムに渡す引数です。

非同期処理版では、make-process:sentinelに実行終了時の処理を指定します。これにより、文章チェック実行中もEmacsを操作できるようになります。

レキシカルスコープ (クロージャを使うための設定)

非同期版を実装したときに遭遇したエラー:

Symbol's value as variable is void: callback

原因は、Emacs Lispのデフォルトが「動的スコープ」で、lambda式が外側の変数をキャプチャできないことでした。

;;; -*- lexical-binding: t -*-

ファイルの先頭にこの1行を追加すると、JavaScriptやPythonのようなレキシカルスコープ(クロージャ)が使えるようになります。

動的スコープは1980年代のLispの名残で、今では後方互換性のために残されているだけとのことでした。こういうのなんだか面白い。

エラーハンドリング (try-finally)

メモリリークを防ぐために、途中でエラーが起きてもクリーンアップの処理は必ず実行したいというユースケースがよくあります。

(let ((buffer (generate-new-buffer "*temp*")))
  (unwind-protect
   ;; メイン処理(エラーが起きるかも)
   (call-process ...)
   ;; 必ず実行されるクリーンアップ
   (kill-buffer buffer)))

unwind-protectは、JavaScriptのtry-finallyやPythonのtry-except-finallyと同じ役割で、正常終了でもエラーでも、必ず処理を実行してくれます。

データ構造 (cons cellでの値の返却)

Pythonスクリプトから終了コードと出力結果の2つの情報を同時に受け取るために、cons cellを使っています。

;; Pythonスクリプトの実行結果を返す
(cons 0 "添削結果のテキスト")           ; 成功時: 終了コード0
(cons 1 "Error: API key not found")  ; 失敗時: 終了コード1

;; 結果を受け取って処理する
(lambda (result)
  (if (= (car result) 0)
   ;; 成功: 添削結果を表示
   (sakubun-show-result (cdr result))
   ;; 失敗: エラーメッセージを表示
   (message "エラー: %s" (cdr result))))

carで終了コード、cdrで出力結果を取り出せます。この命名は歴史的なもので、“Contents of Address Register”と”Contents of Decrement Register”の略だそうです。初見だと謎でした。

設定値の定義 - defvar vs defcustom

プロジェクトのディレクトリパスなど、設定値を定義する方法が2つあります。

;; defvar: 内部変数
(defvar sakubun-dir "~/work/...")

;; defcustom: ユーザー設定可能
(defcustom sakubun-dir "~/work/..."
  "Directory path..."
  :type 'directory
  :group 'sakubun)

defvarで機能としては十分ですが、defcustomを使うことで、ユーザーがM-x customizeでGUIから設定変更できるようになります。型情報(:type 'directory)も指定できるため、ファイル選択ダイアログが使えるようになるなどの利点があります。

標準ライブラリの活用

AIが最初に生成したコードには、カーソル位置の段落を取得する少し複雑な処理がありました。

(save-excursion
  (buffer-substring-no-properties
   (progn (backward-paragraph) (point))
   (progn (forward-paragraph) (point))))

こんなlow-levelの実装をすることに違和感があったので、組み込み関数がないのか聞いてみると、これはEmacs標準のthing-at-pointで置き換えられることを知りました。

(thing-at-point 'paragraph t)

まとめ

AIに生成させたコードを題材にすることで、教科書を読むよりも実践的にElispを学べました。特に良かった点は下記です。

一方で、AIが生成するコードは車輪の再発明をしていることもあるので、標準ライブラリやドキュメントを自分でも調べることが重要だと感じました。

今後は、このsakubun-checkerをさらに拡張して、どういう間違いをおかしやすいか統計処理をしたり、英語版に対応したりしたいと思っています。


Share this post on:

Previous Post
X11で左右のAltキーを区別する
Next Post
Vibe-CodingにADRを導入して開発体験を改善する試み