Scala + Play Framework なアプリを作る前段として、 sbt new した後のコードを読んで分かったこと/分からなかったことをまとめてみた

Scala + Play Framework を試してみようと、公式ドキュメントの Getting Started の Already know a bit about Play? に従ってみました。
Getting Started with Play Framework

ただ、試してみていくうちに

  • 分かったこと
  • 分からないこと

をまとめたくなったため、メモを残します。

もし「分からない」としたことについて、ご存じの方がいれば教えていただけるとありがたいです。

 
目次

 

環境

 
また、すでに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.

scala - Is .g8 directory necessary? - Stack Overflow

 
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と理解しました。詳しくは以下が参考になりました。

 
この @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をつけるとそのフィールドは公開され、外部からアクセスできるようになります。

クラス · Scala研修テキスト

 
また、プライマリーコンストラクタで使用している val は値 (Value) で再代入不可、 var は変数 (Variable) で再代入可でした。
基本 | Scala Documentation

 

分からないこと

@Singleton を付与する基準

上記のように、生成された HelloController には @Singleton が付いています。

ただ、Web上のサンプルコードを見ると、Controllerに @Singleton が付いていない実装もありました。

 
@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 では BaseControllerextends していました。

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 を使うケースはどんなものがあるのか」が分からなかったことから、 AbstractControllerBaseController を使い分ける基準が分かりませんでした。

 

Indexメソッドのブロック式にある構文

Indexメソッドは以下の実装でした。

def index() = Action { implicit request: Request[AnyContent] =>
  Ok(views.html.index())
}

 
まず、 = の左辺の def index() は Index メソッドを定義していると理解しました。

 
次に、 = の右辺の Action {} は、 Action の引数としてブロック式を渡していると理解しました。

Scalaでは {} で複数の式の並びを囲むと、それ全体が式になりますが、便宜上それをブロック式と呼ぶことにします。

ブロック式 | Scalaの制御構文 · Scala研修テキスト

Actionの引数にブロック式を渡せることについては

Scalaでは、引数を1個だけ渡すメソッド呼び出しなら、引数を囲む括弧を中括弧に変えても良いことになっている。 ... 引数を1個渡すときに括弧ではなく中括弧を使える機能は、クライアントプログラマーが中括弧の間に関数リテラルを書き込めるようにすることを目的としている。そうすれば、メソッド呼び出しなのに、制御構造を使っているような感じが強まる

Scalaスケーラブルプログラミング 第4版 - インプレスブックス p180

と理解しました。

以下の記事を参考にしながらActionの実装を見ても、Actionがブロック式を受け取るように見えたためです。

 
問題はブロック式の中身です。ワンライナーに直すとこんな感じです。

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()) }

コンパイルできました。

これらを見る限り理解は合ってそうでしたが、自身は持てませんでした。

その他参考にしたところは以下の通りです。

 

Viewについて

app/views には

の2つのViewテンプレートがありました。

公式ドキュメントによると

Play comes with Twirl, a powerful Scala-based template engine, whose design was inspired by ASP.NET Razor.

The template engine | Scala Templates - 2.8.x

とのことです。

 

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という文脈では、上記の言葉の定義に違いがあるのかどうかが分かりませんでした。