$shibayu36->blog;

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

Scalaスケーラブルプログラミングを再読した

最近ScalaやPlay Frameworkを触っていて、implicitを多用したコードや関数リテラルを使ったDSL的なコードなどが読めずに困ってきたので、もう一度Scalaスケーラブルプログラミングを読んだ。

前回読んだ時 はまだScalaの基本的な文法周りが面白かったけど、今回は8章 関数とクロージャ、21章 暗黙の型変換とパラメーター、26章 抽出子辺りが面白かった。

  • 8章 関数とクロージャや21章 暗黙の型変換とパラメーターを読むことで、フレームワークから提供されるDSLっぽいコードがどうやって作られているのかを理解することが出来た
  • 26章 抽出子を読むことで、val (a, b) = ("hoge", "fuga")でなぜaとbにそれぞれ代入できるのかや、パターンマッチでList(a, b, _*)でaとbになぜ束縛できるのかとかの中身を知ることが出来た

以前Scalaスケーラブルプログラミングを読んだ - $shibayu36->blog; で書いたとおり読みやすい本ではないのだけど、Scalaに慣れてきたら8章、21章あたりはきちっと読んでおくと良さそうと思った。あとは読めないコードが出てきたときにそれに関連する部分を読むとScalaに対する理解が深まりそう。

読書ノート

  • for (とfor {の違いはセミコロンの省略ができるかどうか 136
  • yieldはコレクションの再生成 138
  • アンダースコアはScalaではまったくわけのわからない値のためのプレースホルダとして使われる 142
  • 関数リテラルの省略形 157
  • 関数呼び出しの特殊な形態 164
    • 連続パラメーター
    • 名前付き引数
    • デフォルト値
  • 関数の部分適用を_でやるのって関数リテラルの省略形っぽい
  • http://www.ne.jp/asahi/hishidama/home/tech/scala/function.html#h_function_object
  • 複数引数の場合、_なしでカリー化できるのか? -> defで定義されたメソッドならできない
  • 引数が一つだけの場合、括弧を中括弧で書いても良いことになっているので、これを使って制御構造っぽく書くことができる 180
    • なので、withPrintWriter(file: File)(op: PrintWriter => Unit)みたいに、引数リストを分けることで、制御構造っぽくかける
  • withPrintWriter(file) { writer => … }
  • 名前渡しパラメータの役割 182
  • 空括弧メソッド 186
  • Scalaは定義のための名前空間を2つしか持たない 191
    • 値(フィールド、メソッド、パッケージ、シングルトンオブジェクト)
    • 型(クラス、トレイト)
  • Nothingはすべてのサブ型で、これがあることでthrowなどの返り値を定義できる 216
  • traitのextendsでミックスイン出来るクラスを制限できる 227
  • ミックスインは右から適用される 229
  • Scalaのimport文は 242
    • 任意の場所にかけて
    • パッケージもオブジェクトも参照できて
    • インポートメンバーの名前を変えたりできる
  • java.lang._、scala._、Predef._は暗黙にimportされている 244
  • 単なるprotected・privateは、自分と同じクラスの別インスタンスにアクセス可能だが、thisを付けると同一クラスであっても別インスタンスにはアクセスできなくなる。 246 http://www.ne.jp/asahi/hishidama/home/tech/scala/class.html#h_access_modifiers
  • クラスとコンパニオンオブジェクトはそれぞれがそれぞれの非公開メンバーにアクセスできる 249
  • package.scalaにパッケージオブジェクトを定義し、そのパッケージ全体からアクセス可能にできる 249
  • パターンマッチのいろんな書き方 268 http://qiita.com/techno-tanoC/items/3dd3ed63d161c53f2d89
  • シールドクラスでは、同じファイルで定義されているサブクラス以外は新しいサブクラスを追加できない。そのため、パターンマッチがすべて行われているか検出できる 279
  • new RationalTrait { クラス定義と同様の書き方 } で、無名クラスを作ってインスタンスを作れる 386
  • lazyキーワードを使うと、その変数が利用される時に初めて計算される 390
  • implicitの規則 410
    • マーキングルール:implicitによって修飾された定義だけが暗黙の型変換に使われる
    • スコープルール:挿入される暗黙の型変換は、単一の識別子としてスコープ内にあるか、変換のソース型やターゲット型として対応付けられていなければならない
    • 1度に1回ルール:暗黙の型変換は1度しか挿入されない
    • 明示的変換優先ルール:書かれたままの状態でコードが型チェックをパスするときには、暗黙の型変換は行われない
  • Map等に使われる->はscala.PredefのArrowAssocクラスのメソッドで、暗黙の型変換が使われている 416
  • 暗黙のパラメータでのimplicitの利用は、個別のパラメーターではなくパラメーターリスト全体に適用される 419
  • 暗黙の型変換がおかしかったら、デバッグのために型変換を明示的に書いてみると良い 427
  • 21章のimplicitの説明は必読。いろんなモジュールでimplicitを使いまくってるので、そのあたりのモジュールを読むのにこの辺の知識は必要そう
  • unapplyメソッドが定義されているオブジェクトは抽出子 533
  • unapplyはある一つのものを分解するメソッド
  • 26章の抽出子を学ぶと、val (a, b) = (“hoge”, “fuga”)でどうして変数束縛出来るのかとか、パターンマッチでなぜオブジェクトの中身を変数に束縛できるのかとかが理解できる

ターミナルの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