FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

お気に入りのScalaライブラリ・ツールをだらだら紹介する

この記事は Scala Advent Calendar 2019 の6日目です。

こんにちは。最近はPYXISのデータ基盤チームでSREっぽいことしている 門脇(@blac_k_ey)です。
TETRIS99の次はポケモンに進捗を奪われる日々を送っています。


締め切りギリギリまで ポケモンで忙しかった 「これ書きたい!」というテーマが思いつかなかったので、個人的に最近のScala開発でお世話になっているライブラリやツールなどを紹介していこうと思います。

詳細はあまり書かず、「ここが好き!」ぐらいのお気持ち表明ぐらいの文章でしかないので、ゆるく暖かい気持ちで読んでくれたら幸いです。

Ammonite

リッチなScalaREPL、Scalaスクリプト

自分のScala生活が豊かになっている大きな要因のひとつ。

Magic Importsを使って、ライブラリをちょっと試したいときにササッと使えるし、

$ amm
Loading...
Welcome to the Ammonite Repl 1.7.1
(Scala 2.12.10 Java 1.8.0_181)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@ import $ivy.`com.typesafe.play::play-json:2.7.4`
https://repo1.maven.org/maven2/com/typesafe/play/play-json_2.12/2.7.4/play-json_2.12-2.7.4.pom
  100.0% [##########] 4.2 KiB (1.0 KiB / s)
https://repo1.maven.org/maven2/com/typesafe/play/play-functional_2.12/2.7.4/play-functional_2.12-2.7.4.pom
  100.0% [##########] 1.5 KiB (12.0 KiB / s)
https://repo1.maven.org/maven2/com/typesafe/play/play-functional_2.12/2.7.4/play-functional_2.12-2.7.4-sources.jar
  100.0% [##########] 9.7 KiB (79.4 KiB / s)
https://repo1.maven.org/maven2/com/typesafe/play/play-functional_2.12/2.7.4/play-functional_2.12-2.7.4.jar
  100.0% [##########] 175.5 KiB (389.2 KiB / s)
https://repo1.maven.org/maven2/com/typesafe/play/play-json_2.12/2.7.4/play-json_2.12-2.7.4-sources.jar
  100.0% [##########] 52.5 KiB (94.5 KiB / s)
https://repo1.maven.org/maven2/com/typesafe/play/play-json_2.12/2.7.4/play-json_2.12-2.7.4.jar
  100.0% [##########] 706.6 KiB (705.2 KiB / s)
import $ivy.$

@ import play.api.libs.json._
import play.api.libs.json._

@ val json = Json.parse("""{"language": "Scala", "repl": "Ammonite"}""")
json: JsValue = JsObject(Map("language" -> JsString("Scala"), "repl" -> JsString("Ammonite")))

スケジュール実行に対応したCIツールを使えば、手元で書いたちょっとしたスクリプトでささっと自動化できるし、

(esa.ioのメトリクスをDatadogに送信する例)

# .gitlab-ci.yml

image: openjdk:8

scheduled_post_stats:
  only:
    refs:
      - schedules
    variables:
      - $SCHEDULED_JOB_POST_STATS
  script:
    - ./amm esa-stats-to-datadog.sc --team septeni-original

f:id:Nomad_Blacky:20191205234130p:plain

f:id:Nomad_Blacky:20191205234756p:plain

何度閉じてもダイアログが開くという犯罪臭のするスクリプトだって書けちゃう!!(?)

import java.awt._, event._
import javax.swing._

import scala.util.Random

val (screenCenterW, screenCenterH) = {
  val ss = Toolkit.getDefaultToolkit().getScreenSize
  (ss.getWidth.toInt / 2, ss.getHeight.toInt / 2)
}

val label = new JLabel("何回閉じても無駄ですよ~ww")

def randomLocation(): (Int, Int) =
  (screenCenterW + Random.nextInt(200) - 200, screenCenterH + Random.nextInt(200) - 200)

def newDialog(): Unit = {
  val dialog = new JDialog()
  dialog.setSize(300, 200)
  val (x, y) = randomLocation()
  dialog.setLocation(x, y)
  dialog.add(label)
  dialog.setVisible(true)
  dialog.addWindowListener(new WindowAdapter {
    override def windowClosing(e: WindowEvent): Unit = {
      newDialog()
    }
  })
}

newDialog()

f:id:Nomad_Blacky:20191206000910g:plain

こうしてAmmoniteは自分のScala生活に欠かせないものになっていきました。

気になった人は以下のコマンドを実行して早速インストール! (執筆時点の最新版)

$ sudo sh -c '(echo "#!/usr/bin/env sh" && curl -L https://github.com/lihaoyi/Ammonite/releases/download/1.8.2/2.13-1.8.2) > /usr/local/bin/amm && chmod +x /usr/local/bin/amm' && amm

Ammoniteに関してはいくつか資料を公開していますので、そちらもご覧いただけると幸いです。

upickle

JSON, MessagePack に対応したシリアライゼーションライブラリ。
Ammoniteの依存に含まれるため、上記のコマンドでインストールした方はすぐお試しできます。

@ import upickle.default._
import upickle.default._

@ case class Post(id: Int, name: String, tags: Seq[String])
defined class Post

@ implicit val postRW: ReadWriter[Post] = macroRW
postRW: ReadWriter[Post] = upickle.core.Types$ReadWriter$$anon$3@73fb1d7f

@ val json = """
    [
      {"id":1, "name":"post1", "tags":["Java"]},
      {"id":2, "name":"post2", "tags":["Scala", "Ammonite"]}
    ]
    """
json: String = """
  [
    {"id":1, "name":"post1", "tags":["Java"]},
    {"id":2, "name":"post2", "tags":["Scala", "Ammonite"]}
  ]
  """

@ val posts = read[Seq[Post]](json)
posts: Seq[Post] = Vector(Post(1, "post1", Vector("Java")), Post(2, "post2", Vector("Scala", "Ammonite")))

@ write(posts)
res5: String = "[{\"id\":1,\"name\":\"post1\",\"tags\":[\"Java\"]},{\"id\":2,\"name\":\"post2\",\"tags\":[\"Scala\",\"Ammonite\"]}]"

macroによって、最低限のボイラープレートでJSONの読み書きを実現できていますね。

他のJSONライブラリと比べて好きなところとしては、自身以外のライブラリに依存していないところでしょうか。

$ coursier resolve --tree com.lihaoyi::upickle:0.8.0
  Result:
└─ com.lihaoyi:upickle_2.13:0.8.0
   ├─ com.lihaoyi:ujson_2.13:0.8.0
   │  └─ com.lihaoyi:upickle-core_2.13:0.8.0
   │     └─ org.scala-lang.modules:scala-collection-compat_2.13:2.0.0
   │        └─ org.scala-lang:scala-library:2.13.0
   ├─ com.lihaoyi:upack_2.13:0.8.0
   │  └─ com.lihaoyi:upickle-core_2.13:0.8.0
   │     └─ org.scala-lang.modules:scala-collection-compat_2.13:2.0.0
   │        └─ org.scala-lang:scala-library:2.13.0
   └─ com.lihaoyi:upickle-implicits_2.13:0.8.0
      └─ com.lihaoyi:upickle-core_2.13:0.8.0
         └─ org.scala-lang.modules:scala-collection-compat_2.13:2.0.0
            └─ org.scala-lang:scala-library:2.13.0

$ coursier resolve --tree com.typesafe.play::play-json:2.7.4
  Result:
└─ com.typesafe.play:play-json_2.13:2.7.4
   ├─ com.fasterxml.jackson.core:jackson-annotations:2.9.8
   ├─ com.fasterxml.jackson.core:jackson-core:2.9.8
   ├─ com.fasterxml.jackson.core:jackson-databind:2.9.8
   │  ├─ com.fasterxml.jackson.core:jackson-annotations:2.9.0 -> 2.9.8
   │  └─ com.fasterxml.jackson.core:jackson-core:2.9.8
   ├─ com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.9.8
   │  ├─ com.fasterxml.jackson.core:jackson-core:2.9.8
   │  └─ com.fasterxml.jackson.core:jackson-databind:2.9.8
   │     ├─ com.fasterxml.jackson.core:jackson-annotations:2.9.0 -> 2.9.8
   │     └─ com.fasterxml.jackson.core:jackson-core:2.9.8
   ├─ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.8
   │  ├─ com.fasterxml.jackson.core:jackson-annotations:2.9.0 -> 2.9.8
   │  ├─ com.fasterxml.jackson.core:jackson-core:2.9.8
   │  └─ com.fasterxml.jackson.core:jackson-databind:2.9.8
   │     ├─ com.fasterxml.jackson.core:jackson-annotations:2.9.0 -> 2.9.8
   │     └─ com.fasterxml.jackson.core:jackson-core:2.9.8
   ├─ com.typesafe.play:play-functional_2.13:2.7.4
   │  └─ org.scala-lang:scala-library:2.13.0
   ├─ joda-time:joda-time:2.10.1
   ├─ org.scala-lang:scala-library:2.13.0
   └─ org.scala-lang:scala-reflect:2.13.0
      └─ org.scala-lang:scala-library:2.13.0

$ coursier resolve --tree org.json4s::json4s-native:3.6.7
  Result:
└─ org.json4s:json4s-native_2.13:3.6.7
   ├─ org.json4s:json4s-core_2.13:3.6.7
   │  ├─ com.thoughtworks.paranamer:paranamer:2.8
   │  ├─ org.json4s:json4s-ast_2.13:3.6.7
   │  │  └─ org.scala-lang:scala-library:2.13.0
   │  ├─ org.json4s:json4s-scalap_2.13:3.6.7
   │  │  └─ org.scala-lang:scala-library:2.13.0
   │  └─ org.scala-lang:scala-library:2.13.0
   └─ org.scala-lang:scala-library:2.13.0

$ coursier resolve --tree io.circe::circe-core:0.11.1
  Result:
└─ io.circe:circe-core_2.12:0.11.1
   ├─ io.circe:circe-numbers_2.12:0.11.1
   │  └─ org.scala-lang:scala-library:2.12.8
   ├─ org.scala-lang:scala-library:2.12.8
   └─ org.typelevel:cats-core_2.12:1.5.0
      ├─ org.scala-lang:scala-library:2.12.7 -> 2.12.8
      ├─ org.typelevel:cats-kernel_2.12:1.5.0
      │  └─ org.scala-lang:scala-library:2.12.7 -> 2.12.8
      ├─ org.typelevel:cats-macros_2.12:1.5.0
      │  ├─ org.scala-lang:scala-library:2.12.7 -> 2.12.8
      │  └─ org.typelevel:machinist_2.12:0.6.6
      │     ├─ org.scala-lang:scala-library:2.12.6 -> 2.12.8
      │     └─ org.scala-lang:scala-reflect:2.12.6
      │        └─ org.scala-lang:scala-library:2.12.6 -> 2.12.8
      └─ org.typelevel:machinist_2.12:0.6.6
         ├─ org.scala-lang:scala-library:2.12.6 -> 2.12.8
         └─ org.scala-lang:scala-reflect:2.12.6
            └─ org.scala-lang:scala-library:2.12.6 -> 2.12.8

Scala.js対応している点も人によっては嬉しいかもしれません。

あと、他のJSONライブラリと比較してパフォーマンスが良いそうですが、自身で試してないので深くは言及しません。

requests

とてもシンプルなHTTPクライアント。
PythonのRequestsから影響を受けているみたいです。

これもAmmoniteの依存に含まれます。

@ val response = requests.get("https://google.com")
response: requests.Response = Response(
  "https://www.google.com/",
  200,
  "OK",
  Map(
    "expires" -> Buffer("-1"),
    "server" -> Buffer("gws"),
    "p3p" -> Buffer("CP=\"This is not a P3P policy! See g.co/p3phelp for more info.\""),
    "x-xss-protection" -> Buffer("0"),
    "cache-control" -> Buffer("private, max-age=0"),
    "date" -> Buffer("Thu, 05 Dec 2019 12:54:14 GMT"),
    "content-type" -> Buffer("text/html; charset=ISO-8859-1"),
    "transfer-encoding" -> Buffer("chunked"),
    "x-frame-options" -> Buffer("SAMEORIGIN"),
    "alt-svc" -> Buffer(
      "quic=\":443\"; ma=2592000; v=\"46,43\",h3-Q050=\":443\"; ma=2592000,h3-Q049=\":443\"; ma=2592000,h3-Q048=\":443\...

@ response.text.take(100)
res1: String = "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/WebPage\" lang=\"ja\"><head><meta content"

import文なしにそのまま書ける点が地味ながらすき。

前述のupickleと合わせればWebAPIを叩くスクリプトも組めますね。

sbt-explicit-dependencies

コンパイル時に不要なライブラリや、暗黙的に依存しているライブラリを見つけてくれるsbtプラグインです。

ここでの「暗黙的に依存している」とは、ライブラリAが依存するライブラリBをアプリケーションコードが直接利用していることを指します。 (例: Circeの依存を追加して、ソースコード上でcatsを利用している)

↓インストール方法↓

// 執筆時点の最新版
addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.2.11")

たとえば、以下のようなプロジェクトがあったとします。

// build.sbt
lazy val root = (project in file("."))
  .settings(
    name := "explicitdeps",
    libraryDependencies ++= Seq(
      "com.lihaoyi" % "ammonite" % "1.8.2" cross CrossVersion.full
    )
  )
// Hello.scala
object Hello extends App {
  println(ujson.Obj("key" -> "value", "foo" -> "bar").render(indent = 2))
}

sbtを起動して、 unusedCompileDependencies, undeclaredCompileDependencies を実行します。

sbt:explicitdeps> unusedCompileDependencies
[warn] explicitdeps >>> The following libraries are declared in libraryDependencies but are not needed for compilation:
[warn]  - "com.lihaoyi" % "ammonite" % "1.8.2"
[success] Total time: 0 s, completed Dec 5, 2019 11:28:04 PM

sbt:explicitdeps> undeclaredCompileDependencies
[warn] explicitdeps >>> The project depends on the following libraries for compilation but they are not declared in libraryDependencies:
[warn]  - "com.lihaoyi" %% "ujson" % "0.8.0"
[success] Total time: 0 s, completed Dec 5, 2019 11:28:17 PM

前者でライブラリ依存に加えているのに使われていない ammonite の依存が検出され、
後者でアプリケーションコードで使われているのに明示的に依存していない ujson が検出されました。

これで、闇のライブラリ依存に対する防衛術が手に入りましたね。べんり!

不正な依存があった場合にエラーとなる unusedCompileDependenciesTest, undeclaredCompileDependencies コマンドもあるので、CIに組み込むといい感じになります。

例えば、GitHub Actionsで使う場合はこんな感じ

check_unused_compile_dependencies:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v1
    - name: Set up JDK 1.8
      uses: actions/setup-java@v1
      with:
        java-version: '1.8'
    - name: Check unused compile dependencies
      run: sbt test/unusedCompileDependenciesTest

check_undeclared_compile_dependencies:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v1
    - name: Set up JDK 1.8
      uses: actions/setup-java@v1
      with:
        java-version: '1.8'
    - name: Check undeclared compile dependencies
      run: sbt test/undeclaredCompileDependenciesTest

プロジェクト導入初期からぜひ入れたいプラグインですね!

scala-steward

依存ライブラリのアップデートを探してリポジトリにプルリクエストを投げてくれるとてもかしこいbot
似たものとしてはdependabotなどがあります。
(こちらはsbtに非対応)

自分で動かすことも可能ですが、今回はコミュニティで定期的に動かしてくれているものを使いましょう。

このファイルに自身が公開しているGitHubリポジトリを加えて、プルリクエストを出します。

f:id:Nomad_Blacky:20191205235415p:plain

そのうち筆者のfthomasさんがマージしてくれます。(感謝)

すると、1日1回ぐらい(?)の頻度でライブラリのアップデートを確認して、自身のリポジトリプルリクエストを出してくれます。

f:id:Nomad_Blacky:20191205235445p:plain

f:id:Nomad_Blacky:20191205235548p:plain

自分でライブラリのアップデートを確認する作業は骨が折れるので、とても便利ですね!
便利と感じたら、ぜひ感謝の気持ちとしてBadgeを付けましょう。

f:id:Nomad_Blacky:20191205235626p:plain

まとめ

Scalaすき…