Lispの世界をナビゲートする:Common Lisp、Racket、Clojure、Emacs Lispの比較分析

Lisp系言語はしばしば丸括弧の塊として見られますが、その表層の下には多様な方言のエコシステムがあり、各々が特定の環境向けに調整されています――Emacsの拡張可能なコアからClojureのJVMベースの並行性まで。これらの言語間を行き来する開発者にとって、課題はコアロジックというよりも、構文、標準ライブラリ、実行モデルの微妙な変化です。

本分析はCommon Lisp、Racket、Clojure、Emacs Lispの包括的な横比較リファレンスをまとめ、Lisp哲学の実装においてどこで一致し、どこで分岐しているかをハイライトします。

実行モデルとツール

4つの方言すべてがRead-Eval-Print Loop(REPL)を提供していますが、本番環境への道筋は大きく異なります。

  • Common Lisp (SBCL): 機械コードへのコンパイルに重点を置いています。コミュニティの貢献者が指摘するように、SBCLはデフォルトでコードをコンパイルすることが多く、REPL内でも解釈とコンパイルの境界が曖昧になります。
  • Racket: 洗練されたモジュールシステムと raco ツールセットを提供し、コンパイルやパッケージ管理が可能です。#lang 宣言を用いてスタンドアロン実行ファイルを簡単に生成できる点が特徴的です。
  • Clojure: Java Virtual Machine(JVM)と深く統合されています。その実行はJavaエコシステムに結びついており、.jar ファイルやJVMのメモリ管理・スレッドモデルを利用します。
  • Emacs Lisp: 主にEmacsエディタの拡張言語として設計されています。パフォーマンス向上のためにバイトコンパイルをサポートしていますが、主な実行環境はエディタ自身のプロセス内です。

コア構文の相違点

真偽

初心者にとって最も衝撃的な違いの一つは「truthy」および「falsy」値の定義です。Common Lisp と Emacs Lisp では nil と空リスト () は同義で、false と評価されます。Racket はより厳格で、#f(または false)だけが false とみなされ、null() は true と見なされます。Clojure は中間的な立場で、falsenil は falsy ですが、空リスト () は truthy です。

変数のスコープと代入

変数宣言はローカルに let、グローバルに def/define を使用するという一般的なパターンに従いますが、細かな違いが重要です。

  • Lisp-1 vs Lisp-2: Common Lisp と Emacs Lisp は「Lisp-2」で、関数と変数の名前空間が分かれています。シンボルは同時に値と関数の両方に解決できます。Clojure と Racket は「Lisp-1」で、シンボルは特定の環境で単一のエンティティを指します。
  • Dynamic vs Lexical Scope: Emacs Lisp は歴史的に動的スコープに依存してきました。lexical-let が導入されてレキシカルスコープが提供されましたが、古いバージョンのデフォルト動作は Clojure や Racket の厳格なレキシカルスコープとは異なることが多いです。

データ構造のニュアンス

リストの進化

cons セルは Lisp の祖先的な核ですが、現代の方言は進化しています:

  • Clojure の永続データ構造: Common Lisp や Racket のリンクリストとは異なり、Clojure は永続的で不変なデータ構造を使用します。そのため cons の挙動は異なり、第二引数はリストでなければならず、元のリストは変更されません。
  • car/cdr の遺産: 多くの方言は依然として car(先頭)と cdr(残り)をサポートしていますが、より可読性の高い firstrest への移行が顕著です。Clojure はさらに firstnextnext は単一要素リストで nil を返し、rest は空シーケンスを返す)で簡素化しています。

配列と辞書

固定長配列(ベクター)は至る所にありますが、可変性は方言によって異なります。Racket は不変ベクター(#())と (vector ...) で作成される可変ベクターを区別します。Clojure は不変マップとベクターに大きく依存しており、さまざまなシーケンス型に対して一貫した API を提供します。

高度な言語機能

マクロとハイジーン

マクロは Lisp の「決定的な機能」であり、言語を拡張できます。しかし、安全性へのアプローチは異なります:

  • ハイジーンマクロ: Racket はハイジーンマクロの金字塔であり、マクロ展開が周囲のスコープから変数を誤って捕捉しないことを保証します。
  • 非ハイジーンマクロ: Common Lisp と Emacs Lisp のマクロはハイジーンではありません。名前衝突を避けるために、開発者は gensym を使って手動でユニークなシンボルを生成する必要があります。

例外処理とリスタート

Common Lisp は強力な「条件システム」を提供し、エラーの検出と解決を分離します。restart-case により、プログラマはエラーから回復する複数の方法を定義でき、ハンドラがスタックを巻き戻さずに呼び出すことができます。この機能は他の3つの方言にはほとんど存在しません。

まとめ比較表

機能 Common Lisp Racket Clojure Emacs Lisp
名前空間 Lisp-2 Lisp-1 Lisp-1 Lisp-2
偽値 nil, () #f false, nil nil, ()
マクロ 非ハイジーン ハイジーン 半ハイジーン 非ハイジーン
主な対象 ネイティブ/機械 ネイティブ/バイトコード JVM Emacs エディタ
デフォルトスコープ レキシカル レキシカル レキシカル 動的/レキシカル

最終的な洞察

Lisp ファミリーの分散はしばしば採用の障壁と見なされますが、あるコミュニティメンバーが指摘したように、Lisp を書く体験はその美しく流れる概念ゆえに「詩を読む」ように感じられます。Common Lisp の産業レベルのコンパイル、Racket の学術的厳密さ、Clojure の JVM との相互運用性、あるいは Emacs Lisp のエディタ統合が必要であっても、核心となる哲学は変わりません:コードはデータであり、言語はプログラマのためのキャンバスです。

Sources