プログラミング
EmacsはLispボードである
Emacs Is a Lispboard (en.andros.dev)
要約
Emacsは単なるテキストエディタではなく、組み込みLispインタプリタとクロスプラットフォームランタイムを持つ完全な作業環境であると論じています。ターミナルエミュレータやファイルマネージャーのように、Emacsはオペレーティングシステム上で動作し、多くのプログラムの機能を再実装できるフレームワークです。筆者は、Emacsを電子工作のブレッドボードに例え、Lispモジュールを組み合わせて独自の作業環境を構築できる「Lispボード」と呼んでいます。
全文翻訳
もしEmacsのデフォルトバッファがターミナルエミュレータだったらどうなっていたでしょうか?外部から見れば、Lispをスクリプト言語とする強力なターミナルとして認識されていたかもしれません。あるいは、iTerm2対Emacs、Kitty対Emacs、あるいはBash対Emacsといった比較がされていたかもしれません。もしデフォルトバッファがファイルマネージャーであるDiredだったらどうなっていたでしょうか?歴史はTotal Commander対Emacs、あるいはFinder対Emacsといったライバル関係を生み出していたかもしれません。しかし、歴史は別の道をたどりました。
どのような設定もなしにEmacsを起動すると、以下のメッセージが表示されるバッファが現れることは、私たちはすでに知っています。
;; このバッファは保存されないテキストと、Lispの評価のために使用されます。
;; ファイルを作成するには、「C-x C-f」で開き、そのバッファにテキストを入力してください。
組み込みLispインタプリタを備えたメモ帳です。その自然なライバルは、組み込みインタプリタ(Vimscript)を備えたVimです。しかし、私たち全員に:q!を覚えさせたエディタはバッファにとどまりましたが、Emacsはますます多くのソフトウェアを取り込み拡張し、オペレーティングシステムのインターフェースとなりました。
私はEmacs愛好家であり、おそらくこの記事の信頼性を損なうかもしれません。しかし、私はGNUエディタがいかに素晴らしいかを伝えたり、それを使うように説得したりするためにここにいるのではありません。私の目標は、WebブラウザがEmacsの真の自然なライバルである理由、そしてEmacsがいかにしてあなたが望む働き方を構築できるLispボードであるかを説明することです。
完璧な作業環境を想像する
状況を整えるために、少し思考実験をしてみましょう。個人的なものと仕事上のもの、両方のコンテキストすべてに対応できる単一の場所で働きたいと仮定します。そうすれば、ショートカットを覚え、設定を適用し、コンテキストを切り替える必要がなくなります。すべてが一つのウィンドウの下に存在します。これは問題です。なぜなら、互いに関連性のないプログラムであっても、連携する必要があるものをマージする必要があるからです。例えば、ターミナルエミュレータ、メールクライアント、コードエディタ(またはIDE)などです。
レイアウトシステムがあれば、同じウィンドウ内で、一つの列にメール受信トレイ、別の列にターミナル、そして3つ目の列に私が書く必要があるものを何でも書く準備ができたIDEを配置できます。この空間では、Bashスクリプトを書き、ターミナルから実行し、結果をメールで受け取ることが容易になります。コンテキストを切り替えることなく、同じウィンドウ内で、同じショートカット、同じタイポグラフィ、同じテーマを使用できます。さらに、スクリプトをバージョン管理してリモートリポジトリにプッシュするためのGitクライアントを備えた4つ目の列を追加することさえできます。あるいは、せっかくなので、私の好きなAIエージェントを備えた5つ目の列を追加し、コードと他のすべてのパネルを読み取れるようにすることもできます。
私たちはインターフェースを作成しました。しかし、これが最も有用な部分ではありません。プログラムは互いに統合され、それぞれが独自のスコープで、同時にグローバルなスコープで動作します。これは、コードエディタで特定の構成のスペルチェッカーを有効にすると、それがターミナル、メッセージ作成中のメールクライアント、またはコミットを書く際にもアクティブになることを意味します。すべてが同じインタラクティビティと構成で動作します。これらはパイプで接続された4つの独立したバイナリではありません。それらは同じ言語で書かれ、同じランタイムで実行され、同じイベントバスを共有するプログラムです。私たちはコネクタを作成しました。
次に、不要なレイヤー、つまりBashスクリプトを削除しましょう。このウィンドウ自体にインタプリタが組み込まれている場合、独自のスクリプトを作成できます。外部バイナリは必要ありません。ウィンドウを離れる必要も、オペレーティングシステムに触れる必要もありません。私のすべてのスクリプト、プロトタイプ、ワークフローは一緒に存在できます。私たちはハビタットを作成しました。
残る問題はランタイムだけです。明日、ラップトップを変更したり、サーバーから作業したりしたい場合、ゼロから環境を再構築するつもりはありません。スクリプトがオペレーティングシステムやハードウェアに関係なくまったく同じように動作することを望みます。スクリプトは、Windows、Raspberry Pi、または安価なAndroidデバイスで同じように動作するはずです。環境全体が、どこでも実行できるバイトコードを持つクロスプラットフォームランタイムである必要があります。私たちは仮想マシンを作成しました。
最後に、このアイデアを完成させるために、スクリプト言語としてLispを使用し、全体をEmacsと呼びます。ただし、私は別の呼び方をする方が好きです。Lispボードです。電子工作のブレッドボードのように、接続するすべてのモジュールが他のすべてのモジュールとバスを共有するボードですが、ここではモジュールはLispで書かれています。
そして、この演習中に「これは聞き覚えがあるな:タブ、アプリケーション、クロスプラットフォーム仮想マシン…それは私のブラウザじゃないか」と思ったなら、その直感を大切にしてください。記事の最後にそれに触れます。
Emacsは何ではないか?
これまでに述べたすべてをもって、私はEmacsを取り巻くいくつかの神話を覆したいと考えています。
それはテキストエディタではありません。組み込みスクリプト言語、独自のソフトウェア、クロスプラットフォームランタイムを備えた完全な作業環境です。エディタは、ターミナル、メールクライアント、IRC、ファイルマネージャー、またはWebブラウザ(はい、EmacsにはWebブラウザが付属しています)と同様に、その環境内の単なるプログラムの一つです。
それはオペレーティングシステムではありません。オペレーティングシステム上で動作し、そのオペレーティングシステムが提供する多くのプログラムを置き換えることができる作業環境です。しかし、ドライバー、カーネル、低レベルツールはありません。オペレーティングシステムなしでは実行できません。
それはIDEではありません。特定の言語に焦点を当てたデフォルトのツールのセットはありません。そのためには、パッケージとしてインストールされるLSPバックエンド、デバッガ、リンターが必要です。しかし、確かにそこまで到達できます。それはその最も一般的な用途の一つです。
それは何であるか:オペレーティングシステム上で可能な限り最も非依存的な方法で実行され、オペレーティングシステムが提供するプログラムの大部分を再実装するLispインタプリタとフレームワークです。
私のワークフロー
スクリプティングとプロトタイピング
「タスクを自動化するための簡単なBashスクリプトを書こう」と思ったとき、私は「Elisp関数を書こう」と思います。そして、必要に応じて、フラグやテキスト入力を提供するためにグラフィカルインターフェースを追加します。例えば、CI/CDプロセスやcronのためではなく、自分自身のためにスクリプトを作成する必要があるとします。目標は、特定のフォルダをバックアップすることです(おそらくクラウドに)。次のようなコマンドに至るかもしれません。
./backup.sh /home/user/folder /home/user/backup
しかし、zipを作成するフラグも追加します。
./backup.sh -z /home/user/folder /home/user/backup
しかし、ターミナルを経由する代わりに、同じことを行うElisp関数を作成し、小さなパネルを追加します。次のように実行します。
M-x my/backup RET
すると、次のようなインターフェースを備えたバッファが表示されます。
フォルダバックアップ
フォルダ: [ /home/user/folder ]
ターゲット: [ /home/user/backup ]
[ ] Zipに圧縮
[ キャンセル ] [ 実行 ]
入力フィールド、チェックボックス、ボタン付きです。どこにあったか、パラメータ、またはその順序を覚える必要はありません。
Lispを使ったことがあるなら、コードは非常に基本的であることがわかるでしょう。
(require 'widget)
;; ロジック
(defun my/backup-run (folder target zip)
"Sync FOLDER into TARGET with rsync, or compress it to a zip when ZIP."
(let* ((folder (expand-file-name folder))
(target (expand-file-name target))
(command (if zip
(format "zip -r %s %s" (shell-quote-argument (expand-file-name (format-time-string "backup-%Y-%m-%d.zip") target))
(shell-quote-argument folder))
(format "rsync -a %s/ %s" (shell-quote-argument folder)
(shell-quote-argument target)))))
(async-shell-command command "*Backup log*")))
;; UI
(defun my/backup ()
"Open a panel to back up a folder."
(interactive)
(switch-to-buffer "*Backup*")
(kill-all-local-variables)
(let ((inhibit-read-only t))
(erase-buffer))
(remove-overlays)
(widget-insert "Folder backup\n\n")
(let* ((folder (widget-create 'editable-field :format "Folder: %v\n" "/home/user/folder"))
(target (widget-create 'editable-field :format "Target: %v\n" "/home/user/backup"))
(zip (widget-create 'checkbox nil)))
(widget-insert " Compress into zip\n\n")
(widget-create 'push-button :notify (lambda (&rest _) (kill-buffer)) "Cancel")
(widget-insert " ")
(widget-create 'push-button :notify (lambda (&rest _) (my/backup-run (widget-value folder) (widget-value target) (widget-valu