こんにちは、びしょ〜じょです。 最近はヴァーチャルユーチューバーに脳を破壊されてしまってほぼ毎日観てます。 剣持也くんが好きです。

1. はじめに

Scalaに入門するついでに、文献…というかPDFを管理するシステムを作った。

配色などはともかく、個人的には使いやすくなっとるんじゃないでしょうか。 もちろんボクが作ったので、どこを押すと何が起きるかは100%分かっているため、他の人にとって使いやすいかどうかはよくわかりませんが…。

2. Scala

case classでADTのようなものを定義できる。 トレイトTを継承すれば型Tのデータコンストラクタでまとめられるということでいいのかな。

trait T[A] {}

case class K1[A](a: A) extends T[A]
case class K2[A](a: A) extends T[A]

val s : Seq[T[Int]] = Seq(K1(0), K2(2))

パターンマッチの実態はクラスオブジェクトのunapplyメソッドで、カスタマイザビリティがなかなか高い。 case classapply/unapplyメソッドが自動でクラスのメンバになる。

class K1[A](val a: A) extends T[A] {}
/*
Javaでいうところの

class K1<A> {
  K1<A>(a: A) {
    this.a = a;
  }
}

でいいのか?
*/

object K1 {
  def apply[A](a: A): K1[A] = new K1[A](a)
  def unapply[A](k1: K1[A]): Option[A] = {println("match"); Option(k1.a)}
}

K1(0) match { case K1(x) => x } // prints "match" and returns 0

引数をクラスのメンバにしてくれたりもしているようだ。

またimplicitに定義されたメソッドにより安全な暗黙の型変換を定義できるのも面白い。

object Conv {
  implicit def fromStringToInt(s: String): Int =
    s.toCharArray.toSeq.map(_.toInt).fold(0) { _ + _ }

  def f(i: Int) = println(i * 10)

  def run() = f("aiueo")
}

// convert "aieuo" to Seq(97, 105, 101, 117, 111) ~> (folded)
Conv.run // 5310

型変換が必要そうなところで型の合う直近のimplicitメソッドを参照して変換する、のかな(わかってない)。 またスコープはメソッドの定義されたクラスを超えない。 ただし継承を使うと継承先のクラス内でも型変換が起こる。

Javaみもあって少々つらいときがある(String集合にnullが含まれたり、Javaいライブラリを使うとnullが混入する)が、 概ねモダンでOOPとしつつFunctional Programmingもしっかりできる言語でなかなか良い。 ビルドツールのsbtもカスタマイザビリティが高く、開発もわりとスムーズにいく。

3. Play web framework

Java/Scalaで使えるWebフレームワーク。 結構面白いんじゃないでしょうか、Webフレームワークを触ったことがなかったので評価できませんが。

3-1. ハマりポイント

現在はv2.6が最新だが、2.4、2.5、2.6で非互換な部分がいくつかあるが、インタネッツの記事にバージョンの表記がなかったりして、メソッドがないやんとか型合わないやんとかがあった。

4. Pnyao

本題。 PDFのメタデータにはiTextを使っている。 最初は何も考えずPDFメタデータの読み書きだけを実装したのでプロジェクトが分かれている。

(root)
  + ...
  + (subprj)

のようなプロジェクトの構成になっとるとき、rootのbuild.sbtでサブプロジェクトをガバっとやる。

build.sbt
lazy val subprj = project in file("subprj")
lazy val root = (project in file("."))
    .aggregate(subprj)
    .dependsOn(subprj)

.dependsOnでビルドの依存関係をやっておる。

他はなんか読んでください。

あとはサーバのイベントにアクションをフックするところが面倒だった。 クラスにDIしてApplicationLifecycleをランタイムに突っ込んでいく。

28
29
pnyao/app/services/Pnyao.scala
@Singleton
class Pnyao @Inject()(lifeCycle: ApplicationLifecycle) extends PnyaoService {

このオブジェクトにPlayサーバのイベント時に発火するアクションをフックできる。

59
60
pnyao/app/services/Pnyao.scala
 lifeCycle.addStopHook { () => Future.successful(work) }
 sys.addShutdownHook {() => work}

JVMの終了にもフックしている。 Playサーバのイベントにフックしてたのは、sbtシェル上で起動/終了をしてたから必要であって、後述のとおりSystemdで起動/終了をまかせるようにしたのでもはや不要かも。

この場合JVMの終了かPlayの終了かどちらかだけでアクションを発火してほしいので、lazy valとして定義すればいい。

他はもうないな。 JSONを扱う部分があり、circeとPlayのJSONライブラリという2つの変換器が混在している。 これは先述の通りPDFメタデータ扱う部分だけ最初に実装したことに起因している。

5. アッピケーション化

sbt-assemblyでプロジェクトをjarに固めて実行するようにした。 さらにサーバをsystemd serviceとして起動/停止できるようにした。

6. 所感

楽しかった、が、Webブラウザでのクリックイベントなどはジャバスクを書かざるをえなかったのがつらい。 ScalaJSをPlayがサポートしてくれればいいのかもしれない、ScalaJS書いたことないんですけど。 Scalaはちゃんとかいたのがここ3、4週間くらいなので、もうすこしやっていきたい。

7. おわりに

人生おわった…。