少年易酔學難成

IT/技術的な話題について書きます

Scala開発日記 2019/05/14

5月から1年ぶりにScalaを使った開発が始まりました。

1年ぶりにScalaを触った感想は、「思った以上に忘れてる、ヤバイ」でした。

せっかくまたScala触れたのだから、今度はわすれないよう、日々のScala開発で感じたことやハマったことをメモしていこうと思います。

目次

  • ScalikeJDBCのTypeBinderでハマった話

  • PlayFrameworkの(QueryString|Path)Bindableをもっと便利にしたい

ScalikeJDBCのTypeBinderでハマった話

現在の仕事は、とあるシステムの内部APIをPlayFramework+Scalaで作成するというものです。 DBへのアクセスにScalikeJDBCを使っています。

現象

Scalaではなるべく型安全にしたいということで値オブジェクトを使っています。

一方、DBはレガシーなもので、nullableなカラムが数多くあります。

そのnullableなカラムの値をTypeBinder[Option[A]]を使って受け取るときに問題が起きました。

実際のコードとは少し違う1のですが、最小で問題を再現するTypeBinderは↓の通りです。

import scalikejdbc._

case class UserName(value: String) extends AnyVal

implicit lazy val userNameBinder: Binders[UserName] = Binders.string.xmap(UserName, _.value)

これを使ってカラムのnullを受けようとしたところ、期待していたNoneではなくSome(UserName(null))という大変ざんねんな結果になってしまいました。

原因

値オブジェクトのバインドにLowPriorityTypeBinderImplicits#optionによって生成されたTypeBinder[Option[UserName]]が使われるからです。

implicit def option[A](implicit ev: TypeBinder[A]): TypeBinder[Option[A]] = new TypeBinder[Option[A]] {
  def apply(rs: ResultSet, columnIndex: Int): Option[A] = wrap(ev(rs, columnIndex))
  def apply(rs: ResultSet, columnLabel: String): Option[A] = wrap(ev(rs, columnLabel))
  private def wrap(a: => A): Option[A] =
    try Option(a) catch { case _: NullPointerException | _: UnexpectedNullValueException => None }
}

UserNameの場合、先にTypeBinder[UserName]を使って受けてからOptionでくるんでいるのでSome(UserName(null))のようになります。 もし値オブジェクトの保持する値がLongなら発生しなかったとでしょう。

解決方法

色々アホなことして遠回りしましたが2、ScalikeJDBCがLongなど他のAnyValを処理している方法を真似して、UserNameにバインドする時に文字列がnullだったらNullPointerExceptionを吐くようにしました。

implicit lazy val userNameBinder: Binders[UserName] = Binders.string.xmap(throwExceptionIfNull(UserName), _.value)

// Bindersのコンパニオンオブジェクトで使われているもの
def throwExceptionIfNull[A, B](f: B => A): B => A = {
  a => if (a == null) throw new NullPointerException else f(a)
}

PlayFrameworkの(QueryString|Path)Bindableをもっと便利にしたい

これは未解決というか感想レベルの話なんですけど、(QueryString|Path)Bindableって既存の(QueryString|Path)Bindableからの導出方法がtransformしかなくて、Prismを使って上手く導出出来ないかな?って思っています。 bindの処理を合成するときにflatMapみたいな感じでやりたいです。

まあ、今日は時間もないし、とりあえず良い案でるまで開発進めながら考えようという感じです。


  1. 値オブジェクトの導入によるボイラープレートを軽減するためIsoとか値オブジェクト用のtraitやらを使ってみている。

  2. きっと体調がわるかったせいと自分を慰めている。

SQLにもタプルがあるよ

地味Tipsです。 意外と知らない人もいるので。

SELECT
    *
FROM
    foo
WHERE
    (id, bar_id) IN ((1, 2), (2, 3), (3, 4))

昔、Haskell勉強してたときに、なんとなくSQLでもタプル使えたらいいなあと思って試してみたらイケた書き方。

カラムの組み合わせで条件指定したいことはよくあるので結構重宝します。

第0回 ゆるふわ.scalaで発表しました。

発表資料

PlayFrameworkでFuture[Either[A, B]]がよく出てくるけど、それにどう対応したかを書きました。

スライドを投稿してから気づいたけど、この内容だとまるで自分が解決策を考えたかのような印象与えますね。

解決策を考えたのは私じゃありません。私は知ったかぶりしてるだけです。

ゆるふわ.scalaについて

技術的な話をYokohama.scala以外でするのは初めてで、会場につく前は正直めっちゃ緊張しました。

が、別に緊張する必要ないくらいゆるふわな雰囲気でした。

次回はわいわい.scala?に変わるっぽいですが、またネタ考えて発表したいですね。

QuickTheoriesを使ってProperty Based Testingをやってみよう

(追記) Property-Based Testing Advent Calendar 2018参加にあたって、新たに記事を書こうと思ったのですが、公私にわたる多忙によりあきらめました。 せめて内容を加筆しようと思ったのですが、精根尽き果てました。 大変申し訳ありまきねん。

背景

テストを書くときにテストデータを用意するのは精神的な苦痛を伴うし、また正しく作成するのはなかなか大変な作業です。
私が先月まで居た案件のSuper Scala-ManはこういうときにProperty Based Testingライブラリを使用してデータを用意するそうです。

私も面倒は嫌いな方なので、これを試してみることにしました。

Property Based Testingやその活用方法についてはこちらがわかりやすいです。

さて、最近もメンテされているJavaのProperty Based Testingライブラリは、いくつかの選択肢があるようです。

  • JUnit-QuickCheck
  • FunctionalJava-Quickcheck
  • QuickTheories

私はこのなかでドキュメントもある程度揃っていて、JUnitで独自のTestRunnerを使用しなくても実行できるQuickTheoriesを選択しました。

基本的な書き方

基本的な使い方はREADMEを見てもらうのが早いと思いますが、すこしだけ解説します。

import static org.quicktheories.QuickTheory.qt;
import static org.quicktheories.generators.SourceDSL.*;

public class SomeTests {
  @Test
  public void addingTwoPositiveIntegersAlwaysGivesAPositiveInteger(){
    qt()
    .forAll(
      integers().allPositive(), // 1. 整数のジェネレータを用意する。この場合は正の整数が2つ生成される。
      integers().allPositive())
    .check((i,j) -> i + j > 0); // 2. Genが生成した値に対して行うテストを記述する
  }
}

このテストは、すべての(forAll)2つの正の整数の組み合わせに対して、その合計が0以上であることをチェック(check)すると読めます。
実際にはすべての組み合わせをテストすることはできないので、ランダムにいくつかの組み合わせをチェックします。

試行回数が増えればそれだけ時間もかかるので、QuickTheoriesでは以下のように試行回数も指定することができます。

import static org.quicktheories.QuickTheory.qt;
import static org.quicktheories.generators.SourceDSL.*;

public class SomeTests {
  @Test
  public void addingTwoPositiveIntegersAlwaysGivesAPositiveInteger(){
    qt()
    .withExamples(5) // 5つの組み合わせだけテストします。
    .forAll(
      integers().allPositive(),
      integers().allPositive())
    .check((i,j) -> i + j > 0);
  }
}

forAllに渡されているintegers().allPositive()の型はGenで、RandomnessSourceからIntegerを生成する責務を持っています。

Genインタフェースは抽象メソッドを一つだけ(generateメソッド)もつ関数型インターフェースなので、ラムダ式を代入できます。

// たとえば1だけをひたすら生成するGenはこんな感じ。
Gen<Integer> integerGen = rs -> 1;

実際のプロジェクトへの適用を考える

実際のプロジェクトで使うのであれば、(巨大な)Entityの生成が必要になることもあるでしょう。 QuickTheoriesのドキュメントをざっと読んだ限り、そのような巨大なEntityのGenを生成する便利な仕組みは提供されていないように思います。
そこで、今回は以下のような巨大なEntityを例に、どのようにGenを生成するか考えて見たいと思います。

import lombok.*;

@Getter
@Setter
@AllArgsConstructor
public class Employee {
  private Integer id;
  private String name;
  // EmployeeでcompanyCd持ってることに対するツッコミは無しでお願いします。
  // なんか、元ネタにしたコードにあったんですよね...
  private Integer companyCd;
  // ... ものすごくたくさんのフィールド
}

Genを作る

単純にGen<Employee>を作るのならば、以下のように各フィールドに対応するGenを用意すれば良いでしょう。

import static org.quicktheories.generators.SourceDSL.*;
...
Gen<Employee> empGen = new Gen {
  private Gen<Integer> idGen = integers().all();
  private Gen<Integer> nameGen = strings().ascii().ofLengthBetween(1, 15);
  private Gen<Integer> companyCdGen = integers().all();
  
  @Override
  public Employee generate(RamdomnessSource rs) {
    return new Employee(
      idGen.generate(rs),
      nameGen.generate(rs),
      companyCdGen.generate(rs),
      // ... 以下フィールド分続く...
    );
  }
};

DSLを作る

QuickTheoriesはSourceDSLのように各データ型に対するDSLを用意しています。 同じようにEntityにもDSLを用意してあげるほうが良いでしょう。

public class EmployeeDSL {
  private Gen<Integer> idGen;
  private Gen<String> nameGen;
  private Gen<Integer> companyCdGen;
  // ... 以下フィールド分

  public EmployeeDSL() {
    this(
      integers().all(),
      strings().ascii().ofLengthBetween(1, 15),
      integers().all(),
      // ... 以下フィ
    );
  }

  private EmployeeDSL(Gen<Integer> idGen, Gen<String> nameGen, Gen<Integer> ageGen, Gen<Integer> companyCdGen) {
    this.idGen = idGen;
    this.nameGen = nameGen;
    this.companyCdGen = companyCdGen;
    // ... 以下フィ
  }

  public Gen<Employee> one() {
    return in -> new Employee(
      this.idGen.generate(in),
      this.nameGen.generate(in);
      this.companyCdGen.generate(in);
      // ... 以下
    );
  }

  // ここらへんはoneにしか依存しないのでinterfaceのdefault実装で提供してもよさそう
  public Gen<List<Employee>> listOfSize(int size) {
    return lists().of(one()).ofSize(size);
  }

  public Gen<List<Employee>> listOfSizeBetween(int minSize, int maxSize) {
    return lists().of(one()).ofSizeBetween(minSize, maxSize);
  }

  public Gen<List<Employee>> listOfSizes(Gen<Integer> sizeGen) {
    return lists().of(one()).ofSizes(sizeGen);
  }
  // ... 
}

このDSLにより以下のようにテストを書けるようになります。

  @Test
  public void addingTwoPositiveIntegersAlwaysGivesAPositiveInteger(){
    qt()
    .forAll(
      employees.listOfSize(10)
    .checkAssert(employees -> {
      ... // なにかテスト処理
    });
  }

ところで現在のidGenだと上記のように生成したemployeesの中でidが重複する可能性があります。
(実際にQuickTheoriesは重複が発生しやすいように思います。原因教えてください。)

重複した値を生成したくない場合は、こんなGen<Integer>を用意するといいかもしれません。 これをidGenとして使えば0, 1, 2 ...と順番にIDが生成されるので重複を避けることが出来ます。

  public static Gen<Integer> integerSequenceGen() {
    var seed = IntStream.iterate(0, i -> ++i).iterator();
    return in -> seed.next();
  }

またデータ生成に使うには各フィールドを固定値にしたいと思うかもしれません。
例えば、companyCdは外部キーで、DBに存在する値で固定したいとします。 その場合は以下のメソッドをEmployeeDSLに追加します。

  public EmployeeDSL companyCd(Integer companyCd) {
    this.companyCdGen = Generate.constant(companyCd);
    return this;
  }

idはDBのシーケンスで生成する場合、エンティティに値を設定しない方がいいときもあります。 例えばDomaではデータ挿入時にIDに値が設定してあればその値を使うようなので、EntityにIDを設定しているとキー重複エラーを引き起こす可能性があります。 この場合もcompanyCdと同様に固定値を設定するメソッドを追加すれば以下のように書けます。

  @Test
  public void addingTwoPositiveIntegersAlwaysGivesAPositiveInteger(){
    qt()
    .forAll(
      // Professional Null Cleanerに消されるやつ。
      employees.id(null).companyCd(1).listOfSize(10),
      employees.id(null).companyCd(2).listOfSize(10)
    .checkAssert((company1Employees, company2Employees) -> {
      ... // なにかテスト処理
    });
  }

こんな感じで求めるコードのフォーマットが決まれば、あとはDSLを生成するスクリプトなりGradleプラグインを作れそうです。

まとめ

現場からは以上です。

Scalaでもパイプラインできるもん!

Elixirのパイプライン演算子が羨ましくなったので作ってみました。

と言っても、こいつ↓でオブジェクトにパイプライン演算子を生やすだけです。

implicit class Pipe[A](value: A) {
  def |>[B](f: A => B): B = f(value)
}

こんな感じでかけます。

List(List(1,2,3),List(4),List(5)) |> (_.flatten) |> (_.map(Math.pow(_, 2))) // => List(1, 4, 9, 16, 25)

見易く演算子の前で改行を入れる場合はそのままだとエラーになるので、ちょっとカッコ悪いですが.を入れましょう。

List(List(1,2,3),List(4),List(5))
  .|> (_.flatten)
  .|> (_.map(Math.pow(_, 2))) // => List(1, 4, 9, 16, 25)

メソッドチェーンとは違い(メソッドだけでなく)関数のチェーンを型注釈なしで書けるので、割と使い勝手が良さそう。 値クラスに出来ればもっと良いのだけれど。

二分木の畳み込みを末尾再帰化したい人生だった

※ この記事はアルゴリズムに詳しくない人が自分の思考の記録のために書いた記事です。 なので書いた内容が合っているかは確証が持てません。 「大間違いだよ、馬鹿野郎!」というツッコミは大歓迎します。

突然どうした

現在、自分はプロジェクトでScalaを使っている。 そのプロジェクトでは毎週木曜日に1時間FPinScalaの勉強会をやっているのだけど、現在読んでいる3章は前にも解いたことがあり、ただ同じように解いてもつまらないのでなるべく末尾再帰の形にするようにしている。

昨日はexercise3.25を解いていた。 この問題は二分木のノードの数をカウントするsize関数を実装せよ、という問題だ。 ここで用いる二分木の定義は以下の通り。

sealed trait Tree[+A]
case class Leaf[A](value: A) extends Tree[A]
case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]

で、size関数は普通に書けば↓のように書ける。

def size[A](tree: Tree[A]): Int = tree match {
  case Leaf(_)      => 1
  case Branch(l, r) => size(l) + 1 + size(r)
}

これの末尾再帰化は二分木だし結構時間かかるのでは…と思ったら、意外にもすぐに末尾再帰化できた。

def size[A](tree: Tree[A]): Int = {
  // ListはFPinScalaで定義した自前のものを使っている
  @annotation.tailrec
  def go[A](tree: Tree[A], acc: Int, notCalculated: List[Tree[A]]): Int = tree match {
    case Leaf(_) => notCalculated match {
      case Nil        => acc + 1
      case Cons(h, t) => go(h, acc + 1, t)
    }
    case Branch(l, r) => go(l, acc + 1, Cons(r, notCalculated))
  }
  go(tree, 0, Nil)
}

二分木を扱う関数で末尾再帰化するイメージは自分にはなかったので、とても感動した。 この例では、未計算のツリーのリストを再帰で渡してやることで難なく末尾再帰化できるのだ。

もう少し突き詰めると、スタックの代わりとなるデータ型をうまく考えてやれば色々なものを末尾再帰化できるようになるのではないか。 例えば、二分木のfoldやmap関数だ。

そして試行錯誤が始まった

元々書いていたfold関数の回答は↓の通り。

def fold[A, B](a: Tree[A])(f: A => B)(g: (B, B) => B): B = a match {
  case Branch(l, r) => g(fold(l)(f)(g), fold(r)(f)(g))
  case Leaf(v)      => f(v)
}

これをまずはこのように修正した。

def fold[A, B](tree: Tree[A])(f: A => B)(g: (B, B) => B): B = {
  @annotation.tailrec
  def go(tree: Tree[A], acc: Option[B], notCalculated: List[Tree[A]])(f: A => B)(g: (B, B) => B): B = tree match {
    case Leaf(v) => notCalculated match {
      case Nil        => acc.map(g(_, f(v))).getOrElse(f(v))
      case Cons(h, t) => go(h, acc.map(g(_, f(v))).orElse(Some(f(v))), t)(f)(g)
    }
    case Branch(l, r) => go(l, acc, Cons(r, notCalculated))(f)(g)
  }
  go(tree, None, Nil)(f)(g)
}

が、これは実装の途中で悪手だと気づいた。 これは木の左側からLeafを見つけ次第たたみ込んでいるので元々のfold関数と評価の順番が変わってしまっている。 畳み込み関数gの実装によっては出力される答えが変わってしまうだろう。

ここで元々のfold関数の評価順を守ろうとすると別の問題が出てくる。 再帰呼び出しで渡すデータは未計算のツリーだけではなく左側のLeafで計算した値も必要になるのだ。

この問題の解決方法はなかなか思い浮かばなかった。

途中、notCalculated: List[Tree[A]]の他に、別の引数として左側のリーフを計算した値leftAcc: Option[B]も一緒に渡す事も検討したが、 処理するLeafが左側であるのか右側であるのかわからないのでうまく行かない。

ここで初めてfoldを実装するには関数がツリーの構造を知る必要があるのだと気づいた。

そこでnotCalculated: Tree[A]ではなく(Option[B], List[Tree[A]])のようなデータを渡したらどうかとも考えたが、データ構造がやや複雑に思えたので断念し、他に良いアイデアも思い浮かばなかったのでこの日は諦めた。

試行錯誤二日目

昨晩考えても答えが出なかったので、ちょっと一旦PCを離れて紙と鉛筆で考えてみることにした。

その結果、次の再帰に渡したいデータは、右のLeafの場合も左のLeafの場合も計算の小計だがBranchを処理する場合は未計算のツリーであることに気づいた。 そこで次の再帰に渡したいデータをstack: List[Either[B, Tree[A]]]に格納し、stackに格納されたデータ型によって関数が元のツリーの構造を判断するようにした。

それがこちらの実装。

def fold[A, B](tree: Tree[A])(f: A => B)(g: (B, B) => B): B = {
  @annotation.tailrec
  def go(tree: Tree[A], stack: List[Either[B, Tree[A]]])(f: A => B)(g: (B, B) => B): B = tree match {
    case Leaf(v) => stack match {
      case Cons(Right(next), rest) => go(next, Cons(Left(f(v)), rest))(f)(g) // 左枝の場合
      case Cons(Left(leftAcc), Cons(Right(next), rest)) => go(next, Cons(Left(g(leftAcc, f(v))), rest))(f)(g) // 右枝の場合
      case Cons(Left(leftAcc), Cons(Left(acc), _)) => g(acc, g(leftAcc, f(v))) // 右端のLeaf到達パターン1
      case Cons(Left(leftAcc), Nil) => g(leftAcc, f(v)) // 右端のLeaf到達パターン2
      case Nil => f(v) // この場合はLeafのみのTree
    }
    case Branch(l, r) => go(l, Cons(Right(r), stack))(f)(g)
  }
  go(tree, Nil)(f)(g)
}

自分はこれを書いた後、とても後悔することになった。 よく確かめもせず周囲に完成したと吹聴してしまったからだ。

これは全く正しく動作しない。

右側に偏った木をfoldしようとすると、未計算の木がstack奥深くに格納されてしまうのでパターンマッチで拾えず、未計算の木を残したまま計算が終了してしまうからだ。

これを解消するために、常に未計算の木が先頭に来るまで計算を進めることにした。

def fold[A, B](tree: Tree[A])(f: A => B)(g: (B, B) => B): B = {
  @annotation.tailrec
  def eval(stack: List[Either[B, Tree[A]]]): List[Either[B, Tree[A]]] = stack match {
    case Cons(Left(e1), Cons(Left(e2), rest))   => eval(Cons(Left(g(e2, e1)), rest))
    case Cons(Left(e1), Cons(r@Right(_), rest)) => Cons(r, Cons(Left(e1), rest)) // 呼出元で木を取り出しやすいよう入れ替え
    case _                                      => stack
  }
  @annotation.tailrec
  def go(tree: Tree[A], stack: List[Either[B, Tree[A]]]): B = tree match {
    case Leaf(v) =>
      // 計算済みの値がstackに連続して溜まると未計算の木が取り出せないので出来るだけ計算を進める
      eval(Cons(Left(f(v)), stack)) match {
        case Cons(Right(next), rest) => go(next, rest)
        case Cons(Left(e1), _)       => e1 // 最後まで評価が終わっているはず
        case Nil                     => f(v)
      }
    case Branch(l, r) => go(l, Cons(Right(r), stack))
  }
  go(tree, Nil)
}

これで、正しく動くようになった。(たぶん)

さてfoldさえできれば、mapを実装することは簡単だ。

def map[A, B](tree: Tree[A])(f: A => B): Tree[B] = {
  fold(tree)(v => Leaf(f(v)): Tree[B])(Branch(_, _))
}

パフォーマンスなどの議論は詳しくないから抜きにして、とりあえず二分木の畳み込みを末尾再帰化するという目的は達成できたように思える。

{1,2,3,5,7}みたいなリストを"1-3,5,7"に変換する

電車で移動中にタイムラインに流れて来てちょっと面白そうだということで実装してみた。

スマートに書けるかな、と思ったけどそんな事はなかった。

object ListConverter {
  /**
    * {1,2,3,5,7}みたいな配列(またはリスト)を“1-3,5,7”に変換する
    */
  def toSequenceString(list: List[Int]): String = {
    val sorted = list.sorted
    sorted.lastOption match {
      case None       => ""
      case Some(last) =>
        val (grouped, _) = sorted.init.foldRight((List(List(last)), last)) {
          case (cr, (h :: t, prv)) if prv - 1 == cr => ((cr :: h) :: t, cr)
          case (cr, (acc, _))                       => (List(cr) :: acc, cr)
        }
        grouped.map {
          case Nil      => ""
          case h :: Nil => h.toString
          case l        => s"${l.head}-${l.last}"
        }.mkString(",")
    }
  }
  /**
    * “1-3,5,7”をList(1,2,3,5,7)に変換する(toSequenceStringの逆変換)
    */
  def sequenceStringToList(str: String): Either[Throwable, List[Int]] = {
    def map2[E, A, B, C](e1: Either[E, A], e2: Either[E, B])(f: (A, B) => C): Either[E, C] = for {
      a <- e1
      b <- e2
    } yield f(a, b)

    str.split(",").map { s =>
      s.split("-") match {
        case Array(from, to) => Try((from.toInt to to.toInt).toList).toEither
        case Array(numStr)   => Try(List(numStr.toInt)).toEither
        case _               => Left(new IllegalArgumentException(s"パースできない文字列: $str"))
      }
    }.foldRight[Either[Throwable, List[Int]]](Right(List()))(map2(_, _)(_ ++ _))
  }
}

良いやり方を思い付いたら、また公開する。