Scala用のRisonパーサを作ってみた

RisonというのはJSONライクかつURLに埋め込みやすいようURLエンコーディングが最小限になるよう設計されたデータフォーマットだそうで、Kibanaなどで使われているそうです。日本語だと以下の記事が詳しいです(自分もこの記事を見てRisonを知りました)。

qiita.com

すでにJavaScriptPythonGolangなど様々な言語向けのライブラリが存在するようですが、Scala用のライブラリが見当たらなかったので久しぶりにScalaのパーサコンビネータライブラリを使って実装してみました。

github.com

こんな感じで使います。

import com.github.takezoe.rison._

val parser = new RisonParser()

// parse
parser.parse("(name:Lacazette,age:27)") match {
  case Right(node) => println(node.toScala) // => Map(name -> Lacazette, age -> 27)
  case Left(error) => println(error)
}

// convert from Scala's Map
val node: RisonNode = RisonNode.fromScala(
  Map(
    "name" -> "Alexandre Lacazette", 
    "twitter" -> "@LacazetteAlex"
  )
)
println(node.toRisonString) // => (name:'Alexandre Lacazette',twitter:'@LacazetteAlex')

// URL encode
val encoded: String = node.toUrlEncodedString
println(encoded) // => (name:'Alexandre+Lacazette',twitter:'@LacazetteAlex')

O-risonやA-risonにも対応しています。

// O-rison
val orison: ObjectNode = parser.parseObject("name:Lacazette,age:27")
println(orison.toObjectString) // => name:Lacazette,age:27

// A-rison
val arison: ArrayNode = parser.parseArray("Lacazette,Aubameyang,Ozil")
println(arison.toArrayString) // => Lacazette,Aubameyang,Ozil

上記のサンプルではMapSeqを使っていますが、ケースクラスを使うこともできます。

// case class
case class Player(name: String, age: Int)

// convert from case class
val node: RisonNode = RisonNode.fromScala(Player("Lacazette", 27))
println(node.toRisonString) // => (name:Lacazette',age:27)


// convert to case class
val obj: Player = node.to[Player]
println(node.) // => Player(Lacazette, 27)

ケースクラスとの相互変換にはairframe-surfaceを使いました。

airframe-surfaceは型情報を取得してごにょごにょするためのScalaライブラリで、リフレクション関連の面倒な処理を隠蔽してくれます。Scalaだと同様の処理にはShapelessのようなライブラリを使う方法もありますが、airframe-surfaceはリフレクションならではの制限はあるものの気軽に使えて便利です。

このRisonパーサは冬休み中に作っていたのですが、パーサコンビネータやairframe-surfaceなどいろいろ試すことができてなかなか良い題材でした。

Java/Scala用機械学習ライブラリ「Smile」を使ってみる

手軽に使える機械学習ライブラリというとPythonのscikit-learnが有名ですが、Java/ScalaでもSmileというライブラリがあったので軽く試してみました。

github.com

まずはリリースページからzipファイルをダウンロードして適当な場所に展開します。bin/smileで対話シェルが起動します。

f:id:takezoe:20190104102332p:plain

サンプルデータがついているのでこれを使って学習してみます。

smile> val toy = read.table("data/classification/toy/toy-train.txt", response = Some(new NominalAttribute("class"), 0))
smile> val (x, y) = toy.unzipInt
smile> val model = knn(x, y, 3)

作ったモデルを使って予測してみます。

smile> model.predict(x(0))
res3: Int = 0

学習データとモデルをプロットしてみます。別ウィンドウが立ち上がって以下のようなグラフが表示されるはずです。SmileはJupyterでも使うことができるようですが、その場合はちゃんとノートブックにグラフが表示されるのだろうか…。

smile> plot(x, y, model)

f:id:takezoe:20190104103334p:plain

テストデータを使って精度を確かめてみます。

smile> val test = read.table("data/classification/toy/toy-test.txt", response = Some(new NominalAttribute("class"), 0))
smile> val (testx, testy) = test.unzipInt
smile> val pred = testx.map(model.predict(_))
smile> accuracy(testy, pred)
res9: Double = 0.81205

モデルの保存、読み込みはJava標準のバイナリシリアライゼーションで行われるようです。Xstreamを使ってXML形式で保存する機能も用意されているようです。読み込むときはちゃんとキャストしてあげないとAnyRef型になってしまうので注意が必要です。

smile> write(model, "toy.model")
smile> val model = read("toy.model").asInstanceOf[KNN[Array[Double]]]

データ加工にScalaのコレクションAPIを使えるのは便利ですが、もうちょっと高度なライブラリが欲しいかなという気もします。その辺は別のライブラリを組み合わせればよいのでしょう。以下のような感じで外部ライブラリが使えるみたいです。

smile> import $ivy.`org.scalaz::scalaz-core:7.2.7`, scalaz._, Scalaz._

思っていたよりサクッと使えましたし、対話シェルやグラフの描画などもよくできていました。Java/Scalaアプリケーションにちょっとした機械学習を利用した機能を組み込みたい場合には便利に使えるのではないかと思います。

Scalaのテストケース内でDockerコンテナを使う

GitBucketではもともとデータベースを使ったテストに組み込みMySQLやPostgreSQLの機能を提供するライブラリを使っていたのですが、これらのライブラリのメンテナンスが怪しくなかなか新しいバージョンに対応したテストを行うことができないという問題があり、Dockerを使えないかなと思っていたところ、以下のようなライブラリを教えてもらいました。

github.com

プログラム中からDockerコンテナを制御するためのライブラリで、Scala版の他にもJava版やGo版など様々な言語向けのライブラリが提供されています(Scala版はJava版のラッパーのようです)。

デフォルトでよく使うコンテナ向けの実装が提供されているのが特徴で、たとえばMySQLを使う場合、まずは以下の依存関係をbuild.sbtに追加します。

libraryDependencies ++= Seq(
  "com.dimafeng" %% "testcontainers-scala" % "0.22.0" % "test",
  "org.testcontainers" % "mysql" % "1.10.3" % "test"
)

以下のような感じでコンテナを使ったテストを簡単に行うことができます。JDBC URLやユーザ名、パスワードをコンテナから取得できるのが地味に便利です。

import com.dimafeng.testcontainers.{ForAllTestContainer, MySQLContainer}

class MysqlSpec extends FlatSpec with ForAllTestContainer {

  override val container = MySQLContainer()

  it should "do something" in {
    Class.forName(container.driverClassName)
    val connection = DriverManager.getConnection(
      container.jdbcUrl, container.username, container.password)
    ...
  }
}

複数のコンテナを使いたい場合はMultipleContainersを使います。

val mySqlContainer = MySQLContainer()
val postgresContainer = PostgreSQLContainer()

override val container = MultipleContainers(
  mySqlContainer, postgresContainer)

また、DockerComposeContainerでdocker-compose.ymlを使ってコンテナを起動することもできるようです。

override val container = DockerComposeContainer(
  new File("src/test/resources/docker-compose.yml"))

ちなみにデフォルトで用意されていないコンテナを使いたい場合はGenericContainerあたりを継承して自分で作成します。JDBC経由でアクセスするデータベースであればJdbcDatabaseContainerを継承するのが簡単です。

コンテナの起動をForAllTestContainerで制御するのではなく、コード中で直接制御することもできます。

val container = new MySQLContainer()
container.starting()
...
container.finished()

GitBucketでは特定のテストケースで複数の種類・バージョンのデータベースでテストを行う必要があったので、以下のような感じでテスト内で必要に応じて対象バージョンのコンテナを起動するような使い方をしています。

Seq("11", "10").foreach { tag =>
  test(s"Migration PostgreSQL $tag") {
    val container = PostgreSQLContainer(s"postgres:$tag")
    container.starting()
    try {
      // ここでテスト
    } finally {
      container.finished()
    }
  }
}

簡単に使える上で拡張性もあってなかなか便利なライブラリですね。

なお、同種のライブラリとして以下のようなものもあるようです。こちらでも同じようなことができそうですが、プログラム中からコンテナ単体での制御を行うことはあまり考えられていなそうな印象です。

github.com

2018年の振り返り

今年も今日で仕事納めなので1年の振り返りを書いてみたいと思います。

f:id:takezoe:20181228085429j:plain

Arm Treasure Dataに転職しました

前職であるビズリーチでは割と自由にやらせていただいていたのですが、先のことも色々考えないとなと思っていたところ(とは言っても積極的に転職活動していたわけではないのですが…)縁あって今年の8月にトレジャーデータに転職することになりました。

takezoe.hatenablog.com

takezoe.hatenablog.com

今のところはPresto、Sparkやその周辺のツール・ライブラリ開発などをメインでやっています。チャレンジングな課題が多く、英語力に関しても日々鍛えられていますw チームメンバーも猛者揃いで学ぶことばかりの日々ですが、少しでも多く貢献していけるよう努力していきたいと思います。

英語学習について

元々続けていたのですが、トレジャーデータへの転職もあり、英語学習に使う時間が一気に増えました。オンライン英会話をやりつつ、書籍やスマートフォンアプリなどを使ってボキャブラリを強化するという感じでやっていますが、まだまだ厳しいです。会話を追いかけるだけでも集中力が必要なので短時間のミーティングでもかなり消耗を感じます。

まあ、人間追い詰められたらやらざるを得ないというのはそうなのですが、あまり追い詰められすぎると精神衛生上も良くないですし、他の人にも迷惑がかかるので、追い詰められなくても済むのであればそれに越したことはないです。というわけで英語は余裕がある時にちゃんと勉強しておくと良いと思いますw

OSSなどの個人活動について

前半はAlpakkaにいくつか新機能をコントリビュートしたりといった活動ができていたのですが、後半は転職していっぱいいっぱいだったということもありあまりアクティブな活動はできませんでした。GitBucketもついに月一のリリースペースを崩してしまいました。

takezoe.hatenablog.com

takezoe.hatenablog.com

とはいえ、台湾で開催されたJCConf 2018や、Scala関西サミット2018で登壇の機会をいただいたり、PredictionIOのAPIサーバをSprayからAkka HTTPに移行したりと、それなりに活動はできたのではないかと思います。

takezoe.hatenablog.com

github.com

あと、以前書いたJavaの書籍の改訂作業を行なっています。できれば転職前に終わらせておきたかったのですが、Java 11のリリース時期の関係もあり現職に持ち越してしまいました。まだ作業中ですが、来年春頃発売されると思います。

まとめ

歳のせいもあってか体力的にもしんどい感じになってきてしまったので、2019年は外向けの活動は少し控えめにして、できるだけ業務に集中する時間や、英語学習に使う時間を増やしたいと思っています。OSS活動も今までと違って仕事と関係ないものに手を出す余力はあまりなさそうなので、注力するものをある程度絞っていかないといけないと思います。もちろんGitBucketは今後も続けていくつもりです!

GitBucket 4.30.0をリリースしました

Scalaで実装されたオープンソースのGitサーバ、GitBucket 4.30.0をリリースしました。

https://github.com/takezoe/gitbucket/releases/tag/4.30.0

ChangeLogの自動生成

リリースの作成時にGitのコミットログから直前のタグとの間の更新履歴を自動生成できるようになりました。"Insert ChangeLog"というボタンをクリックするとリリースの説明文にコミットの一覧が挿入されます。直前のタグは手動で選択することも可能です。

f:id:takezoe:20181215100240p:plain

Web APIの追加

以下のWeb APIが新たに追加されました。

Gistプラグインでのグループのサポート

Gistプラグインでグループのスニペットが作れるようになりました。

f:id:takezoe:20181215100307p:plain

Markdownでのチェックボックス表示

Markdown中に以下のような記述があった場合、IssueやPull requestではチェックボックスとして表示されていましたが、Gitリポジトリ内のMarkdownファイルでは表示されていませんでした。このバージョンからはdisabledなチェックボックスとして表示されるようになりました。

- [ ] checkbox1
- [ ] checkbox2

f:id:takezoe:20181215103848p:plain

タイムラインからリリースページへのリンクを表示

アクティビティのタイムラインからリリースページへのリンクが表示されるようになりました。

f:id:takezoe:20181215103927p:plain

プラグイン向けの新しい拡張ポイント

プラグイン向けに新たにanonymousAccessiblePathsという拡張ポイントが追加されました。この拡張ポイントによってプライベートモード(アクセスするには必ずログインが必要)で運用されているGitBucket上での認証不要のページをプラグインから提供できるようになります。

今回のバージョンではこの他にも様々な改善やバグフィックスを行っています。詳細についてはIssueの一覧をご覧ください。

Akka Streamsで簡単にリトライするFlowを作る

akka-http-contribというリポジトリRetryというユーティリティがあり、リトライ処理が簡単に書けるようだったので試しに見てみました。

github.com

テストケースによると使い方はこんな感じです。

def flow[T] = Flow.fromFunction[(Int, T), (Try[Int], T)] {
  case (i, j) if i % 2 == 0 
    => (Failure(new Exception(“cooked failure”)), j)
  case (i, j)
    => (Success(i + 1), j)
}
val (source, sink) = TestSource.probe[Int]
  .map(i => (i, i))
  .via(Retry(flow[Int]) { s =>
    if (s < 42) Some((s + 1, s + 1)) // recover
    else None // give up
  })
  .toMat(TestSink.probe)(Keep.both)
  .run()

Retry.apply()は以下の2つのパラメータを取ります。

  • リトライさせたいFlow。このFlowTrySuccessまたはFailure)を返すように実装しておく必要があります。
  • 失敗した時のリカバリ用関数。この関数はOptionSomeまたはNone)を返すように実装しておく必要があります。

FlowFailureを返した場合、リカバリ用の関数が呼ばれるという仕組みです。リカバリ用の関数がSomeを返した場合はその値でリトライされ、Noneを返した場合はリトライをギブアップします。

簡単なものですが、処理に失敗する可能性のあるフローをシンプルにリトライ可能にしたい場合には便利なユーティリティだと思います。akka-stream-contribにはこの他にも便利なユーティリティがあるようなのでAkka Streamsを使っているのであれば一度チェックしてみるとよいと思います。

SONYのオープンイヤー型イヤホンSTH40DJPを買ってみた

以前ビデオ会議の際にオープンイヤー型のイヤホンなら自分の声も聞こえて便利という話を聞いていたのですが、最近自分もオンラインミーティングや英会話をやる機会が多いので購入してみました。

実際に使ってみると、イヤホンで音を鳴らしたまま周囲の音がちゃんと聞こえるというのはなかなか新体験です。思っていたより音質もいいですし、音漏れも気になりません。ただ、当然ですが電車など騒音の大きな場所ではその音もそのまま聞こえるので打ち消されてイヤホンの音が聞こえなくなります。

イヤホン自体は見た目より軽いのですが形状が独特なので合う合わないはあるかもしれません。ずっとイヤホンを鳴らしていても聴き疲れ感はないのですが、自分は耳がちょっと痛くなる感じがあります。また、マイク内蔵のリモコンがついており、自分の使っているAndroidスマートフォンでは再生・停止、ボリュームの増減がちゃんと機能しました。

騒音の多い通勤電車や集中したい場合はこれまで通りSHUREの弾丸イヤホン、オンラインミーティングや英会話、仕事中などはこちらと使い分けてみたいと思います。

ワイヤレス版もあり、こちらならイヤホンをつけたまま家の中を移動できそうです。が、お値段が大分張ります…。オープンイヤー型に限らず、左右分離型のワイヤレスイヤホンは今だとちゃんとしたものを買おうとするとこのくらいの価格帯になる感じなのでもう少し様子見ですかね…。