プログラミング言語Flixに入門しようとしたけど敗北した
概要
この記事は株式会社アットウェアのアドベントカレンダー10日目の記事です(間に合いませんでした...)
朝、log4jの話題で盛り上がるまでは仕事SlackでFlixが話題になってて、今日昨日のアドベントカレンダーのネタにも困ってたので、簡単なコードを書いて記事にしようと考えました。
しかし、if文を書くにも色々ハマってしまい、結局Hello Worldくらいしかできませんでした、という記事です。
Flixとは
主にオーフス大学、ウォータールー大学によって開発されている、関数型・論理型プログラミング言語らしいです。 ぱっと見はScalaからオブジェクト指向成分を抜いた感じに見えます。1
以下の公式サイトをみれば、雰囲気だけは何となくわかるかもしれません。 https://flix.dev/
環境構築
環境構築はVSCodeのflixプラグインを使って行うのが簡単です。 以下の通りに行ってください。
- flixプラグインをVSCodeにインストールします
- サンプルプロジェクト用のフォルダを作ってVSCodeで開きます
- コマンドパレットから
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とかOCamlのlet ... 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日目に遅刻して、本当に申し訳ありませんでした!
- 次回までにもう少しドキュメントを読んでリベンジします。
続・JShellで外部ライブラリを追加しデバッグする方法
概要
JShellで外部ライブラリを追加しデバッグする方法の続きです。
前回の投稿では、以下の手順でApache Mavenを使ってデバッグに必要なライブラリをJShellに追加する方法を書きました。
- 使いたいライブラリを記載したpom.xmlを作成する
- 依存ライブラリをダウンロードし、クラスパスを出力する
- jshellに2で出力したクラスパスを食わせる
しかしサードパーティのMavenプラグインを用いれば、2, 3の手順をまとめて実行できることがわかったので、今回はそのやり方を書きます。
Mavenプラグインを使ってライブラリをJShellに追加する方法
手順は以下のステップで行います。
- 使いたいライブラリを記載したpom.xmlを作成する
- 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に渡して起動する方法を調べました。
手順
手順は以下のステップで行います。
- 使いたいライブラリを記載したpom.xmlを作成する
- 依存ライブラリをダウンロードし、クラスパスを出力する
- 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を使って対話的にデバッグできると便利なのではないかと思います。
よりスマート方法があれば、是非コメントで教えてください。
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
みたいな感じでやりたいです。
まあ、今日は時間もないし、とりあえず良い案でるまで開発進めながら考えようという感じです。
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
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プラグインを作れそうです。
まとめ
現場からは以上です。