少年易酔學難成

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

プログラミング言語Flixに入門しようとしたけど敗北した

概要

この記事は株式会社アットウェアのアドベントカレンダー10日目の記事です(間に合いませんでした...)

朝、log4jの話題で盛り上がるまでは仕事SlackでFlixが話題になってて、今日昨日のアドベントカレンダーのネタにも困ってたので、簡単なコードを書いて記事にしようと考えました。 しかし、if文を書くにも色々ハマってしまい、結局Hello Worldくらいしかできませんでした、という記事です。

Flixとは

主にオーフス大学、ウォータールー大学によって開発されている、関数型・論理型プログラミング言語らしいです。 ぱっと見はScalaからオブジェクト指向成分を抜いた感じに見えます。1

以下の公式サイトをみれば、雰囲気だけは何となくわかるかもしれません。 https://flix.dev/

環境構築

環境構築はVSCodeflixプラグインを使って行うのが簡単です。 以下の通りに行ってください。

  1. flixプラグインVSCodeにインストールします
  2. サンプルプロジェクト用のフォルダを作ってVSCodeで開きます
  3. コマンドパレットからFlix Package Manager: initを実行します。

すると、2で作ったフォルダにプロジェクトの雛形が展開されます。 src/Main.flixには既にHello Worldするコードが生成されていますね。

// The main entry point.
def main(_args: Array[String]): Int32 & Impure =
  Console.printLine("Hello World!");
  0 // exit code

main関数の戻り値がInt32 & Impureとなっています。 なぜこうした型になるのかはわかりませんが、副作用が型に現れているという理解で良いんでしょうか。 ここらへん時間なくてドキュメントを全く読めてないので、後日リベンジまでに読み込んでおきたいところです。

このコードはコマンドパレットでFlix Package Manager: runを選択すれば実行できます。

$ java -jar '/Users/tsatow/Library/Application Support/Code/User/globalStorage/flix.flix/flix.jar' run 
Hello World!                                                                    
Main exited with status code 0.

↑を見ていて気づくと思いますが、Flix Package Manager: runは単にflix.jarを呼び出しているだけです。

これがどんな機能をもっているかは公式ドキュメントに書いてあります。
このjarは公式サイトにもリンクがなくどこからダウンロードすればいいのか良くわからなかったですが、とりあえずリポジトリをcloneしてgradleでビルドしてやれば手に入ります。 初回のビルドは自分のPCで約10分程度でした。

簡単なプログラムを書く

さて、さすがにHello Worldしただけで1記事書くのは厳しいので、もう少しだけ複雑なプログラムを書きます。 ちょうど詳解Rustプログラミングを読んでいたので、その中に出てきたgrep-liteをFlixで実装しようと思います。

grep-liteは、grep-lite <pattern> <file>という形式で呼び出し、inputで指定されたファイルを読み込んで正規表現でpatternにマッチした行を表示するものとします。

とりあえず、patternとファイルが指定されなかった時は標準エラー出力に使い方を表示するところまで書いてみます。

def main(args: Array[String]): Int32 & Impure = {
  if (args.length != 2) {
    Console/StdErr.printLine("USAGE:\n    grep-lite <pattern> <input>\nARGS:    <pattern>    The pattern to search for\n    <input>      File to search");
    1
  } else {
    Console.printLine("");
    0
  }
}

標準エラー出力にはちょっと手こずりました。
Console.StdErrを使うべきだということはドキュメントからすぐわかったのですが、Console.StdErr.printLine("...");と呼び出すものと思っていて、なかなかエラーを解消できず苦労しました。2 改めてConsoleを打ち直して注意深くVSCodeのサジェストを観察して、ようやくConsole/StdErr.printLine("...");と書くべきだったことに気づきました。 こういったことも現時点ではまだドキュメント化されていません。(多分)

あと、if式も書き方がわからなくて、↓のflixリポジトリのコードをみたんですけど、複数式でも{...}が無いように見えたので、オフサイドルールでもあるのかなと思ってしまいました。 でもこれ多分、HaskellとかOCamllet ... inみたいなやつで一つの式なんですかね。手元で書く限りは、Flixでは複数の式を書くときは{...}で囲む必要がありそうでした。

        else if (nonValue(e1))
            // An application where the first argument is *NOT* a value.
            // Reduction should continue in the first argument.
            let (rdx, ec) = redex(e1);
                (rdx, EApp1(ec, e2))

さて、次はファイルを読み込んで1行ずつ正規表現でマッチさせて...と考えて標準ライブラリのドキュメントを読んでいたら、正規表現用のライブラリがまだ実装されていないことに気が付きました。 さすがにどこかで使ってるだろうとソースコードを検索したら、どうやらJavaのライブラリを使っているようです。

ならばと、ここの書きっぷりを真似てみると...

def main(args: Array[String]): Int32 & Impure = {
  import static java.util.regex.Pattern.compile(String); // Error!
  if (args.length != 2) {
    Console/StdErr.printLine("USAGE:\n    grep-lite <pattern> [input]\nARGS:    <pattern>    The pattern to search for\n    <input>      File to search");
    1
  } else {
    Console.printLine("");
    0
  }
}

なぜかエラーが出ます。

Invalid input "static j", expected '=', Constructor, Method, StaticMethod, GetField, PutField, GetStaticField, PutStaticField, ':', "->", '[', '.', "::" or ":::" (line 2, column 10):
  import static java.util.regex.Pattern.compile(String);
         ^

今日はここまで来たところで心が折れたので撤退することにしました。

一応、String.flixには↓のような実装のisMatch関数もあるんですけど、この用途で使うのはちょっと気が引けました。

    pub def isMatch(regex: {regex :: String}, s: String): Bool =
        try {
            import java.lang.String.matches(String);
            matches(s, regex.regex) as & Pure
        } catch {
            case _: ##java.util.regex.PatternSyntaxException => false
        }

感想

  • まだ使ってる人もドキュメントも少ない新しい言語の入門はなかなかハードでした
  • Scalaに似てるから余裕だろうと舐めてギリギリにブログ書き始めた挙げ句アドベントカレンダーの10日目に遅刻して、本当に申し訳ありませんでした!
  • 次回までにもう少しドキュメントを読んでリベンジします。

  1. 実際、この記事のFlixのコードはScalaシンタックスハイライトを入れていますが、それなりに効いています。

  2. .を入力しても補完が出なくてバグを疑ってしまいました

続・JShellで外部ライブラリを追加しデバッグする方法

概要

JShellで外部ライブラリを追加しデバッグする方法の続きです。

前回の投稿では、以下の手順でApache Mavenを使ってデバッグに必要なライブラリをJShellに追加する方法を書きました。

  1. 使いたいライブラリを記載したpom.xmlを作成する
  2. 依存ライブラリをダウンロードし、クラスパスを出力する
  3. jshellに2で出力したクラスパスを食わせる

しかしサードパーティMavenプラグインを用いれば、2, 3の手順をまとめて実行できることがわかったので、今回はそのやり方を書きます。

Mavenプラグインを使ってライブラリをJShellに追加する方法

手順は以下のステップで行います。

  1. 使いたいライブラリを記載したpom.xmlを作成する
  2. jshell-maven-pluginを使ってJShellを起動する

前回と比較して、クラスパスをファイルに出力する必要がなくなり、ワンステップ短くなりました。

順番に見ていきましょう。

1. 使いたいライブラリを記載したpom.xmlを作成する

こちらは前回と同じなので特にいうことはありません。 必要なライブラリをdependenciesに追加してください。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>example</groupId>
  <artifactId>miniodebug</artifactId>
  <version>1.0</version>
 
  <dependencies>
    <dependency>
      <groupId>io.minio</groupId>
      <artifactId>minio</artifactId>
      <version>8.2.1</version>
    </dependency>
  </dependencies>
</project>

2. jshell-maven-pluginを使ってJShellを起動する

mvn compile com.github.johnpoth:jshell-maven-plugin:1.3:runと打てば、プラグイン及び依存ライブラリのダウンロードが始まり、その後にJShellが起動します。

$ mvn compile com.github.johnpoth:jshell-maven-plugin:1.3:run
[INFO] Scanning for projects...
Downloading from central: https://repo.maven.apache.org/maven2/com/github/johnpoth/jshell-maven-plugin/1.3/jshell-maven-plugin-1.3.pom
Downloaded from central: https://repo.maven.apache.org/maven2/com/github/johnpoth/jshell-maven-plugin/1.3/jshell-maven-plugin-1.3.pom (4.7 kB at 4.1 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/com/github/johnpoth/jshell-maven-plugin/1.3/jshell-maven-plugin-1.3.jar
Downloaded from central: https://repo.maven.apache.org/maven2/com/github/johnpoth/jshell-maven-plugin/1.3/jshell-maven-plugin-1.3.jar (9.9 kB at 21 kB/s)
[INFO]
[INFO] -------------------------< example:miniodebug >-------------------------
[INFO] Building miniodebug 1.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ miniodebug ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /Users/tsatow/works/miniodebug/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ miniodebug ---
[INFO] No sources to compile
[INFO]
[INFO] --- jshell-maven-plugin:1.3:run (default-cli) @ miniodebug ---
...
Downloaded from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-compiler-javac/2.8.2/plexus-compiler-javac-2.8.2.jar (20 kB at 17 kB/s)
Downloaded from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-utils/3.1.0/plexus-utils-3.1.0.jar (262 kB at 187 kB/s)
[WARNING] Removing: /Users/tsatow/works/miniodebug/target/classes from the classpath.
If this is unexpected, please make sure you correctly build the project beforehand by invoking the correct Maven build phase (usually `install`, `test-compile` or `compile`). For example:
mvn test-compile com.github.johnpoth:jshell-maven-plugin:1.3:run
For more information visit https://github.com/johnpoth/jshell-maven-plugin
|  JShellへようこそ -- バージョン11.0.6
|  概要については、次を入力してください: /help intro

jshell> import io.minio.MinioClient

jshell> var minioClient = MinioClient.builder().endpoint("http://xxxxx:9000").credentials("user", "pass").build()
minioClient ==> io.minio.MinioClient@51399530

これで少しだけ手順が簡単になりました。

まとめ

前回に続き、JShellで外部ライブラリを追加しデバッグする方法について書きました。 jshell-maven-pluginを使うことによって、前回より1ステップ手順を減らすことができました。 こういうのはプラグイン化されているものですね。なぜ最初にプラグインを探さなかったのかと反省しています。

JShellでライブラリを追加しデバッグする

概要

Javaアプリケーションで問題が発生したときに、状況を再現できる最小のライブラリを使って対話的にデバッグしたくなることがあります。

JShellでは、jshell --class-path <<クラスパス>>でクラスパスを指定することで外部ライブラリを使用できます。が、使いたいライブラリが1つであっても、大抵はそのライブラリの動作させるためにさらに多くのライブラリが必要になります。

ライブラリの依存関係を調べて必要なライブラリを手で全てダウンロードするには人生は短すぎるので、これらの作業はビルドツールを使用した方が良さそうです。

今回はApache Mavenを使ってライブラリをダウンロードし、必要なクラスパスをJShellに渡して起動する方法を調べました。

手順

手順は以下のステップで行います。

  1. 使いたいライブラリを記載したpom.xmlを作成する
  2. 依存ライブラリをダウンロードし、クラスパスを出力する
  3. jshellに2で出力したクラスパスを食わせる

順番に見ていきます。

1. 使いたいライブラリを記載したpom.xmlを作成する

今回は例として、JavaのMinIOクライアントの動作を確認するために以下のようなpom.xmlを作成しました。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>example</groupId>
  <artifactId>miniodebug</artifactId>
  <version>1.0</version>
 
  <dependencies>
    <dependency>
      <groupId>io.minio</groupId>
      <artifactId>minio</artifactId>
      <version>8.2.1</version>
    </dependency>
  </dependencies>
</project>

2. 依存ライブラリをダウンロードし、クラスパスを出力する

依存関係のダウンロードとクラスパスの出力はApache Maven Dependency Pluginを使います。 このプラグインにはbuild-classpathというゴールを設定でき、これはまさにここでやりたいことをやってくれます。 以下のコマンドでdep.txtにクラスパスを出力します。

$ mvn dependency:build-classpath -DincludeTypes=jar -Dmdep.outputFile=dep.txt

dep.txtを確認してみます。

$ cat dep.txt
/Users/tsatow/.m2/repository/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.70/kotlin-stdlib-common-1.3.70.jar:/Users/tsatow/.m2/repository/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar:/Users/tsatow/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.11.2/jackson-annotations-2.11.2.jar:/Users/tsatow/.m2/repository/com/google/guava/guava/29.0-jre/guava-29.0-jre.jar:/Users/tsatow/.m2/repository/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar:/Users/tsatow/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.11.2/jackson-databind-2.11.2.jar:/Users/tsatow/.m2/repository/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar:/Users/tsatow/.m2/repository/com/carrotsearch/thirdparty/simple-xml-safe/2.7.1/simple-xml-safe-2.7.1.jar:/Users/tsatow/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.11.2/jackson-core-2.11.2.jar:/Users/tsatow/.m2/repository/com/google/errorprone/error_prone_annotations/2.3.4/error_prone_annotations-2.3.4.jar:/Users/tsatow/.m2/repository/io/minio/minio/8.2.1/minio-8.2.1.jar:/Users/tsatow/.m2/repository/org/checkerframework/checker-qual/2.11.1/checker-qual-2.11.1.jar:/Users/tsatow/.m2/repository/com/squareup/okhttp3/okhttp/4.8.1/okhttp-4.8.1.jar:/Users/tsatow/.m2/repository/org/jetbrains/kotlin/kotlin-stdlib/1.3.72/kotlin-stdlib-1.3.72.jar:/Users/tsatow/.m2/repository/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar:/Users/tsatow/.m2/repository/com/squareup/okio/okio/2.7.0/okio-2.7.0.jar:/Users/tsatow/.m2/repository/org/jetbrains/annotations/13.0/annotations-13.0.jar

3. JShellに2で出力したクラスパスを食わせる

あとは先程出力したdep.txtを使ってJShellにクラスパスを食わせるだけです。

$ jshell --class-path `cat dep.txt`
|  JShellへようこそ -- バージョン11.0.6
|  概要については、次を入力してください: /help intro

jshell> import io.minio.MinioClient

jshell> var minioClient = MinioClient.builder().endpoint("http://xxxxx:9000").credentials("user", "pass").build()
    minioClient ==> io.minio.MinioClient@44c03695
...

ここまで来たら、あとは色々試しながらデバッグすることができます。

応用

コンテナ環境で使う

この方法は自分のPCで試すのにも便利ですが、例えばKubernetesクラスタ内で作業を行いたい場合にも便利です。 殆ど手数が増えずにデバッグを開始できますし、試行錯誤の際にいちいちimageをbuildしてpushし直す必要もありません。

$ kubectl run minio-debug -it --rm --restart=Never --image=maven:3.8.3-jdk-11 --command -- bash
root@minio-debug:/# cat <<EOF > ./pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>example</groupId>
  <artifactId>miniodebug</artifactId>
  <version>1.0</version>
 
  <dependencies>
    <dependency>
      <groupId>io.minio</groupId>
      <artifactId>minio</artifactId>
      <version>8.2.1</version>
    </dependency>
  </dependencies>
</project>
EOF
root@minio-debug:/# mvn dependency:build-classpath -DincludeTypes=jar -Dmdep.outputFile=dep.txt
root@minio-debug:/# jshell --class-path `cat dep.txt`
|  Welcome to JShell -- Version 11.0.13
|  For on introduction type: /help intro
    
jshell> ...

実はこれがやりたかったので今回の方法を調べたのでした。

既存のMavenプロジェクトに適用する

今回はデバッグするのに必要な最小構成のMavenプロジェクトを新規に作成する方法を紹介しました。 しかし、既存のMavenプロジェクトを使って手順の2以降を実行すれば、プロジェクトの依存ライブラリ全てを使える状態でJShellを起動することができます。

Gradleでも同様のことを行う

最近のJavaプロジェクトでは、MavenよりもGradleが使われていることが多いように思います。 既存のプロジェクトへの適用を考えると、Gradleで同様のことを行う方法ができるといいのですが... 残念ながら私はGradleがよくわからないので1、誰か教えてください。

まとめ

今回はMavenでライブラリを追加しJShellでデバッグする方法方法について調べてみました。 問題が起きてる環境がローカルではなくステージングだった場合、デバッグコードをアプリに埋め込んだりデバッグ用のプロジェクトを立ち上げるのは結構大変です。 そういった場合に、問題を再現可能な最小構成でJShellを使って対話的にデバッグできると便利なのではないかと思います。

よりスマート方法があれば、是非コメントで教えてください。


  1. じゃあMavenはわかるのかというと、Mavenも同様にわかりません

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プラグインを作れそうです。

まとめ

現場からは以上です。