200x: Haskellと英語
2000年代後半、プログラミングを勉強しようと思った私は、Paul GrahamとJoel Spolskyという人々が書いた文章の日本語訳を見つけ、関数型言語を覚えることにした。関数型言語を色々と調べているうちに、Haskellという言語が最も勉強すべき言語であると知り、Haskellを覚えることにした。
Haskellは1980年代に始まった比較的古い言語だ。Haskellの開発にはmottoがあった。Avoid success at all costsだ。私はこれにinspirationを得て、Avoid Japanese at all costsを自分のmottoとすることにした。日本はもうだめであり、海外で働くためには、英語での仕事のパフォーマンスを最大化する必要がある。母語である日本語でのパフォーマンスよりも、英語でのパフォーマンスのほうが高くならなければならない。プログラミングを日本語で学習してしまうと、自分の考えのすべてを英語で説明することができないのに理解したつもりなってしまうだろう。自分に都合のいいように日本語を使ったり英語を使ったりすると、結局どちらの言語でも100%理解できていないことになる。ソフトウェアエンジニアリングを純粋に英語で学びたかった私は、日本語のインプットも日本人との交友関係も遮断して引きこもることにした。
実際には、日本人英語学習者が「英語で考えられる」ようになることを目標とするのは誤りだ。少なくとも、英語教育の研究者である青谷正妥という人は著書の中でそのようなことを述べていた。日本の「伝統的な」英語教育は誤りで、言語はspeakingが基礎である(そうすべきである)。しかし当時の私はspeakingをカジュアルに練習できる環境になかった。まあこれは言い訳かもしれない。
2017: Vimを捨てる
2017年、私はEmacsのディストリビューションであるSpacemacsを使い始めた。それまで私は主にVimを使っていたが、Emacsに切り替えたのには次の理由があったと記憶している。
- VimはUNIX哲学の"do one thing well"に従っている。言い換えれば、VimユーザはVim以外のツールも色々と使う。
- それらのCLI/TUIツールの設定ファイルに互換性がなく、ツールごとに同じカスタマイズを繰り返さなければならないし、OSが変わったときにも問題が起きやすい。Emacsはクロスプラットフォームなので、.emacs.dをどのOSでも使える。※当時の私は、Nixのhome-managerを使っていなかった。Linuxデスクトップ環境の再現性を確保するためにAnsibleを使っていたが、ディストリビューションが変わるだけでも面倒だった。
- VimとそれらのTUIツールの間のウィンドウ切り替えが煩雑で、外国語(英語とは限らない)で思考しているワーキングメモリーが限られた状態ではソフトウェアの状態を記憶しておくことが難しく、パフォーマンスが低下する。
EmacsにはM-x
があり、これは英単語を入力すると関連するコマンドが選択肢として出てくるというものだ。Sublime Text以降のエディタには大抵Command Paletteの機能があるが、Emacsほどユーザが自由自在にコマンドを定義して容易に検索できる環境は他にない。これは自然言語で考える人間にとって優れたUIだ(最近のウェブアプリケーションにはもっと強力な検索機能があるが)。
つまるところ、私がVimを使うのをやめたのは、頭に入れておかないといけない状態の数を減らしたいからだった。2010年代は、今では問題になっている移民が流行していた。人間にはたとえば、どの自然言語で会話すべきかという状態変数もある。外界では既に状態の数が爆発しているので、ソフトウェアの状態はなるべく少ないほうがよい。
もう一つの理由はOrg (org-mode)だ。Vimを使っている人の中にもOrgに興味を持っている人はいるだろう。 (Neo)Vimを使いながら、org-modeのためだけにEmacsを使っている人もいるようだ。 文章を書くためのエディタと、コードを書くためのエディタを使い分けるなんて、面倒ではないだろうか。 それがEmacsの考え方だ。
私がVimを使っていた痕跡が残っている。 削除する前にコミットしておいたらしいneovimの設定のREADMEに、次のような説明がある。
I have almost switched to Spacemacs, so I rarely use NeoVim now. I just keep these files in case I switch back to vim in the future.
結局その後、私がVimに戻ることはなかった。
2018: Emacsコミュニティとの出会い
Spacemacsは重いので、自分用のEmacsの設定を書き始めることにした。それまで私は、Emacsの中でVimをエミュレートするevil-modeを使っていたが、設定がややこしくなるのでやめることにした。そもそもVim (evil-mode)の編集モードも状態だ。Vim (evil-mode)の操作は編集モードに依存するので、外国語(英語とは限らない)で思考している、ワーキングメモリーが限られた状態では、編集モードを記憶しておくことが難しく、パフォーマンスが低下する。
何の設定も入っていないデフォルトのEmacsを使ってEmacsの設定を書いていくのは辛そうだと思ったので、Spacemacsを使いながら新しいEmacsを起動して試せるように、Emacs Lispコードを書いた。このコードを切り出してMELPAにPRを送ったところ、Adam Porter (alphapapa)氏からパッケージの名前についてコメントがあった。氏は私のGitHubアカウントの最初の相互フォロワーとなった。それから当時のMELPAメンテナであったSteve Purcell氏の支援もあって、これが私の最初のEmacs Lispパッケージとなった。Redditでアナウンスしたところ、スターが10個くらい付いた。
ペットが餌を貰う方法を学習するように、私はMELPAにPRを送るとフィードバックが得られるということを学んだので、それからしばらくEmacs Lispばかり書いていた。Redditに入り浸り、当時のEmacsについてのベストプラクティスを学び自分のemacs.dに取り入れた。日本とアメリカは時差があったので、私は昼間の中にEmacs Lispを書いてもアメリカに住んでいる人から即座にコメントをもらうことができた。自分の英語での思考力(英語でもらったコメントにその場で英語で返す能力)に限界があることも感じたし、自分の限界を知ることができるのはよいことだ。楽しかった。そして自然言語の学習にはEmacsが重要だ。ポジティブフィードバックが機能しているように思えた。
就職してNixを使い始める
その後、twitterでたまたま遭遇したアカウントの紹介で、間違えてSESで働くことになった。6か月の契約社員(更新あり)という雇用契約だったが、最初に2か月連続で給料を遅配されたので私は契約更新を拒否した。
仕事ではEmacsを使えなかったが、仕事以外ではEmacsを使いたかった。私はEmacs以外の環境構築をしたくなかったので、Chromebookを購入してChromeOS(当時はChrome OSという間にスペースが入る名称だった)のLinuxコンテナでEmacsを使い始めた。ChromeOSにデフォルトで入るLinuxディストリビューションはDebian stableであり、Emacsのバージョンが古かったので、Emacsコミュニティの人々が使っていたNixパッケージマネージャーで最新バージョンのEmacsをインストールすることにした。これが私とNixの付き合いの始まりであり、最初はnix-env
でEmacsをインストールする以外には使っていなかった。
2019: フルタイムでEmacsを使い始める
それから、twitterでたまたま知り合ったpiacere_exさんの紹介で、福岡の受託会社に転職した。仕事でEmacsを使えるようになった。Nixのhome-managerを導入した。私はNix言語の経験がなかったので、まとまった時間が取れる5月の連休中に実行した。そもそも私がEmacsを使い始めたのは、Emacs以外のカスタマイズをしたくないからというのが理由だったが、WSLの設定が必要になった。WSLに関係する最小限の設定を、取引先企業のPCと所属企業のPCの間で同期するために、home-managerを使うことにした。日本の会社で働くために日本語入力も必要なので、mozcのバイナリをEmacs側のリポジトリにNixで入れた。
MELPAレシピ
ある日、GitHubでconao3というアカウントによるactivityがあった。その数日前にも何か言われていたが、私は炎上案件を終えたばかりで眠かった。
問題は、私が単一のGitリポジトリに新たなEmacs Lispパッケージを追加したことで既存のMELPAレシピ(Emacs Lispのパッケージをビルドするためのパッケージの定義)に問題が発生し、ビルドエラーが起きていたことだ。私はパッケージマネージャとしてEmacs標準のpackage.elではなくstraight.elを使っていたので問題なく使えており、レシピの間違いに気が付かなかった。
私は修正のPRをmelpa/melpa
(本家)に出したのだが、(私の知識不足と眠かったので)模範的な解答ではなく、まだマージされなかった。conao3はコードを提示したものの、何を言っているのか私にはよくわからなかった(彼は当時広島大学の学生だったから英語圏の大学でacademic writingの訓練を受けていないのだろう。私も受けたことはないが)が、Purcell氏が私にも理解できる自然言語で説明してくれたので、私はPurcell氏のコメントの通りに修正した。これがconao3の怒りを買った。実は私がmelpa/melpa
(本家)のPRを修正する前に、conao3はakirak/melpa
(クローン)にconao3自身が出していたPRをforce-pushで修正していた。私は先行していたconao3のakirak/melpa
へのforce-pushに気づかずmelpa/melpa
のPRに同一の修正を加えたため、私がmelpa/melpa
のPRにおいてconao3のakirak/melpa
へのPRの内容を剽窃したと訴えられた。
私はOSS活動一般へのリスペクトがないとconao3に非難され、最終的には彼の指示通りに彼をco-authorに入れることで決着した。conao3には次のように指導された。
I recommend you modify recipe before adding files as different packages.
しかし私の信念では、人間の注意力は当てにならない。どうすれば同種のエラーを(おそらくはCIで)再発防止できるだろうかと私は悩み始めた。再発防止策を編み出すまでは新しいEmacs Lispパッケージを出すことはできないとも思った。私は本業のためにWeb開発の勉強をしたかったのだが、Emacsについて悩んでいた。
conao3はまた次のように言っていた。
I thought I was too interference you, sorry.
英語として解釈しようとすると何を言っているのかわからないが、日本語に直訳すると既視感があった。私が20代のときに、40代のおじさんに言われたのと同じ発言だ。conao3は学生でありながら既に老害だ。そのおじさんはまた、私の年下の京大生を潰したことがあった。conao3は将来パワハラおじさんになるだろうと思った。conao3は私がGitHubでフォローした最初の日本人だったのだが、フォローを外した。
2020: 歴史は韻を踏む
それから間もなく、COVID-19への対策として引きこもり生活が始まった。MELPAレシピのエラーを防ぐためのEmacs LispのCIについて、Nixを使うべきだという仮説を立てた私は、実装してmelpa-checkというパッケージのPRを出した。これについてconao3が噴き上がることは予想していた。当時の私のNixスキルはまだ貧弱で、結局このPRはマージされなかったが、新たなつながりができた。
同じ問題に対してconao3が代案として出したのがKegだ。彼が以前から温めていたアイデアだったのだろう。それから彼は同じリポジトリにkeg-mode, flycheck-kegとパッケージを追加していき、一年前の私と同じ間違いを犯した。私は彼の間違いを指摘し、コメントにて正解を示した。すると彼は、私が示した通りに修正したが私をco-authorに追加しなかった(確かに私はこのときPRを出さなかったが、一年前の私は彼のPRを見なかったにもかかわらず私は彼をco-authorに入れた)。なんだconao3は自分に甘く他人に厳しい嘘つきかと思い、私は彼をGitHubでブロックした。やはり日本人は、たとえフォロワーの多いオープンソース開発者であろうと信用できないと思い、それ以来私はGitHubで日本人を一人もフォローしていない。同時に、conao3が出したPRをそのままマージしたMELPA、ひいてはEmacsコミュニティに対する信頼も失うことになった。
Zettelkasten
この年、Emacsコミュニティではorg-roamが話題になっていた。私はorg-roamの元になったRoam Researchというアプリケーションを試用し始め、Zettelkastenのハックをしていたので、Emacsにはあまり時間を費やさなくなった。それに、本業でも準委任のJava案件から離脱したので、モダンなWeb開発を勉強しようと思っていた。
2020年前後に、LSPやtree-sitter、それにminad/oantolin両氏による新しいminibuffer completionのパッケージ群などで、Emacsの世界は大きく一新された。
2021: 新たに直面したEmacsについての課題
Java案件から離脱した私はElixirの仕事を希望していたが、当時Elixirの案件は少なかったので、そのまま何もしないわけにもいかず、私はフロントエンドをやることにした。社内のフロントエンドエンジニアにEmacsを使っている人が一人おり、私はEmacsに強かったおかげで首がつながった。
それまで私は、SIerのJava案件に100%の割合で割り当てられていた。WSL 1でEmacsを動かしており、Emacsの反応が全体的に遅かったが、ミスをしないようにというプレッシャーが強く考えながら仕事をする必要があり、スケジュールにも余裕があったので、Emacsの性能はあまり問題にならなかった。取引先からHDDのPCを貸与されており、そちらのほうが問題だった。
これがフロントエンドになると、速いエディタが必要になる。遅延評価言語Haskellで育った私のEmacs Lispコードは(Haskellのせいではなく私のskill issueだろうが)富豪的プログラミングの産物であり、膨大なメモリ消費により頻繁にGCでEmacs全体が止まることになった(GCの発動を最適化するパッケージであるgcmhは使っていたが、それでも有意にGC待ちの時間があった)。そのほかにも私のEmacsにはglobalなフックやadviceにより全体的に重しがかかっており、認知負荷・キータイプ数の軽減を重視して改良されてきた私のEmacsは方針の見直しを迫られることになった。
複数案件を並行することにもなったので、時間の記録が必要になった。これはEmacsのorg-modeにorg-clockという機能があり役に立ったが、止め忘れてしまうことがあり、運用に改善の余地があった。
twist.nix
その年に退職して無職になった。退職したのはEmacs以外にも事情があったが、私の全ての活動はEmacsから始まるので、まずは積み上がっていたEmacsの課題に向き合うことにした。
まず必要だったのはパッケージマネージャである。次の理由により、Emacsのパッケージマネージャにもロックファイルが必要だという結論に達した。
- 複数のマシンの間でEmacsのリポジトリを同期する場合、パッケージのバージョン違いにより、起動時にエラーが発生する可能性がある。このトラブルシューティングにたとえば朝の1時間が費やされると、その日のパフォーマンスは致命的。すべてのEmacs Lispパッケージのバージョンは、リポジトリにコミットされるべきだ。
- パッケージのバージョンがロックされていない場合、Emacsを新規でインストールするときは、最新のバージョンがインストールされることになる。最新のバージョンには悪意あるコードが混入されているかもしれない。特に、自分のemacs.dをpublicリポジトリとして公開していれば、その人個人を標的とした攻撃コードをどれかのパッケージ作者が仕込むのは簡単だ。本番環境のサーバを扱うSWEなら信用を失いかねない。SWEにとって、エディタのパッケージセキュリティは、就職する前に万全にしておく必要がある。
straight.elはロックファイルにopt-inで対応していたが、強制ではなかった。そもそも2019年にconao3との一件が起きたのも、straight.elのビルド方式にpackage.elとの互換性がなかったのが一因なので、straight.elを使うことはできない。しかも当時のstraight.elはバグが多かった。
私は特にconao3を怖れていた。その年のconao3は既にインターンに参加しておりEmacs界隈には首を突っ込まなくなっていたが、彼には仲間が多い。私が使っているパッケージのどれかがconao3の仲間によって書かれたものだったら、私が攻撃を受ける可能性も否定できない。私は、自分が使っているすべてのパッケージを把握する必要があり、すべてのパッケージの更新を確認する必要があった。
そこで私は、Nix-basedなEmacsのパッケージマネージャーtwist.nixを書き、自分が使うすべてのEmacs Lispパッケージがflake.lockに記録されるようにした。私が間違えてconao3のパッケージをインストールしてしまわないように。conao3のパッケージに依存する別の誰かのパッケージをインストールしてしまうことさえないように。Nix flakeなら、flake.lockに記録されていないソースを取得する処理は--impure
フラグを付けなければ実行できない。2019年から悩んでいたEmacs LispパッケージのCIも、twist.nixベースにすることでエレガントに解決できる目処が立った。
それまで私はNixをちゃんと勉強したことがなかったので、自分の知識を見直すよい機会になった。 2021年末にようやくflakeを導入したことで、物事が整然とした。
それからパッケージを安全に更新するために、flake.lockに記録されたバージョンの差分(に相当するコミットの履歴)をMagitで確認することのできるパッケージ、nix3.elを書いた。これで、conao3がいちcontributorとしてコミットし、メンテナによってマージされた差分も、私を攻撃する意図がないかどうか確認できる。
こうしてEmacsの安全な運用方法を確立し、つまり攻撃に対する防御策ができてから、攻撃手段の存在を明かした。自分の仕事に責任を持つために必要なことだった。
新しい世界の幕開け
私自身がtwist.nixを導入する際に、それまでのEmacsの設定を捨てて書き直した。このときには既に、私が理想とするEmacsのありかたも、私がEmacsを使い始めた当初とは別のものになっていた。
- Emacsは一度起動したら何ヶ月も再起動せずに使えると聞いていた。実際には私は、Vimを使っていたときほどではないにせよ、Emacsを再起動している。Emacsの性能はnative-compileやGCの改善によって向上しているが、私がGitバージョンを使うようになったせいもあってか、Emacsは意外と不安定だし、調子が悪くなったら再起動した方がよい。
- Emacsが再起動するものである以上、Emacsに状態を持たせるのは心許(こころもと)ない。Emacsに近く、それでいて分散したインフラに状態を保持させるのがよさそう。具体的なアーキテクチャはまだ描けていない。
- 純粋なEmacs Lispパッケージだけで構成された世界が理想だと思っていたが、どうやら現実的ではなさそうだ。LSPやtree-sitter、スペルチェッカーのjinxなど、Emacs本体も非標準のパッケージも、どんどん外部のコンポーネントを利用するようになっている。Nixはこういうものを統合するのに最適だ。
- Emacsを拡張するときは、最初から性能への影響を意識したほうがよい。Emacsを遅くするコードは、脂肪のように積み上がっていく。
パッケージを更新する際に差分を確認する運用にしたことで、わかったことがある。
- たくさんのパッケージをインストールすると、更新時の確認にも時間がかかるようになるので、インストールするパッケージはなるべく少ないほうがよい。Emacs本体の機能強化は重要だし、Emacsコアメンテナの責任は何よりも重く重要。
- 比較的たくさんのパッケージを書いている作者でも、デフォルト(master/main)ブランチにforce-pushする人がいる(この人のパッケージは避けるようになった)。
- 重要なパッケージでも、個別のコミットを見るとあまり同意できない決定をしていることがある。頑張り過ぎでは?と思わせることも。ただし、重要なパッケージだし他にやってくれる人もいないだろうから、私も少しだけ応援(寄付)する。
org-modeについてだが、私はorg-roamもdenoteも使っていない。自分のデータがメンテナの意向次第で使えなくなってしまうのは嫌なので自分でパッケージを書いているが、私も他人のデータの将来に責任を持てないので自分のパッケージを他人に共有するつもりがない。最大公約数であるorg本体のメンテナの責任は重い。
Emacsはstatefulでmutableだったが、statelessでimmutableな方向に変わっていく。 EmacsはVimのようになっていくと言えるかもしれない。つまり、収斂進化だ。 面白いでしょ?