Play Framework 2.9.0 に送ったPull Request

そろそろPlay Framework 2.9.0が出そうですね。ということで自分の送ったPull Requestをまとめてみました。

大きいところだとTwirlのScala 3対応ですね。いい感じにエラーメッセージ出すのにdottyとかsbtまで調べることになって沼でしたがたぶんなんとかなりました。

playframework/playframework

playframework/twirl

sbt/zinc

おまけ

今年もかぼちゃ育てたので見て

ScalaMatsuri 2022でCats Effect 3の話をしました

speakerdeck.com

純粋関数型スタイルのプログラミングは私はあまりやって来なかったんですが、たまには違ったことをやってみようかなと思って発表してみました。年末年始にTypelevelの技術スタックをいろいろ見ててhttp4sとかさわってみてたりしたんですが、その過程でCats Effect 3が気になった感じです。

やはり純粋関数型スタイル自体にはそんなに興味ないんですが、Cats Effectのランタイムは面白くて、それに乗っかるためのフレームワークとして純粋関数型スタイルが要求されるというのはそんなに悪くないなと思いました。あと資料作成や準備の過程でCats Effectの作者の方(Danielさん)のトークを何本かYouTubeで見たんてすが面白すぎる。全部面白い。

Scalaは人気のピーク(ピークと言うほどでもない)を越えてしまって少しコミュニティの熱量は下がってるのかなって気がしますね。とは言え個人的には他に乗り換えたい言語もなく、今やるならRustなんだろうけどRustでWebアプリとか作る気にはならないしなあ、みたいな微妙な気持ちです。まあしばらくはplayframeworkのほうをやっていきます。あとPHPとかですかね?(冗談じゃなくて今本業PHPなんです)

Play Frameworkの開発チームに入れてもらいました

Play FrameworkがLightbendの手から離れる という発表が昨年の10月にあったんですが、それをきっかけにやりとりして Play FrameworkのGitHub Team に入れてもらいました。 しばらく待ちの状態だったんですが、先週くらいから動き初めたところです。とりあえず当面はTravis CIからGitHub Actionsへの移行やScala3対応になるのかなと思います。

PlayはLightbendから離れる決定をしたあとからOpenCollectiveで寄付を募っています。 寄付してくれてる方々を見ると日本の方も多いですね。ありがとうございます。 mkurzさんからも感謝のメッセージをいただいてます。引き続きご支援お願いします 🙏

// mkurzさんは私が何か働きかけたように思ってそうだが何もしていない
Also, a high proportion of the current Play backers on Open Collective are Japanese,
so it seems you reached out to the Japanese community already? If so, 
and if you have a mailing list or similar set up, 
would you mind to say thank you to them from me and the other contributors ;)

Emacs + Metals + lsp-mode でScalaを書く

最近重たい処理走らせてる時とかにIntelliJが遅くてつらかったのでEmacsでもある程度コード読んだり書けたり出来るようにしました。 最近はLSPが人気あるようでEmacsでもlsp-modeというのが結構使われているみたい。Scalaの場合はLSPとしてMetalsを使うことになります。

設定

  • lsp-modeが入っていればとりあえずOK
  • flycheckを入れてコンパイルエラーの表示が出来るようにしましょう
  • lsp-uiを入れてなんかUIからポチポチ出来るようにしましょう
  • companyをコード補完機能に使いましょう
(use-package flycheck
  :init (global-flycheck-mode))

(use-package lsp-mode
  :hook
  (lsp-mode . lsp-lens-mode)
  (scala-mode . lsp)
  :config
  (setq lsp-prefer-flymake nil))

(use-package lsp-ui)

(use-package company
  :hook
  (scala-mode . company-mode)
  :config
  (setq lsp-completion-provider :capf)
  (setq company-minimum-prefix-length 1))

そんなにガリガリ設定書かなくても割といい感じになるのがすごい。

使い方

  • インストールはMetalsのドキュメントでどうぞ。
  • M-., M-, で定義元にジャンプしたり戻ったりできる。
  • キーバインドの一覧は https://emacs-lsp.github.io/lsp-mode/page/keybindings/
  • 一覧に乗ってないけど s-l g e でエラー一覧表示ができた。ドキュメント更新のpull req送った方良いのかな?

f:id:tototoshi:20210406224447j:plain
コード補完

f:id:tototoshi:20210406224357p:plain
エラーの一覧表示

f:id:tototoshi:20210406224517p:plain
コンパイルエラーの表示と選択できるアクション一覧

トラブルシューティング

デバッグはとりあえず *lsp-log* バッファを見てみると何か分かる。Metalsの場合は .metals/metals.log でも良し。

必殺再起動は M-x lsp-restart-workspace

ハマったとこ

補完がうまく行かないなあ、Any型と判定されるなあとか思ってたんだけど、monorepo的なリポジトリでworkspaceのルートが誤判定されてちゃんとプロジェクトが読み込まれていないのが原因でした。M-x lsp-workspace-folder-add で設定したら補完ができるようになりました。

あと大きなプロジェクトで遅くなるけどファイルをウォッチするか?遅くなるけど?と聞かれた時に読み込まないようにしたら補完が効きませんでした。ウォッチしないとダメなのかな。とりあえずウォッチすることにして、遅くなってから考えることにしました。

制限

今のところ機能的にここが限界だろうなあというのが2つ。

  • 高度なリファクタリング機能
    • さすがにIntelliJには勝てない。なんかすごいことやろうと思ったらIntelliJを起動しよう。
  • Javaのコードを追えない
    • Metals = scalameta + LSP なのでそりゃそうかという感じ。

感想

コード読んだり、調査だったり、ちょっとした修正には十分な機能なので嬉しい。まあガッツリコード書くときはIntelliJだけどそれ以外を軽い環境でカバー出来るのは良いですね。

個人用のscala-stewardを動かす

scala-stewardを使い始めました。

ちょっと使うだけならscala-stewardのrepos.mdにプルリクエスト送るだけで良くて便利なんですが、個人の雑多なプロジェクト全てにそれをやるのはちょっと気が引けるので自前でも動かせるようにしました。

今だとGitHub Actionsを使うのが手取り早いんだけど、使うトークンの権限の問題でちょっとややこしかったです。そのあたりは吉田さんのブログが詳しいです。

xuwei-k.hatenablog.com xuwei-k.hatenablog.com

自分は次の形にしました。

github.com

で、試行錯誤の上なんとかscalaおじさんがプルリクくれるようになりました。

f:id:tototoshi:20210213145753p:plain Update sbt to 1.4.7 by scala-ojisan · Pull Request #3 · tototoshi/hello.g8 · GitHub

sbtn 便利かも

sbt 1.4.0 から sbtn という機能が追加されました。

https://eed3si9n.com/sbt-1.4.0-beta

Native thin client sbt 1.4.0 adds an official native thin client called sbtn that supports all tasks. > https://github.com/sbt/sbtn-dist/releases/tag/v1.4.0-RC2

This lets you run sbt tasks from the system shell as:

$ sbtn compile

$ sbtn shutdown

The native thin client will run sbt (server) as a daemon, which avoids the JVM > spinup and loading time for the second call onwards. This could an option if you > would like to use sbt from the system shell such as Zsh and Fish.

Remember to call sbtn shutdown when you're done!

Later on, sbt script will also support --client option to run the native thin client:

$ sbt --client compile

$ sbt --client shutdown

#5620 by @eatkins

sbtn 一度起動すると sbt をデーモンとして常駐させ、以降は sbt を起動するのではなくすでに起動している sbt に接続しに行くため起動時間が短縮されます。

emacs 使ってる人には emacsclient ですって言えば通じそう。あと sbt と同じ Scala のビルドツールである mill も似たようなことやってますよね。mill 最近使い始めたばかりでよく知らないけど。

sbt は起動が遅いので対話シェルを起動させておいてそれを利用するのが普通でしたが、sbtn によってシェルを起動しっぱなしにする必要がなくなります。私は気づくと tmux のウィンドウが無限に開いていて、sbt 起動してるのはどこのウィンドウだっけ??と探して回るのを毎日やっているのですが、sbtn を使うと今いる window でコマンド打てばいいのでそれがなくなりそうです。

あと他のシェルスクリプトとの連携がしやすくなるのも良いですね。今までは

#!/bin/sh
sbt compile
set_up_database
sbt test
tear_down_database

みたいなスクリプトを書くのは sbt の起動が遅いので辛く、シェルスクリプト側の処理を sbt の方に組み込むことになり、sbt 職人が build.sbt に

lazy val setUpDatabase = taskKey[Unit]("setup_database...")
lazy val tearDownDatabase = taskKey[Unit]("teardown_database...")
...

とか書いてドヤッって感じだったんですが、sbtn を使えば

#!/bin/sh
sbtn compile
set_up_database
sbtn test
tear_down_database

でもう十分ですね。build.sbtのダイエットができそうです。

追記

  • project/build.properties のバージョンを上げるのに加えてsbtコマンドのバージョンも 1.4 に上げましょう。
  • sbtnでPlayのプロジェクトをrunするとプロジェクトは起動するけれどsbtn側で標準入力を受け取れないっぽい。(Playプロジェクトのログを受け取る前にデタッチされてそう?)

Modules should be fast and side-effect free と Guice は言っているがPlay Frameworkはそうなってはいない話

github.com

Modules should be fast and side-effect free

Modules should be fast and side-effect free(モジュールは速くて副作用なしであるべき)と GuiceWiki にはあります。

これは Guice モジュールの中で DB に接続したりスレッド開始したりするなという話で、これに従うのならば Guice の Module (extends AbstractModule したクラスの中とか)の他にも Module を構成する各クラスのコンストラクタの中でも副作用を起こしてはいけないことになります。

副作用を起こしてはいけない理由としては

  • Modules start up, but they don't shut down.
  • Modules should be tested.
  • Modules can be overridden.

Guice は挙げています。

Guice の意図に沿ったコードを書くと初期化処理、終了処理のためのメソッドをコンストラクタ以外に用意して、アプリケーションの起動、終了時になんらかの形で呼んであげることになります。

public interface Service {
  void start() throws Exception;
  void stop();
}
 public static void main(String[] args) throws Exception {
    Injector injector = Guice.createInjector(
        new WebserverModule(),
        ...
    );

    Service webserver = injector.getInstance(
        Key.get(Service.class, WebserverService.class));
    webserver.start();
    addShutdownHook(webserver);
  }

サンプルでは単純ですが徹底するとなると例えば設定ファイルを読み込むとかも禁止となるのでなかなか厳しい制限にも感じます。(実際には設定ファイル読み込みとかは OK にして、スレッド開始とかは NG にする、くらいが落とし所かも?)

Play Framework の場合

Play Framework は Guice を使っていますが、Guice のこの主張を完全に無視して副作用をバリバリにつかっています。Play を構成するクラスは大体コンストラクタでスレッド開始したり設定読み込んだりとやって、終了処理はコンストラクタ内で ApplicationLifecycle に登録します。

@Singleton
class NanrakanoService @Inject()(lifecycle: ApplicationLifecycle) {
  val resource = new NanrakanoBackgroundResource().start()
  lifecycle.addStopHook(() => Future.successful(resource.shutdown()))
}

Guicewiki を見たあとでこれを見るとギョッとするんですが実際のところそんなに困ることはないです。

とはいえやっぱり時々は困るし、Guice の推奨しない方法で Guice をヘビーに使っているというのは精神衛生上あまり良くはありません。そして実はコンストラクタで副作用を起こすようになったのは Play が Guice を使い始めた Play 2.4 からの話で、Guice を使っていなかった Play 2.3 以前の Plugin の仕組みでは副作用のためのメソッドが分かれていたのです。

package plugins

import play.api.{Plugin, Application}

class MyPlugin(app: Application) extends Plugin {
  val myComponent = new MyComponent()

  override def onStart() = {
    myComponent.start()
  }

  override def onStop() = {
    myComponent.stop()
  }

  override def enabled = true
}

Guice を使う前は副作用が分かれていたのに、互換性を壊してまで Guice を使い始めたら副作用起こすようになったというのがなんかチグハグだなあって思います。