$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

ターミナルのsbtで起動したプログラムをIntelliJのデバッガでデバッグする

最終的に結構簡単な設定で出来たのだけど、いろんな情報に右往左往させられてしまったので、自分用にメモをとっておく。

やりたいこと

Scalaで開発する時、基本的にターミナルでsbtを起動して、runとかtestとかを実行している。その時、挙動を動かしながら確認するために、ステップ実行を行いたい時がある。

ステップ実行しながらデバッグする時、IntelliJのデバッガが便利なのでそれを使いたい。

作戦

ターミナルで、sbtをJVMデバッグオプション付きで立ち上げて、そのプロセスにIntelliJのデバッガでアタッチするという方法を取る。

IntelliJの標準のScala開発環境で、「Enable debbuging for SBT shell」という設定もあるのだけど、これがなかなかうまく動かなかった。そこでこちらのやり方は諦めて、上記した方法を取ることにした。

やり方

sbtをJVMデバッグオプション付きで立ち上げる

まずsbtをJVMデバッグオプション付きで立ち上げる。最近のsbtではデフォルトでこのオプションが付いている。

$ sbt -jvm-debug 5005
Listening for transport dt_socket at address: 5005

ここで指定した5005がIntelliJのデバッガに設定すべきポート番号となる。このポート番号は自分で自由に変えても良い。

IntelliJのデバッガの設定を行う

まず「Select Run/Debug Configuration」から「Edit Configurations」に。

f:id:shiba_yu36:20170825071259p:plain

プラスボタンからRemoteを選択。

f:id:shiba_yu36:20170825071358p:plain

先程立ち上げたsbtのポートが指定されているか確認し、OKを押す。

f:id:shiba_yu36:20170825071550p:plain

これで設定は終わり。

デバッグする

あとはブレークポイントを設定してデバッグするだけ。以下のようにブレークポイントを設定して、虫のボタンを押す。

f:id:shiba_yu36:20170825072139p:plain

その後、ターミナルのsbtでrun。

> runMain SbtDebugExample
[info] Running SbtDebugExample

すると、IntelliJの画面に移ってデバッグできる。以下のように変数の内容がソースコードに出力されたり、ステップ実行できたりとめちゃくちゃ便利。

f:id:shiba_yu36:20170825072346p:plain

まとめ

色んな情報があって、ここまで設定するのに右往左往してしまったので、ターミナルのsbtで起動したプログラムをIntelliJのデバッガでデバッグする方法を自分用にまとめておいた。たぶんこの流れがわかっていれば、JVM起動する系は大体デバッグできそう?

Scalaの開発環境をセットアップした

Scalaを書くことになったので、Scalaの開発環境をセットアップした。Emacsで編集できるように

  • scala-modeを使って編集
  • ensimeを導入し、できる限り動作が重くならないように
  • tagsファイルを使って定義ジャンプをできるように
  • IntelliJEmacsを行き来できるように

あたりを行った。

scala-modeを使って編集

昔はscala-mode2を使っていたみたいだけど、最近はscala-mode を使うみたい。

(require 'scala-mode)

ensimeを導入し、できる限り動作が重くならないように

EmacsでもScalaシンタックスチェックや定義ジャンプなどを使えるように ensime を導入する。ensimeを使えるように Emacs + ENSIME でScalaの開発環境を作る - Qiita とかに書いてあるとおり、

  • Emacs上でensime packageのインストール
  • sbtでプロジェクトごとにensimeの設定を吐き出す

の二つをやれば導入できる。


ただし普通に導入すると、大きめなプロジェクトを編集する時に非常に重くなってしまうので、できる限りミニマムに利用するために以下の設定を入れた。

(require 'ensime)
;; この設定を入れないと毎回ensimeを起動するたびにメッセージが表示される。
(setq ensime-startup-snapshot-notification nil)
(setq ensime-startup-notification nil)
;; デフォルトではカーソル移動のたびに型の探索が走るため、
;; カーソル移動自体が重くなる。編集困難になるので、
;; カーソル位置の型をミニバッファに表示しないようにする
(setq ensime-eldoc-hints nil)
;; 補完機能が走り出した時に、固まるときがあったので一旦オフ
(setq ensime-completion-style nil)
;; シンタックスチェックを自動で走らせないようにする。
;; この設定を入れてもファイル保存時にはシンタックスチェックが走るので問題ない。
(setq ensime-typecheck-when-idle nil)
;; semantic highlightを有効にすると、CPUが100%に張り付いたので
;; 無効にする。
(setq ensime-sem-high-enabled-p nil)

;; シンタックスエラーの時の表示をflycheckと同じものにしておく
(custom-set-faces
 '(ensime-errline-highlight ((t (:inherit flycheck-error))))
 '(ensime-warnline-highlight ((t (:inherit flycheck-warning)))))

;; 便利なやつだけ自分の好きなkeybindを設定
(define-key scala-mode-map (kbd "C-@") 'ensime-edit-definition)
(define-key scala-mode-map (kbd "M-@") 'ensime-pop-find-definition-stack)
(define-key scala-mode-map (kbd "M-t") 'ensime-type-at-point-full-name)

いろんな機能をオフにしたが、これでも型チェックや定義元ジャンプなど、便利な機能が幾つか使えるので良い。

tagsファイルを使って定義ジャンプをできるように

小さいプロジェクトではensimeの定義ジャンプが使えるが、大きいプロジェクトで非常に重くなる時もあった。そこで、ctagsでtagsファイルを作り、tagsファイルを使った定義ジャンプも併用できるようにした。

scalaはctagsに定義がないので、以下のように~/.ctagsファイルに書いておく。valを入れている人も多いが、valを入れると定義する先が非常に多くなってしまうこともあるので、今は入れていない。

--langdef=scala
--langmap=scala:.scala
--regex-scala=/^[ \t]*((abstract|final|sealed|implicit|lazy)[ \t]*)*(private[^ ]*|protected)?[ \t]*class[ \t]+([a-zA-Z0-9_]+)/\4/c,classes/
--regex-scala=/^[ \t]*((abstract|final|sealed|implicit|lazy)[ \t]*)*(private[^ ]*|protected)?[ \t]*object[ \t]+([a-zA-Z0-9_]+)/\4/c,objects/
--regex-scala=/^[ \t]*((abstract|final|sealed|implicit|lazy)[ \t]*)*(private[^ ]*|protected)?[ \t]*((abstract|final|sealed|implicit|lazy)[ \t]*)*case class[ \t]+([a-zA-Z0-9_]+)/\6/c,case classes/
--regex-scala=/^[ \t]*((abstract|final|sealed|implicit|lazy)[ \t]*)*(private[^ ]*|protected)?[ \t]*case object[ \t]+([a-zA-Z0-9_]+)/\4/c,case objects/
--regex-scala=/^[ \t]*((abstract|final|sealed|implicit|lazy)[ \t]*)*(private[^ ]*|protected)?[ \t]*trait[ \t]+([a-zA-Z0-9_]+)/\4/t,traits/
--regex-scala=/^[ \t]*((@inline|@noinline|abstract|final|sealed|implicit|lazy|private[^ ]*(\[[a-z]*\])*|protected)[ \t]*)*def[ \t]+([a-zA-Z0-9_]+)/\4/m,methods/
--regex-scala=/^[ \t]*package[ \t]+([a-zA-Z0-9_.]+)/\1/p,packages/

あとはScalaのプロジェクトルートでctagsを使ってtagsファイルを作る。

$ ctags --verbose -R -e

Emacs側では helm-etags-plus を使う。

(require 'helm-etags-plus)
(setq helm-etags-plus-use-absolute-path nil)
(set-face-foreground 'helm-etags-plus-file-face "green")
(global-set-key (kbd "C-@") 'helm-etags-plus-select)

これでScalaでもtagsファイルを使った定義ジャンプが出来るようになった。基本はensimeの定義ジャンプを使った方が便利だが、なんか重いと感じたらtagsを使ったジャンプに切り替えるようにしている。簡単に切り替えられるように以下のようなユーティリティを定義している。

;;; ensimeのタグジャンプを使うようにする
(defun scala/use-ensime-definition-jump ()
  (interactive)
  (define-key scala-mode-map (kbd "C-@") 'ensime-edit-definition))

;;; ctagsのタグジャンプを使うようにする
;;; グローバルのキーバインドにhelm-etags-plusを設定しているので、
;;; scala-mode-mapの方のキーバインドを消せば良い。
(defun scala/use-ctags-definition-jump ()
  (interactive)
  (define-key scala-mode-map (kbd "C-@") nil))

IntelliJEmacsを行き来できるように

以上でEmacsでとりあえず編集できるようになったが、たまにリファクタリングとかでIntelliJを使いたいということもあるので、EmacsIntelliJを行き来できるようにしておく。

これは以前ブログに書いた設定をしておくと良い。

blog.shibayu36.org

Scalaの関数リテラルの学習メモ

Scalaスケーラブルプログラミングを読んでいて、関数リテラルについて気になったことがあったので、いろいろ試したことをメモしておく。

関数リテラルの基本形

基本形はこんな感じ。

val f = (a: Int, b: Int) => a + b
f(1,2)

省略記法

アンダースコアを使って省略できる。

val f = (_: Int) + (_: Int)
f(1,3)

このままだとアンダースコアに型を付けないといけないけど、変数側に型を明示しておくとアンダースコア側に型を書かなくても済む。これが利用されてfilter関数とかはアンダースコアに型が書かなくても良い。

val f: (Int, Int) => Int = _ + _
f(1,4)

関数の部分適用

関数の部分適応もアンダースコアを使って作れる。

val f2 = f(1, _: Int)
f2(2)

これ、記法覚えにくいなと思ったけど、よく考えると関数の省略形と同じと気づいたので難しくなかった。つまり以下に頭の中で変換すれば難しくない。

val f2 = (a: Int) => f(1, a)
f2(2)

これも変数側に型を書けばアンダースコア側に書かなくても良さそう。

val f2: (Int) => Int = f(1, _)

関数オブジェクト化

defで書かれた関数を関数オブジェクト化するのにもアンダースコアを使う。これはC言語の関数ポインタを取得するのとか、Perlの&で関数リファレンスを取り出すのと同じと考えたら分かりやすかった。

def sum(a: Int, b: Int) = a + b
val f = sum _

推論できないときにだけ明示的にアンダースコアを使うということなので、推論が必要なければアンダースコアも省略できる。

val f: (Int, Int) => Int = sum

推論が必要なければアンダースコアが省略できるので、以下のようにListのforeachにdefで定義された関数を渡す時もアンダースコアを使わなくて良いと理解した。

List(1,2,3).foreach(println)