例外をcase classとして定義してはいけない
Scalaで独自の例外を定義する場合
class MyException(message: String) extends RuntimeException(message)
と
case class MyException(message: String) extends RuntimeException(message)
のどちらが良いでしょうか。タイトルで言っちゃってますが、まずほとんどの場合はcase classではなくclassを使う方が良いと思います。
case classにすると等価性がそのインスタンスの属性によって判断されるので、同じ属性を持っている例外は同じものと見なされます。
scala> case class MyException(message: String) extends RuntimeException(message) defined class MyException scala> MyException("error") res0: MyException = MyException: error scala> MyException("error") res1: MyException = MyException: error scala> res0 == res1 res2: Boolean = true
一方、classの場合はequalsをオーバーライドしていない限り、属性が同じでもインスタンスが別なら別物です。
scala> class MyException(message: String) extends RuntimeException(message) defined class MyException scala> new MyException("error") res0: MyException = MyException: error scala> new MyException("error") res1: MyException = MyException: error scala> res0 == res1 res2: Boolean = false
基本的にはcase classではなくclassを使った時の挙動が好ましいと思います。messageが同じとはいえ、起きたタイミングの異なる2つの例外を同じものと見なすのは不自然ではないでしょうか。スタックトレースだって異なるかもしれないですしね。
AirframeでPlayを動かそうとした
taroleoさんが開発しているAirframeが最近Twitterのタイムラインで話題になっていることが多いので、そろそろ触ってみようかと思い、とりあえずPlay frameworkのDIコンテナをGuiceからAirframeに置き換えるというのをやって見ました。結果は失敗に終わったんですが、GuiceとAirframeの違いを知ることはできました。
PlayではデフォルトではRuntime DIにはGuiceが利用されますが、Play2.6ではDIコンテナのためのインターフェイスが作られたのでGuice以外のDIコンテナも利用できます。ApplicationLoaderというクラスがPlayのApplicationオブジェクトをロードする役割を担っているのでこれを好きなDIコンテナで実装します。
class AirframeApplicationLoader extends ApplicationLoader { override def load(context: ApplicationLoader.Context): Application = { import wvlet.airframe._ // ここでAirframeでApplicationオブジェクトを構築する } }
実際にはPlayとの橋渡し的なコードをそれなりに書く必要があり、そんなに簡単ではないです。例えばPlayが定義しているInjectorインターフェイスを実装する必要があったり、Playのビルトインモジュールを利用したいDIコンテナで使えるように変換するようなコードを書いたりです。実用的なことを考えるとPlayが提供しているGuiceApplicationBuilderみたいなコードも書く必要があり、なかなか骨が折れます。
Airframeの場合、実際にPlayの各モジュールをロードする段階でもつまづきます。Playの各モジュールは @Inject
アノテーションでDIコンテナに対してクラスの生成方法のヒントを与えていますが、Airframeは @Inject
アノテーションには対応していない方針のようでそれが機能しません。自分でクラスの生成方法を記述する必要があるのですが、Playの各モジュールの構築を手動でやるのはかなり面倒、ということで挫折しました。
その他Guiceとの違いで重要そうなのはsessionという概念でしょうか。Airframeではsessionが有効な間だけ機能を利用でき、また@PostConstruct
, @PreDestroy
アノテーションを利用するとsessionの開始時、終了時をフックして処理を挟みこめます。例えばスレッドの開始や終了などをAirframeに任せると便利ですね。Guiceにはこの機能はないので、Playでは @PostConstruct
や @PreDestroy
の代わりに各クラスのコンストラクタとApplicationLifecycleというクラスでライフサイクルフックが実装されています。
同じDIコンテナという種類のライブラリでも比較して見るとそれなりに差異があります。それにDIコンテナはアプリケーションの基盤部分を担うようなものなので、気軽に置き換えられるようなものではないですね。でも気が向いたら再チャレンジしてみようと思います(多分やらない)。
Homebrewを使うのをやめた
プログラムのお仕事してるといろんな言語やツールをインストールするのが大変ですが、パッケージ管理ツールとかを使うと今度はそのパッケージ管理ツール由来のトラブルに巻き込まれたりするんですよね。それが面倒でHomebrewとかなんとかenvとか使わなくなってだいぶ経ちました。
今はほとんどのツールを手動でインストールしています。だいたいは ./configure --prefix=$PREFIX && make && make install。scalaとかはzip落としてきて展開してます。
ディレクトリ構成とかどうでも良いと思うんですが、私は$HOME/opt/srcにtarとかzipとか展開したソースを置いてprefixを$HOME/opt/pkg/... にし、$HOME/opt/bin以下にシンボリックリンク作ってPATH通してます。複数のバージョンを併用するのも単純にフルパスでコマンド叩けば良いだけですごく単純です。
/Users/toshi/opt ├── bin │ ├── php -> /Users/toshi/opt/pkg/php-7.2.5/bin/php │ ├── python3 -> /Users/toshi/opt/pkg/python-3.6.5/bin/python3 │ ├── ruby -> /Users/toshi/opt/pkg/ruby-2.5.1/bin/ruby │ ├── scala -> /Users/toshi/opt/pkg/scala-2.12.6/bin/scala ├── pkg │ ├── php-7.2.5 │ ├── python-3.6.5 │ ├── ruby-2.5.1 │ ├── scala-2.12.6 └── src ├── Python-3.6.5 ├── php-7.2.5 ├── ruby-2.5.1
依存ライブラリ集めたりビルド環境整えたり確かに手間はかかるんですが、その代わり各ツールへの理解は深まります。ビルドのエラーが出ることはたまにありますが結構単純なやつが多いのでそんなに困るほどではないです。少なくともHomebrewとかなんとかenvをデバッグする時のような不毛さはないし、自分の環境もよく把握できるのでトラブル自体が減るように思います。
もうとにかく依存ライブラリとか多くてビルドが死ぬほどめんどくさい!ってのはdockerとか使ってごまかしたりします。あとはプログラミング言語みたいに複数バージョン使いわける必要性があるやつでなければ公式のインストーラを使うこともあります。その辺明確な基準は持っていないです。まあ単にHomebrewが気に食わなかっただけですね。
play-jsonでReads/Writes/Formatを定義しなくてもよくする
play-jsonはReads/Writes/Formatを定義するのが面倒とよく言われるので、shapelessの練習も兼ねて作ってみました。
https://github.com/tototoshi/play-json-generic
scala> import play.api.libs.json._ import play.api.libs.json._ scala> case class Person(firstName: String, lastName: String, friends: List[Person]) defined class Person scala> val people = Person("John", | "Lennon", | List(Person("Paul", "McCartney", Nil), | Person("George", "Harrison", Nil), | Person("Ringo", "Starr", Nil))) people: Person = Person(John,Lennon,List(Person(Paul,McCartney,List()), Person(George,Harrison,List()), Person(Ringo,Starr,List()))) scala> Json.prettyPrint(Json.toJson(people)) <console>:18: error: No Json serializer found for type Person. Try to implement an implicit Writes or Format for this type. Json.prettyPrint(Json.toJson(people)) ^
Reads/Writes/Formatを定義していないcase classをシリアライズしようとすると当然こんなエラーが出ますが、
import com.github.tototoshi.play.json.generic._
することでシリアライズ/デシリアライズできるようになります。
scala> import com.github.tototoshi.play.json.generic._ import com.github.tototoshi.play.json.generic._ scala> Json.prettyPrint(Json.toJson(people)) res1: String = { "firstName" : "John", "lastName" : "Lennon", "friends" : [ { "firstName" : "Paul", "lastName" : "McCartney", "friends" : [ ] }, { "firstName" : "George", "lastName" : "Harrison", "friends" : [ ] }, { "firstName" : "Ringo", "lastName" : "Starr", "friends" : [ ] } ] } scala> Json.parse(res1).as[Person] res2: Person = Person(John,Lennon,List(Person(Paul,McCartney,List()), Person(George,Harrison,List()), Person(Ringo,Starr,List())))
play-jsonのJson.formatとかはマクロで実装されているので、そのマクロのためのオプションとかには一部対応できていないです。JsonNamingには対応できました。
Shapeless使ってみたいという人には The Type Astronaut's Guide to Shapeless - Underscore がすごくわかりやすくてオススメです。
追記
指摘されて気づいたんですが、HListを使っているので23個以上のフィールドを持つcase classにまつわる問題が解決しちゃってました。
https://github.com/tototoshi/play-json-generic/commit/0382c149efb4ed9fd4b4bcea99841641f2ca7949
Akka Streamをいい感じに停止させる
Akka Streamを使ったサーバーを書いていて、サーバーの終了処理を行う時にstreamを停止させる必要が出てきました。どうしたら良いでしょうか。
いきなり ActorSystem#terminate
とりあえずAkka Stream自体の終了のさせ方。これは無理やり ActorSystem#terminate
を呼んでも止まることは止まります。
implicit val system = ActorSystem() implicit val materializer = ActorMaterializer() val src: Source[Int, NotUsed] = Source.fromIterator[Int](() => Iterator.continually(1)) val sink: Sink[Int, Future[Int]] = Sink.fold(0)(_ + _) val result = src.runWith(sink) Thread.sleep(1000) system.terminate() logger.info("result:" + Await.result(result, Duration.Inf)))
ですが、以下のようなエラーが出てしまいます。ダメそうです。
Exception in thread "main" akka.stream.AbruptStageTerminationException: GraphStage [akka.stream.impl.HeadOptionStage$$anon$3@783971c3] terminated abruptly, caused by for example materializer or actor system termination.
KillSwitch
というわけで適当にググると https://stackoverflow.com/a/38326082 というStackOverflowの回答がすぐに出てきます。どうやらKillSwitchというのを使えば良いようです。(Konrad さんの回答なので安心)ちなみにちゃんとAkkaのドキュメントにも載っています。
こんな感じ
implicit val system = ActorSystem() implicit val materializer = ActorMaterializer() val src: Source[Int, NotUsed] = Source.fromIterator[Int](() => Iterator.continually(1)) val sink: Sink[Int, Future[Int]] = Sink.fold(0)(_ + _) val (killSwitch, result) = src .viaMat(KillSwitches.single)(Keep.right) .toMat(sink)(Keep.both) .run() Thread.sleep(1000) killSwitch.shutdown() system.terminate() logger.info("result: " + Await.result(result, Duration.Inf))
KillSwitchでまずストリームを終了させ、それからActorSystemを終了させればエラーは起きません。
JVMのshutdown hookを使う
さて、サーバーの終了時にAkka Streamを停止させたいので Runtime.getRuntime.addShutdownHook
を使うことにしました。
implicit val system = ActorSystem() implicit val materializer = ActorMaterializer() val src: Source[Int, NotUsed] = Source.fromIterator[Int](() => Iterator.continually(1)) val sink: Sink[Int, Future[Int]] = Sink.fold(0)(_ + _) val (killSwitch, result) = src .viaMat(KillSwitches.single)(Keep.right) .toMat(sink)(Keep.both) .run() Runtime.getRuntime.addShutdownHook(new Thread(() => { killSwitch.shutdown() logger.info("result: " + Await.result(result, Duration.Inf)) system.terminate() }))
一見良さそうですが、これはエラーになることがあります。
Exception in thread "Thread-1" akka.stream.AbruptStageTerminationException: GraphStage [akka.stream.impl.HeadOptionStage$$anon$3@773caeb2] terminated abruptly, caused by for example materializer or actor system termination.
なぜ?
CoordinatedShutdown
不思議に思ってActorSystemのコードを読んでいたら、ActorSystemのインスタンスが作られる時には、JVMのshutdown hookにActorSystemの終了処理が追加されていることを知りました。これが原因でActorSystemに依存するような処理、例えばAkka StreamをJVMのshutdown hookで行おうとしても、タイミングによっては先にActorSystemが終了しうまくいかないようです。
ActorSystemのshutdown hookはCoordinatedShutdownというクラスで登録されています。そしてActorSystemに依存する処理のShutdownもCoordinatedShutdownを使うことで安全に行えます。次のコードがその例で、ActorSystemが終了する前(CoordinatedShtudown.PhaseBeforeActorSystemTerminateという値で指定している)にAkka Streamの終了処理を行っています。
implicit val system = ActorSystem() implicit val materializer = ActorMaterializer() val src: Source[Int, NotUsed] = Source.fromIterator[Int](() => Iterator.continually(1)) val sink: Sink[Int, Future[Int]] = Sink.fold(0)(_ + _) val (killSwitch, result) = src .viaMat(KillSwitches.single)(Keep.right) .toMat(sink)(Keep.both) .run() CoordinatedShutdown(system).addTask( CoordinatedShutdown.PhaseBeforeActorSystemTerminate, "app-shutdown") { () => killSwitch.shutdown() logger.info("result: " + Await.result(result, Duration.Inf)) // ActorSystemはCoordinatedShutdownを使っていれば勝手に終了する // system.terminate() Future.successful(akka.Done) }
ちなみにCoordinatedShutdownによる終了処理はJVMのshutdown hookに任せずに自分で呼ぶことも可能です。
CoordinatedShutdown(actorSystem) .run(CoordinatedShutdown.JvmExitReason)
CoordinatedShutdownは主にakka-clusterのための機能のようで、不要であれば使わないように設定することもできます。
slick2とslick3を同居させる
slick2からslick3への移行はAPIが根本的に変わったためにめちゃくちゃ大変です。blocking-slick を使った方法なども知られていますが、ちょっと使って良いものかは悩みます。
blocking-slickを使っても使わなくてもslick2からslick3への書き換えを一度にやるのは大きなプロジェクトだとかなりのパワーが必要です。slick2とslick3を1つのプロジェクト内で同居させ、徐々にslick3に移行させていくことはできないでしょうか。
通常、同一ライブラリの2つのバージョンがプロジェクト内に同居するということはありえません。ところがslick2とslick3の場合、package名が
- slick2: scala.slick.*
- slick3: slick.*
と変わっているのでslick2とslick3の両方を依存関係に加えることができればできるはずです。
slick2とslick3の両方を依存関係に加える
単純に考えると次のようなことがしたいのですが、これはできません。
libraryDependencies ++= Seq( "com.typesafe.slick" %% "slick" % "2.1.0", "com.typesafe.slick" %% "slick" % "3.2.1" )
同一ライブラリの別バージョンがあった場合、sbtはバージョンが新しいものを優先して依存関係に追加します。そのためslick2は読み込んでくれません。sbtのバージョンによっては次のようなメッセージが出ているでしょう。
[warn] There may be incompatibilities among your library dependencies. [warn] Here are some of the libraries that were evicted: [warn] * com.typesafe.slick:slick_2.11:2.1.0 -> 3.2.1
sbtに依存関係を管理させるとslick2とslick3を同居させることはできなそうです。幸い、sbtはlibというディレクトリにjarを追加することでそのjarを読み込んでくれますから、slick3のjarを自分で用意してlibに入れます。
バイナリ互換性について
slick2はtypesafe/configの1.2に依存しているけどslick3はtypesafe/configの1.3に依存している。でもtypesafe/configの1.2と1.3はバイナリ互換なようでセーフ。
コード例
というわけで次のコードはslick2とslick3を同居させた例です。
package com.example import scala.concurrent.ExecutionContext.Implicits.global object Slick3 { import _root_.slick.jdbc.H2Profile.api._ val db = Database.forConfig("h2mem1") class Suppliers(tag: Tag) extends Table[(Int, String, String, String, String, String)](tag, "SUPPLIERS") { def id = column[Int]("SUP_ID", O.PrimaryKey) // This is the primary key column def name = column[String]("SUP_NAME") def street = column[String]("STREET") def city = column[String]("CITY") def state = column[String]("STATE") def zip = column[String]("ZIP") // Every table needs a * projection with the same type as the table's type parameter def * = (id, name, street, city, state, zip) } val suppliers = TableQuery[Suppliers] // Definition of the COFFEES table class Coffees(tag: Tag) extends Table[(String, Int, Double, Int, Int)](tag, "COFFEES") { def name = column[String]("COF_NAME", O.PrimaryKey) def supID = column[Int]("SUP_ID") def price = column[Double]("PRICE") def sales = column[Int]("SALES") def total = column[Int]("TOTAL") def * = (name, supID, price, sales, total) // A reified foreign key relation that can be navigated to create a join def supplier = foreignKey("SUP_FK", supID, suppliers)(_.id) } val coffees = TableQuery[Coffees] def setup() = { val action = DBIO.seq( // Create the tables, including primary and foreign keys (suppliers.schema ++ coffees.schema).create, // Insert some suppliers suppliers += (101, "Acme, Inc.", "99 Market Street", "Groundsville", "CA", "95199"), suppliers += ( 49, "Superior Coffee", "1 Party Place", "Mendocino", "CA", "95460"), suppliers += (150, "The High Ground", "100 Coffee Lane", "Meadows", "CA", "93966"), // Equivalent SQL code: // insert into SUPPLIERS(SUP_ID, SUP_NAME, STREET, CITY, STATE, ZIP) values (?,?,?,?,?,?) // Insert some coffees (using JDBC's batch insert feature, if supported by the DB) coffees ++= Seq( ("Colombian", 101, 7.99, 0, 0), ("French_Roast", 49, 8.99, 0, 0), ("Espresso", 150, 9.99, 0, 0), ("Colombian_Decaf", 101, 8.99, 0, 0), ("French_Roast_Decaf", 49, 9.99, 0, 0) ) // Equivalent SQL code: // insert into COFFEES(COF_NAME, SUP_ID, PRICE, SALES, TOTAL) values (?,?,?,?,?) ) db.run(action) } def run(): Unit = { println("Coffees:") for { _ <- setup() result <- db.run(coffees.result) } { result.foreach { case (name, supID, price, sales, total) => println(" " + name + "\t" + supID + "\t" + price + "\t" + sales + "\t" + total) } } } } object Slick2 { import scala.slick.driver.H2Driver.simple._ val db = Database.forConfig("h2mem2") class Suppliers(tag: Tag) extends Table[(Int, String, String, String, String, String)](tag, "SUPPLIERS") { def id = column[Int]("SUP_ID", O.PrimaryKey) // This is the primary key column def name = column[String]("SUP_NAME") def street = column[String]("STREET") def city = column[String]("CITY") def state = column[String]("STATE") def zip = column[String]("ZIP") // Every table needs a * projection with the same type as the table's type parameter def * = (id, name, street, city, state, zip) } val suppliers = TableQuery[Suppliers] // Definition of the COFFEES table class Coffees(tag: Tag) extends Table[(String, Int, Double, Int, Int)](tag, "COFFEES") { def name = column[String]("COF_NAME", O.PrimaryKey) def supID = column[Int]("SUP_ID") def price = column[Double]("PRICE") def sales = column[Int]("SALES") def total = column[Int]("TOTAL") def * = (name, supID, price, sales, total) // A reified foreign key relation that can be navigated to create a join def supplier = foreignKey("SUP_FK", supID, suppliers)(_.id) } val coffees = TableQuery[Coffees] def setup() = { db.withSession { implicit session => // Create the tables, including primary and foreign keys suppliers.ddl.create coffees.ddl.create // Insert some suppliers suppliers += (101, "Acme, Inc.", "99 Market Street", "Groundsville", "CA", "95199") suppliers += ( 49, "Superior Coffee", "1 Party Place", "Mendocino", "CA", "95460") suppliers += (150, "The High Ground", "100 Coffee Lane", "Meadows", "CA", "93966") // Equivalent SQL code: // insert into SUPPLIERS(SUP_ID, SUP_NAME, STREET, CITY, STATE, ZIP) values (?,?,?,?,?,?) // Insert some coffees (using JDBC's batch insert feature, if supported by the DB) coffees ++= Seq( ("Colombian", 101, 7.99, 0, 0), ("French_Roast", 49, 8.99, 0, 0), ("Espresso", 150, 9.99, 0, 0), ("Colombian_Decaf", 101, 8.99, 0, 0), ("French_Roast_Decaf", 49, 9.99, 0, 0) ) // Equivalent SQL code: // insert into COFFEES(COF_NAME, SUP_ID, PRICE, SALES, TOTAL) values (?,?,?,?,?) } } def run(): Unit = { println("Coffees:") db.withSession { implicit session => coffees.list.foreach { case (name, supID, price, sales, total) => println(" " + name + "\t" + supID + "\t" + price + "\t" + sales + "\t" + total) } } } } object Main { def main(args: Array[String]): Unit = { println("------------- Slick3 --------------") Slick3.setup() Slick3.run() println("------------- Slick2 --------------") Slick2.setup() Slick2.run() } }
slick3のクラスをimportする時は _root_
を頭につけた方が良いと思います。scalaは
import scala.io.Source
が
import io.Source
と書けるように、scalaパッケージがデフォルトでimportされているので、_root_
をつけないとslick3を読み込みたいのかslick2を読み込みたいのか不明瞭になってしまうためです。
というわけでslick2とslick3を同居させることができたんですが、これはこれで新たな地獄を生み出してしまったのかもしれません。
circeについて
混迷極まるScalaのJsonライブラリ事情ですが、最近はcirceというライブラリもメジャーになってきました。ただ個人的にはあまり仕事では採用する気にはならないライブラリです。
catsに依存している
circeの売りとして、cats以外のライブラリに依存していないというものがあるんですが、catsに依存してるという時点ですでに判断が別れるところだと思います。私はscalazやcatsは仕事ではほぼ使ったことがないので依存ライブラリがcatsに依存しているのは結構嫌です。またscalaz派から見ても同じようなライブラリが加わるのは嫌でしょう。
shapelessに依存している
なぜcirceの人気が出てきたかといえば、おそらくgenericの存在ではないでしょうか。
play-jsonでは毎回 implicit val hogeFormat = Json.Format[Hoge]
を書かされるのが辛いですが、circeのgenericを使えばその煩わしさから解放されます。
ところがcirceのgenericってつまりshapelessのgenericなんですよね。従ってcirceのgenericモジュールを使うとshapelessがくっついてきます。shapelessはscalazやcats以上に難易度が高いライブラリだと思います。少なくとも implicit val hogeFormat = Json.Format[Hoge]
と書きたくないばかりにshapelessを使う、というのはやり過ぎではないかと思います。
scalazに依存している
circeにはopticsというモジュールがあります。Jsonの構造をグリグリといじるのに便利なモジュールなんですが、これはMonocleというライブラリを使っています。Monocleはshapelessに加えてscalazを使っています。なんか強い奴らばっかり出てきますね。
まあopticsの機能はよくあるようなアプリケーションでは使わないでしょう。 jsonの構造を変更するとしてもそれは一旦jsonをドメインオブジェクトに変更したのちに、そのドメインオブジェクトを変更し、それをjsonに戻すという手順を踏むでしょう。jsonライブラリ自信がjsonの構造をいじるというのはjson自体が重要な関心事でない限りはあまり必要にならなそうです。
以上難しいライブラリ使いたくないマンのぼやきでした。