Schemeの式をEmacsで簡単に実行する

この記事は、ISer Advent Calendar 2021の21日目の記事です。前日の記事はfushiguさんによる「噂のCPU実験」でした。翌日の記事は81uemanさんによる「ノルムで遊ぼ」です。

この記事では、SchemeOCamlの式を簡単に実行するためのEmacsの設定を書き残しておきます。なお、SchemeOCamlを書くことになったのは、学科の実験(プログラミング課題)のためなので、そのことが前提となっています。

なぜEmacs

VS Codeで書いてもいいんですが、2Aの実験のサイト*1で、Emacsを使うための設定が書かれているため、とりあえずEmacsを使うか~となりました。

使ってみると、いろいろカスタマイズができることがわかりました。こんな感じで、キーボードショートカット一つで、1画面で結果が表示されるようにできました。 f:id:ryu_tk:20210902161016p:plain

Scheme on Scheme*2までたどり着けたのも、これがあったからかもしれません。

特に、Schemeの課題の大部分やOCaml課題*3の前半は「フィボナッチ数列の第n項を求める関数fibを定義せよ。」のような小さな関数一発であることが多いので、毎回コンパイルするよりも、ショートカット一発でできるこちらのほうが楽でした。*4

この手法の限界

ただし、Scheme on Schemeのように、大量の関数を定義するような大きいファイルは、毎回実行のショートカットを連打するよりも、素直にターミナルでコンパイルしたほうが早いかもしれません*5。そのうえ、Emacs上のScheme処理系は重いので、guileだと動くはずのScheme on Scheme on Schemeが動かなくなった気がします。

それに、VS Codeのほうが大量のファイルを管理するのは楽なので*6、それが求められる場面(OCaml課題の後半のOCaml処理系作成、など)ではVS Codeのほうがいいかもしれません。(僕は3SまではEmacs + ターミナル + Makefileで頑張りましたが、3Aのコンパイラ実験でVS Codeを使い始めました。)

環境

Win10に乗っているWSL1です。 このサイトとかが参考になると思います。 GUI(X Server)を動かすのに苦労した記憶があるんですが、なぜだか具体的な手順を覚えていないです。~/.bashrcに

export DISPLAY=:0.0
export PATH=~/anaconda3/bin:$PATH

という文言があることだけは確かです。

前提知識

Emacs教習所にお世話になりました。ここのinit.elの設定は、undo-treeと見た目の変更、multi-term以外はやりました。(undo-treeは入れたかったんですが、なぜか激重になったので諦めました……) また、実験のサイトの設定も追加しました。

本題

ただ、実験のサイトの設定だけだと、ターミナルがEmacsの画面上で開くのみです。

そこからさらに一歩進めて、「今開いている.scmファイルの、カーソルの位置の式 *7 を、キー操作一つで開いたターミナルに打ち込める」ことができれば便利です。発想としては、C-c C-eで使える(eval-last-sexp)に近いですが、もっと便利です。

挙動をもう少し詳しく説明しておくと、

  • C-c C-j で、現在のカーソルの位置の式を処理系に打ち込む。
  • このとき、現在Scheme処理系が立ち上がっていないならば、自動的に開く。*8
  • このとき、現在Scheme処理系が立ち上がっているもののそのウィンドウが開かれていないならば、そのウィンドウを自動的に開く。

みたいな処理をします。

それを実現するために、init.elに次のようなコードを書きました。*9

かなりガバガバなコードな気がします。怒らないでください。

(defun my-execute-scheme ()
  (interactive)
  (if (string= mode-name "Scheme")
      (progn (run-scheme scheme-program-name)
             (end-of-buffer)
             (other-window -1)
             (scheme-send-last-sexp))
    )
  )

(add-hook 'scheme-mode-hook
          '(lambda ()
             (define-key scheme-mode-map (kbd "C-c C-j") 'my-execute-scheme)))


(defun my-execute-scheme-all ()
  (interactive)
  (if (string= mode-name "Scheme")
      (progn (run-scheme scheme-program-name)
             (end-of-buffer)
             (other-window -1)
             (scheme-load-file (buffer-file-name (current-buffer))))))
;;(global-set-key (kbd "C-c C-a") 'my-execute-scheme-all)
(add-hook 'scheme-mode-hook
          '(lambda ()
             (define-key scheme-mode-map (kbd "C-c C-a") 'my-execute-scheme-all)))

とりあえず、Emacsで適当に.scmファイルを開き、適当に任意の式を書いて、任意の式の後ろでC-c C-j *10してみると挙動がわかると思います。

解説

(interactive)

コマンドを書くためには、関数の最初にこれを入れることが必要らしいです。

  (if (string= mode-name "Scheme")

「もし現在のモード名が"Scheme"なら」。Scheme以外の言語でSchemeの処理系が開かれると困ります。*11

      (progn (run-scheme scheme-program-name)
             (end-of-buffer)
             (other-window -1)
             (scheme-send-last-sexp))

⓪現時点では、フォーカスは.scmファイルを編集しているウィンドウ(以下「編集ウィンドウ」)にあるはずである。

① (run-scheme)でschemeを起動する。このとき、(defadvice run-scheme ...)の影響で、最終的には「編集ウィンドウとguileが走っているウィンドウの二つがあり、後者にフォーカスがある」状態になる。*12

②よってこのバッファの最下部に飛んでから、

③編集ウィンドウに戻り、

④「カーソルの位置の式」をguileに送る。

すると評価結果が表示されます。

(add-hook 'scheme-mode-hook
          '(lambda ()
             (define-key scheme-mode-map (kbd "C-c C-j") 'my-execute-scheme)))

scheme-modeを起動するときに、"C-c C-j"を上述の関数に割り当てます。

my-execute-scheme-all も似たようなことをやっています。

ちなみに、OCamlProlog ver は以下の通りです。やってることはだいたい同じです。

;; https://y0m0r.hateblo.jp/entry/20130524/1369405033 から4行

(defun my-execute-ocaml ()
  (interactive)
  (if (string= mode-name "Tuareg")
      (progn (tuareg-run-process-if-needed tuareg-interactive-program)
             (if (not (string= mode-name "Tuareg"))
                 (other-window -1))
             (tuareg-eval-phrase)
             )))
(add-hook 'tuareg-mode-hook
          '(lambda ()
             (define-key tuareg-mode-map (kbd "C-c C-j") 'my-execute-ocaml)))


(autoload 'prolog-mode "prolog" "Major mode for editing Prolog programs." t)
(add-to-list 'auto-mode-alist '("\\.pl\\'" . prolog-mode))

(defun my-prolog-consult-file ()
  (interactive)
  (if (string= mode-name "Prolog")
      (progn
        (run-prolog t)
        (prolog-consult-file)
        (other-window -1))))

(defun my-prolog-query-clause ()
  (interactive)
  (if (string= mode-name "Prolog")
      (let* ((send-string
              (progn (prolog-mark-clause)
                     (buffer-substring-no-properties (region-beginning) (region-end)))))

        (progn       
          (if (eq (get-process "prolog") nil)
              (my-prolog-consult-file))
          (run-prolog)
          (end-of-buffer)
          (insert send-string "\n"))
        )
      )
)

(add-hook 'prolog-mode-hook
          '(lambda ()
             (define-key prolog-mode-map (kbd "C-c C-j") 'my-prolog-query-clause)))
(add-hook 'prolog-mode-hook
          '(lambda ()
             (define-key prolog-mode-map (kbd "C-c C-k") 'my-prolog-consult-file)))


おまけ

init.elのその他の設定のうち、自分が書いた部分のみ載せておきます。Emacs教習所にいろいろ載っています(再掲)。

smartparens

これがあるとカッコが自動で補完されて、いい感じになります。

(leaf smartparens
  :require smartparens-config 
  :ensure t
  :custom ((smartparens-global-mode . t))
)

ウィンドウの大きさの初期値を変える

デフォルトだと10行くらいしかなく、縦に狭いので、こうすると広げられます。

(add-to-list 'initial-frame-alist '(height . 35))
(add-to-list 'default-frame-alist '(height . 35))

余談

  • 一度init.elを書くと、init.elさえ移せば他の環境でも同じ環境が簡単に再現できるのがEmacsの魅力です。具体的には、私物のPCから学科PCの仮想環境への移行がスムーズでした。ここまでの設定をすればCのコードもそこそこ書きやすくなっているはずです。*13 2021年度以降は、ASMでリモートアクセスすることになるECCSの環境にも適応してくれると思います。*14
  • 最終的にはいろいろ書いたので、単にコピペするだけじゃなくて、他の人の書いたinit.elを解読したりelispのドキュメントを読んだりEmacsでdescribe-variableしたりして、自分なりにelispによるinit.elの書き方を勉強することができました。
  • elispは、Schemeと同じくLispの方言らしいです。なので、動かねえ~~~って言いながらelispのドキュメントを読んでる間、なんでLispの方言(Scheme)を学ぶためにLispの方言(elisp)を学んでるんだろう、という気持ちになりました。勉強になってよかったです。

*1:2021/12/21現在、「情報科学基礎実験」でググるとこのサイトが出てくるので、これは公開情報のはずです

*2:Schemeで、自分自身を読み込めるScheme処理系を書いてみよう!という、Schemeの課題の最終目標です。

*3:OCamlは3Sでやります

*4:Q. Code Runnerか何か入れればいいんじゃないですか? A.それでもいいんですが、Windows上にguileを入れるにしてもCode Runnerと連携させるのがよくわからず、WSL上でやるにしてもVS CodeをWSLと連携させる方法が当時よくわかりませんでした…(3Aで学科の人に手伝ってもらいながらやっとできたので、今でもよくわかっていないです)

*5:いちおうC-c C-aで全実行できるようにはしていますが…

*6:VS Codeでは、常に画面上にファイル一覧が表示されていて、GUIでそれを並べ替えたり、クリックで編集するファイルを切り替えられたりするのが非常に便利です。Emacsでもそれを実現する方法はあるかもしれませんが…あと、Emacs、ウィンドウ名が全部emacs@DESKTOP-...なんですよね。これが致命的でした。

*7:関数型言語だと、C言語における int a = 1; のような命令も、すべて「式」と呼ばれます。例えば、Schemeだとこれと等価な式は (define a 1) となります。この式を処理系に打ち込むと、「aが1である」という定義を現在の処理系で行うことができます。

*8:なお、複数ウィンドウ開いていると、現在のウィンドウの次のウィンドウが勝手にScheme処理系に移ってしまうようです。

*9:この記事のコードの著作権は放棄しますが、使うときは自己責任でお使いください。

*10: jikkou のj。あと、C-c C-eだと両方左手で使いにくかったため。

*11: (add-hook 'scheme-mode-hook ...)してるから不要っちゃ不要ですが、一応の保険です。これを書いた当時 ;;(global-set-key (kbd "C-c C-j") 'my-execute-scheme) みたいなマネをしてた名残でもあります。

*12:編集ウィンドウのみでもすでにguileのウィンドウが開いていても、まだguileが走っていなくとももうguileが走っていてもOKです。このへんについては、Scheme-mode で C-h f run-scheme したら出てくるヘルプと (defadvice) を眺めてください。

*13:2Aでは自分のPCでコードを書くことになると思います。3Sからは学科PCが学科から配布されます。このinit.elは、2Aのとき、自分のPCのEmacsのために書いたものでしたが、それをそのまま、3Sにおいて学科PCのHyper-Vで動かすUbuntuに輸入しました。

*14:2020年度ではECCSのEmacsはver22とかだったと思います。そのせいでleafが使えず、whitespaceもautocompleteもsmartparensも読み込めなかったため、泣きながら手元のVS Codeで書いたアセンブリコードをコピペしていました。