Emacs+MetalsでScalaのデバッガを使う

こういう話がありました。

Feature Request : Support for scala in dap-mode · Issue #196 · emacs-lsp/dap-mode を見てもdap-modeの使い方がよく分からなかったし別に時間をかける所じゃないなと思ったので、デバッグする時だけはIntelliJを使うことにしようとしたのですが、私の環境だとUIが崩壊してデバッグ設定以前の問題になってしまいます。

実はMetalsの(というかlsp-mode+dap-modeの)デバッガはいま普通に機能するけど、確かに使い方(使える状態にするコツ)がちょっとむずかしい感じがしますね。実際にはやることはあんまりないんだけど、いざやろうとすると時間を食うと思うので、時間を食われてやった側の人間としてやり方を書き記しておこうと思います。本当は最近のEmacsのモダンな環境ぜんぶ紹介するみたいなのを書いてそこで(他の言語の場合も含めて)書くつもりだったけど、取り急ぎScalaのことだけ書きます。

設定方法

実はこれだけ。

(add-hook 'lsp-mode-hook 'dap-mode)
(add-hook 'lsp-mode-hook 'dap-ui-mode)
(add-hook 'lsp-mode-hook 'lsp-lens-mode) ; これはdap-modeに必須ではないがScalaではほぼ必要

参考までに僕が実際に使っている設定を貼っておきます。

デバッガの使い方

lsp-modeというかdap-modeでのデバッガを利用するには、普通なら、M-x dap-debug-edit-templateデバッグテンプレートを書くか、dap-modeの各言語サポートで用意されているテンプレートを用いることになります。たとえばGoだとM-x dap-debugすると一覧が現れてGo Attach Executable Configurationを選ぶと実行中のプロセスに対してプロセスID指定でアタッチしてデバッグを開始できます。しかしScala(Metals)では(まだ)デバッグテンプレートが提供されていません。かと言って自分で書ける気がしない...... (書こうとしたことあるんだけど、なんかうまくいった試しがない。)

実はMetalsではM-x dap-debugするよりもっと簡単なやり方があって、それはCode Lensを使う方法。というかおそらくEmacs上ではいまこれがMetalsでデバッグ開始する唯一の方法なんじゃないかと思います。(リモートデバッグもBloopで最近サポートされたのでデバッグテンプレートちゃんと書けば動きそうではある。)

lsp-lens-modeを有効にしていると、mainメソッドを持つクラス(やオブジェクト)に対してrun|debugが表示されるようになります(コンパイルのタイミングとかで出ないときがあるのでそういうときは頃合いを見てM-x revert-bufferするとよさそう)。

あとはM-x lsp-avy-lens等でdebugのCode Lensを選択すると、デバッガが立ち上がります。M-x dap-breakpoint-toggleブレークポイント張れるし、M-x dap-nextで次のステップに進んだりできます(コマンド一覧はこちら)。

ちなみに、どういう原理なのかは知らないけどmainメソッドを直書きしてなくてもScalaTestなどのテストクラスにもCode Lens出るので、特定のテストクラスだけ単体で実行するのもこれでやれて非常にべんりです。

蛇足: デバッガ本当に使うのか

テストの単体実行は(debugではなくrunの方で)毎日使ってるけど、dap-modeのデバッガは正直まだ一度も役立ったことがないですね。プログラミングを初めてしたのはVisual C++ 6.0上だったので、ステップ実行してローカル変数の状態を確認しながらデバッグできるのが初学者にとって役立つのは重々承知しているものの、職業ソフトウェアエンジニアをやっていて思うのはデバッガによるデバッグは、なんというか遅い。個人的にはデバッグ方法は以下の順で早いと思っています。

  1. 念力デバッグ
    • 現象だけを見て、コードを全く見ないでバグの原因箇所を当てる
      • 「あそこのコードがこうなってたら、このバグを引き起こすよね」という仮説を立てる
      • 実際そのコードを見に行って「ほらやっぱりこうなってるからこれダメだよね、直そう」とやる
      • (原因箇所を当てるのだけやって、コードの確認と修正を他人に任せるのが真の念力)
    • 熟知したコードベースのシステムに対してしかできない
  2. コールドリーディング (本来の意味とは違うと思うけど自分の頭の中でこう呼んでしまってる)
    • コードだけ読んで、実行はせずにバグの原因を特定する
    • 「コールド」というか静的
  3. printfデバッグ
    • 関係ありそうな値を根こそぎ出力しておく
    • 実行し終わった後で落ちついて一つずつ見ていっておかしくなる箇所を探す
    • ログレベルDEBUGとかでいろいろ吐くようにしておいてそれを眺めるのも含む
    • DBに投げてるSQLをぜんぶ出力する等
  4. デバッガで追う

デバッガを使う場合、まずポチポチするのが遅い。ブレークポイントで止まったけど、ああ、この回じゃなくてもっと後で同じメソッドが呼ばれる時にたぶんおかしいんだよな、うげぇ、行きすぎた、もっかいやろう、えーとこの変数がこうなってるからもうちょっと先でダメになるんかな、あ゛あ゛あ゛タイムアウトしたやんけ、やり直し...... みたいになるんですよね。僕だけかな。使い方が下手なんだろうとは思うけど、やってるうちに「あー、もう、鬱陶しい! こんなん、関係ありそうな値をぜんぶ出力しておいて実行し終えてから見たったらええねん!」とキレてしまう。

うまく使えばきっとprintfデバッグより効率よかったりするんだろうけど、それでも原理上、念力デバッグとコールドリーディングより早いことは絶対になくて、仕事で長くメンテしているシステムのデバッグをするときなんかはだいたいそれで済んでますね。

もちろん全く使わないわけではなくて、最近だとたとえばrenovateにpull requestしたときに、挙動がよくわからなくて意図通りにいかなかったのでデバッガで追いました(ただ、Node.js環境をdap-modeでデバッグするのがうまくいかず、ブラウザからデバッグ用のポートに接続するやつでやってしまった)。

まぁいざというときのために使えるようにしておくに越したことはないとは思います。