Scala + Play Framework を試してみようと、公式ドキュメントの Getting Started の Already know a bit about Play?
に従ってみました。
Getting Started with Play Framework
ただ、試してみていくうちに
- 分かったこと
- 分からないこと
をまとめたくなったため、メモを残します。
もし「分からない」としたことについて、ご存じの方がいれば教えていただけるとありがたいです。
目次
- 環境
- Play Frameworkのディレクトリ構造について
- Controllerについて
- Viewについて
- conf/routes について
- project/build.properties で sbt のバージョンを指定
- .sdkmanrc について
- その他分からないこと
環境
- SDKMAN! で Java と sbt をインストール済
- openjdk 11.0.12 2021-07-20
- sbt 1.5.5
- SDKMAN! 5.12.4
- Scala 2.13.6
- play framework 2.8.8
また、すでにPlay Frameworkのプロジェクト生成は、以下のようにして済んだものとします。
% sbt new playframework/play-scala-seed.g8 [info] welcome to sbt 1.5.5 (Eclipse Foundation Java 11.0.12) [info] loading global plugins from path/to/.sbt/1.0/plugins [info] set current project to new (in build file:/private/var/folders/9j/rhvj56hj4zl6kbkc0zxswj400000gn/T/sbt_d5c88b9e/new/) This template generates a Play Scala project name [play-scala-seed]: hello organization [com.example]: com.example.thinkami Template applied in path/to/hello
Play Frameworkのディレクトリ構造について
.gitignore
にあるディレクトリやファイルを取り除いて残ったものは、以下の通りでした。
% tree -a . ├── .g8 │ └── form │ ├── app │ │ ├── controllers │ │ │ └── $model__Camel$Controller.scala │ │ └── views │ │ └── $model__camel$ │ │ └── form.scala.html │ ├── default.properties │ └── test │ └── controllers │ └── $model__Camel$ControllerSpec.scala ├── .gitignore ├── app │ ├── controllers │ │ └── HomeController.scala │ └── views │ ├── index.scala.html │ └── main.scala.html ├── blog.md ├── build.sbt ├── conf │ ├── application.conf │ ├── logback.xml │ ├── messages │ └── routes ├── logs │ └── application.log ├── project │ ├── build.properties │ ├── plugins.sbt │ └── project ├── public │ ├── images │ │ └── favicon.png │ ├── javascripts │ │ └── main.js │ └── stylesheets │ └── main.css └── test └── controllers └── HomeControllerSpec.scala
それぞれのディレクトリの役割については、公式ドキュメントに記載がありました。
公式の2.8系のドキュメントと日本語である2.3系のドキュメントでは、2.8系では dist
ディレクトリが増えているくらいの違いでした。
ただ、上記ドキュメントについては .g8
ディレクトリの説明がなかったため、調べてみました。
.g8 ディレクトリについて
stackoverflowによると、 scaffolds
用のものが置いてあるディレクトリのようです。
The project supports using giter8 to create scaffolds So technically it is safe to delete, but you will lose the g8Scaffold form feature.
play-scala-seed.g8
のリポジトリはこちら。
playframework/play-scala-seed.g8: Play Scala Seed Template: run "sbt new playframework/play-scala-seed.g8"
ディレクトリ構造がわかったので、次は実行可能なコードが含まれる app
ディレクトリの中を見てみます。
Controllerについて
新規プロジェクトの生成によりできた HelloController
を見てみます。
import
とコメントを消してみると、こんな感じでした。
@Singleton class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController { def index() = Action { implicit request: Request[AnyContent] => Ok(views.html.index()) } }
これを読んで、分かったこと/分からなかったことがあったため、まとめてみます。
分かったこと
@Injectについて
@Inject()
は JSR330
のアノテーションによるDIと理解しました。詳しくは以下が参考になりました。
- PlayFramework 2.6.X のDIについて - FLINTERS Engineer's Blog
- ScalaでDI(Play framework2.7 + Guice編) - Lambdaカクテル
この @Inject
を使い、 trait ControllerComponents
のデータがinjectされます。
trait ControllerComponents
は
The base controller components dependencies that most controllers rely on.
とあり、コントローラーで必要な機能を持っているようです。 https://www.playframework.com/documentation/2.8.x/api/scala/play/api/mvc/ControllerComponents.html
そして、 inject した内容は、以下のような順で渡されていくようです。
ControllerComponents の中身を少し見てみる – tchiba.dev
プライマリーコンストラクタについて
(val controllerComponents: ControllerComponents)
の部分は、プライマリーコンストラクタでした。
プライマリコンストラクタの引数にval/varをつけるとそのフィールドは公開され、外部からアクセスできるようになります。
また、プライマリーコンストラクタで使用している val
は値 (Value) で再代入不可、 var
は変数 (Variable) で再代入可でした。
基本 | Scala Documentation
分からないこと
@Singleton を付与する基準
上記のように、生成された HelloController には @Singleton
が付いています。
ただ、Web上のサンプルコードを見ると、Controllerに @Singleton
が付いていない実装もありました。
- 公式サンプル
- RealWorld (ただし、ここは
@Inject
も使ってない) - BizReachさんのハンズオン
- その他
@Singleton
について調べたところ、stackoverflowには
In general, it is probably best to not use
@Singleton
unless you have a fair understanding of immutability and thread-safety. If you think you have a use case for Singleton though just make sure you are protecting any shared state.In a nutshell, don't use
@Singleton
.playframework - Why use singleton controllers in play 2.5? - Stack Overflow
や、上記を引用した別の質問へのコメントがありました。
In effect HomeController is behaving like a singleton and avoiding singletons could be safer. For example, by default Play Framework uses Guice dependency injection which creates a new controller instance per request as long as it is not a @Singleton. One motivation is there is less state to worry about regarding concurrency protection as suggested by Nio's answer:
multithreading - Play Scala and thread safety - Stack Overflow
とはいえ、これだけでは @Singleton
を使うかどうかの判断基準が分かりませんでした。
Controllerで BaseController と AbstractController のどちらを継承させるかの基準
HomeController
では BaseController
を extends
していました。
class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {}
公式ドキュメントでControllerで継承すべきものを見てみると、以下に記載がありました。
Scala Controller changes | Migration26 - 2.6.x
- BaseController
- AbstractController
- InjectedController
このうち InjectedController
については、説明に
a trait, extending BaseController, that obtains the ControllerComponents through method injection (calling a setControllerComponents method). If you are using a runtime DI framework like Guice, this is done automatically.
とあり、Play FrameworkのデフォルのDI フレームワークである Guice
でのDIではなく、メソッドでのDIをする時に使うものと理解しました。
また、 AbstractController
については、説明に
an abstract class extending BaseController with a ControllerComponents constructor parameter that can be injected using constructor injection.
とありました。
そのため、基本は AbstractController
を使うのかなと思いつつも、「BaseController
を使うケースはどんなものがあるのか」が分からなかったことから、 AbstractController
と BaseController
を使い分ける基準が分かりませんでした。
Indexメソッドのブロック式にある構文
Indexメソッドは以下の実装でした。
def index() = Action { implicit request: Request[AnyContent] =>
Ok(views.html.index())
}
まず、 =
の左辺の def index()
は Index メソッドを定義していると理解しました。
次に、 =
の右辺の Action {}
は、 Action の引数としてブロック式を渡していると理解しました。
Scalaでは {} で複数の式の並びを囲むと、それ全体が式になりますが、便宜上それをブロック式と呼ぶことにします。
Actionの引数にブロック式を渡せることについては
Scalaでは、引数を1個だけ渡すメソッド呼び出しなら、引数を囲む括弧を中括弧に変えても良いことになっている。 ... 引数を1個渡すときに括弧ではなく中括弧を使える機能は、クライアントプログラマーが中括弧の間に関数リテラルを書き込めるようにすることを目的としている。そうすれば、メソッド呼び出しなのに、制御構造を使っているような感じが強まる
と理解しました。
以下の記事を参考にしながらActionの実装を見ても、Actionがブロック式を受け取るように見えたためです。
- PlayframeworkのActionからscala文法を紐解く - Qiita
- ActionFunction の紹介 - Play framework Advent Calendar 2014 7日目
問題はブロック式の中身です。ワンライナーに直すとこんな感じです。
implicit request: Request[AnyContent] => Ok(views.html.index())
「暗黙的なパラメータである request
を受け取り、 Ok
を返す」という処理は分かりましたが、記法に対する自分の理解が合っているのかが分かりませんでした。
まず、 =>
を使っていることから、この部分は関数リテラルと考えました。
次に =>
の左辺 implicit request: Request[AnyContent]
については、暗黙のパラメータと考えました。
ここの暗黙のパラメータの意味については、実践Scala入門:書籍案内|技術評論社 の p97 に
「sumを呼び出す時に、implicit とマークされた Adder[T] の値が存在すれば、それを暗黙のうちに補完してください」とコンパイラに指示するものです
とあることから、この部分は「関数リテラルを呼び出す時に、 implicit とマークされた request の値があれば、暗黙のうちに補完する」と理解しました。
ただ、ここで悩んだのは関数リテラルの書式です。
言語仕様のページを見ると
Expr ::= (Bindings | [‘implicit’] id | ‘_’) ‘=>’ Expr ResultExpr ::= (Bindings | ([‘implicit’] id | ‘_’) ‘:’ CompoundType) ‘=>’ Block Bindings ::= ‘(’ Binding {‘,’ Binding} ‘)’ Binding ::= (id | ‘_’) [‘:’ Type]
とありました。
https://scala-lang.org/files/archive/spec/2.13/06-expressions.html#anonymous-functions
Action の引数は ResultExpr を指しているのかなと考えたものの、果たしてこれが正しい理解なのかは分かりませんでした。
そこでためしに、
def index() = Action { (implicit request: Request[AnyContent]) => Ok(views.html.index()) }
と実装を変えてみたところ、 '=>' expected but ')' found.
というエラーでコンパイルができなくなりました。
一方、implicit
を外してみた
def index() = Action { (request: Request[AnyContent]) => Ok(views.html.index()) }
はコンパイルできました。
これらを見る限り理解は合ってそうでしたが、自身は持てませんでした。
その他参考にしたところは以下の通りです。
- scala - Implicit parameter on function literal puzzle - Stack Overflow
- Scala Implicit parameters by passing a function as argument To feel the adnvatage - Stack Overflow
- Implicit keyword before a parameter in anonymous function in Scala - Stack Overflow
Viewについて
app/views
には
の2つのViewテンプレートがありました。
公式ドキュメントによると
Play comes with Twirl, a powerful Scala-based template engine, whose design was inspired by ASP.NET Razor.
とのことです。
conf/routes について
routes
はルーティングファイルでした。
Scala Routing - 2.8.x
複数のルーティングファイルを用意することもできそうです。
scala - PlayFramework: multiple routes file in project - Stack Overflow
project/build.properties で sbt のバージョンを指定
生成したプロジェクトを起動してみると
[info] welcome to sbt 1.5.2 (Eclipse Foundation Java 11.0.12)
と、ローカルでは sbt 1.5.5 で動作するはずが、1.5.2 で動作していました。
原因は build.properties
に
sbt.version=1.5.2
と書かれていたためです。
Ensure sbt and Scala versions compatibility | sbt | IntelliJ IDEA
そこで、build.properties
を
sbt.version=1.5.5
としたところ、
[info] welcome to sbt 1.5.5 (Eclipse Foundation Java 11.0.12)
と表示されるようになりました。
.sdkmanrc について
ここは Play Framework とは関係ありませんが。。。
pyenvやrbenvのようにディレクトリに移動したら自動的に SDKMAN! でインストールした Java や sbt のバージョンを切り替えたくなりました。
以下の記事によると、 SDKMAN! でインストールした場合も、 .sdkmanrc
ファイルがあれば切り替えができそうです。
sdkman で複数のバージョンの Java をディレクトリーごとに切り替える - mike-neckのブログ
そこで、生成したプロジェクトには .sdkmanrc
ファイルは含まれていないことから、追加してみました。
java=11.0.12-tem sbt=1.5.5
その他分からないこと
関数リテラルと無名関数とラムダ式の違い
=>
まわりを調べていたところ
などの用語を見かけました。
Scalaという文脈では、上記の言葉の定義に違いがあるのかどうかが分かりませんでした。