Kotlin の関数型プログラミング用ライブラリ Λrrow を試してみる
Kotlin で Scala の Scalaz や Cats のような関数型プログラミング用のライブラリを探していたところ、以下を見つけたので試してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20180822/
はじめに
Λrrow は以下のような要素で構成されており、Haskell の型クラス・インスタンスのような仕組みを実現しているようです。
- Datatypes (Option, Either, Try, Kleisli, StateT, WriterT 等)
- Typeclasses (Functor, Applicative, Monad, Monoid, Eq, Show 等)
- Instances (OptionMonadInstance 等)
そして、その実現にはアノテーションプロセッサが活用されているようです。(@higherkind
や @instance
から補助的なコードを生成)
Option のソースコード
例えば、Option
のソースコードは以下のようになっていますが、OptionOf
の定義は github のソース上には見当たりません。
arrow/core/Option.kt
@higherkind sealed class Option<out A> : OptionOf<A> { ・・・ }
OptionOf はアノテーションプロセッサ ※ で生成したコード(以下)で定義されています。(OptionOf<A>
は Kind<ForOption, A>
の型エイリアス)
higherkind.arrow.core.Option.kt
package arrow.core class ForOption private constructor() { companion object } typealias OptionOf<A> = arrow.Kind<ForOption, A> @Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") inline fun <A> OptionOf<A>.fix(): Option<A> = this as Option<A>
※ @higherkind に対する処理は arrow.higherkinds.HigherKindsFileGenerator に実装されているようです
ここで、ForOption
というクラスが定義されていますが、この For + 型名
はコンテナ型を表現するための型のようです。
ForOption がある事で Kind<Option<A>, A>
ではなく Kind<ForOption, A>
と定義できるようになっており、Functor<F>
や Monad<F>
における F の具体型として使われています。(例. Monad<ForOption>
)
Either のソースコード
次に Either
のソースコードも見てみます。
Either のように型パラメータが複数の場合はアノテーションプロセッサで生成されるコードの内容に多少の違いがあるようです。
arrow/core/Either.kt
@higherkind sealed class Either<out A, out B> : EitherOf<A, B> { ・・・ }
アノテーションプロセッサで生成されたコードは以下のように EitherOf
の他に EitherPartialOf
が型エイリアスとして定義されています。
higherkind.arrow.core.Either.kt
package arrow.core class ForEither private constructor() { companion object } typealias EitherOf<A, B> = arrow.Kind2<ForEither, A, B> typealias EitherPartialOf<A> = arrow.Kind<ForEither, A> @Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") inline fun <A, B> EitherOf<A, B>.fix(): Either<A, B> = this as Either<A, B>
また、上記で定義されている ForEither クラスとは別に arrow-instances-core
モジュールには ForEither 関数
も用意されています。
arrow/instances/either.kt
・・・ class EitherContext<L> : EitherMonadErrorInstance<L>, EitherTraverseInstance<L>, EitherSemigroupKInstance<L> { override fun <A, B> Kind<EitherPartialOf<L>, A>.map(f: (A) -> B): Either<L, B> = fix().map(f) } class EitherContextPartiallyApplied<L> { infix fun <A> extensions(f: EitherContext<L>.() -> A): A = f(EitherContext()) } fun <L> ForEither(): EitherContextPartiallyApplied<L> = EitherContextPartiallyApplied()
この ForEither 関数は extensions
を呼び出す際に使用する事になります。
サンプルコード
Option と Either をそれぞれ使ったサンプルを作成してみます。
(a) Option の利用
Option の Some
や None
の作成にはいくつかの方法が用意されています。
Monadbinding
を使うと Haskell の do 記法や Scala の for 式のようにモナドを処理できるようです。(Kotlin の coroutines 機能で実現)
binding の呼び出し方はいくつか考えられますが、ForOption extensions { ・・・ }
を使うのが基本のようです。
ForOption extensions へ渡す処理内では this が OptionContext
となるため、OptionContext の処理を呼び出せるようになっています。
また、binding へ渡す処理内では MonadContinuation<ForOption, *>
が this となります。
bind
は MonadContinuation 内で suspend fun <B> Kind<F, B>.bind(): B
と定義されており、Kind<F, B> から B の値を取り出す処理となっています。
src/main/kotlin/App.kt
import arrow.core.* import arrow.instances.* import arrow.typeclasses.binding fun main(args: Array<String>) { // Some val d1: Option<Int> = Option.just(10) val d2: Option<Int> = 5.some() val d3: Option<Int> = Some(2) // None val d4: Option<Int> = Option.empty() val d5: Option<Int> = none() val d6: Option<Int> = None // Some(15) val r1 = d1.flatMap { a -> d2.map { b -> a + b } } println(r1) // Some(17) val r2 = Option.monad().binding { // this: MonadContinuation<ForOption, *> val a = d1.bind() // 10 val b = d2.bind() // 5 val c = d3.bind() // 2 a + b + c } println(r2) // Some(17) val r3 = ForOption extensions { // this: OptionContext println(this) // arrow.instances.OptionContext@3ffc5af1 binding { // this: MonadContinuation<ForOption, *> println(this) // arrow.typeclasses.MonadContinuation@26653222 val a = d1.bind() val b = d2.bind() val c = d3.bind() a + b + c } } println(r3) // Some(17) val r4 = OptionContext.binding { val a = d1.bind() val b = d2.bind() val c = d3.bind() a + b + c } println(r4) // None val r5 = Option.monad().binding { val a = d1.bind() val b = d4.bind() a + b } println(r5) // None val r6 = ForOption extensions { binding { val a = d5.bind() val b = d2.bind() val c = d6.bind() a + b + c } } println(r6) }
Option のような基本的な型を使うだけであれば、依存ライブラリとして arrow-instances-core
を指定するだけで問題なさそうです。
そのため、Gradle ビルド定義ファイルは下記のような内容になります。
ここでは The feature "coroutines" is experimental (see: https://kotlinlang.org/docs/diagnostics/experimental-coroutines)
警告ログを出力しないように coroutines を有効化しています。
build.gradle (Gradle ビルド定義)
plugins { id 'org.jetbrains.kotlin.jvm' version '1.2.51' id 'application' } mainClassName = 'AppKt' // 実行するクラス名 repositories { jcenter() } dependencies { compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' compile 'io.arrow-kt:arrow-instances-core:0.7.3' } // coroutines の有効化(ビルド時の警告を抑制) kotlin { experimental { coroutines 'enable' } }
実行結果は以下の通りです。
実行結果
> gradle run ・・・ Some(15) Some(17) arrow.instances.OptionContext@3ffc5af1 arrow.typeclasses.MonadContinuation@26653222 Some(17) Some(17) None None
(b) Either の利用
Either の場合、extensions の呼び出し方が Option とは多少異なります。
Option の場合は ForOption の extensions を呼び出しましたが、Either の場合は ForEither に extensions は用意されておらず、代わりに EitherContextPartiallyApplied<L>
クラス内に定義されています。
EitherContextPartiallyAppliedForEither 関数
で取得できるので、これを使って ForEither<Left 側の型>() extensions { ・・・ }
のようにします。
ForEither<String>() extensions
へ渡す処理内の this は EitherContext<String>
で、binding へ渡す処理内の this は MonadContinuation<EitherPartialOf<String>, *>
となります。
src/main/kotlin/App.kt
import arrow.core.* import arrow.instances.* import arrow.typeclasses.binding fun main(args: Array<String>) { // Right val d1: Either<String, Int> = Either.right(10) val d2: Either<String, Int> = 5.right() val d3: Either<String, Int> = Right(2) // Left val d4: Either<String, Int> = Either.left("error data") // Right(b=15) val r1 = d1.flatMap { a -> d2.map { b -> a + b } } println(r1) // Right(b=17) val r2 = Either.monad<String>().binding { // this: MonadContinuation<EitherPartialOf<String>, *> val a = d1.bind() val b = d2.bind() val c = d3.bind() a + b + c } println(r2) // Right(b=17) // ForEither<String>() 関数の呼び出し val r3 = ForEither<String>() extensions { // this: EitherContext<String> binding { // this: MonadContinuation<EitherPartialOf<String>, *> val a = d1.bind() val b = d2.bind() val c = d3.bind() a + b + c } } println(r3) // Left(a=error data) val r4 = ForEither<String>() extensions { binding { val a = d1.bind() val b = d4.bind() val c = d3.bind() a + b + c } } println(r4) }
(a) と同じ内容の build.gradle を使って実行します。
実行結果
> gradle run ・・・ Right(b=15) Right(b=17) Right(b=17) Left(a=error data)
TypeScript で funfix を使用 - tsc, FuseBox
funfix は JavaScript, TypeScript, Flow の関数型プログラミング用ライブラリで、Fantasy Land や Static Land ※ に準拠し Scala の Option, Either, Try, Future 等と同等の型が用意されているようです。
※ JavaScript 用に Monoid や Monad 等の代数的構造に関する仕様を定義したもの
今回は Option を使った単純な処理を TypeScript で実装し Node.js で実行してみます。
ソースは http://github.com/fits/try_samples/tree/master/blog/20180730/
はじめに
Option
を使った下記サンプルをコンパイルして実行します。
サンプルソース
import { Option, Some } from 'funfix' const f = (ma, mb) => ma.flatMap(a => mb.map(b => `${a} + ${b} = ${a + b}`)) const d1 = Some(10) const d2 = Some(2) console.log( d1 ) console.log('-----') console.log( f(d1, d2) ) console.log( f(d1, Option.none()) ) console.log('-----') console.log( f(d1, d2).getOrElse('none') ) console.log( f(d1, Option.none()).getOrElse('none') )
ビルドと実行
上記ソースファイルを以下の 2通りでビルドして実行してみます。
- (a) tsc 利用
- (b) FuseBox 利用
(a) tsc を利用する場合
tsc コマンドを使って TypeScript のソースをコンパイルします。
まずは typescript と funfix モジュールをそれぞれインストールします。
typescript インストール
> npm install --save-dev typescript
funfix インストール
> npm install --save funfix
この状態で sample.ts
ファイルをコンパイルしてみると、型関係のエラーが出るものの sample.js
は正常に作られました。
コンパイル1
> tsc sample.ts node_modules/funfix-core/dist/disjunctions.d.ts:775:14 - error TS2416: Property 'value' in type 'TNone' is not assignable to the same property in base type 'Option<never>'. Type 'undefined' is not assignable to type 'never'. 775 readonly value: undefined; ~~~~~ node_modules/funfix-effect/dist/eval.d.ts:256:42 - error TS2304: Cannot find name 'Iterable'. 256 static sequence<A>(list: Eval<A>[] | Iterable<Eval<A>>): Eval<A[]>; ~~~~~~~~ ・・・
sample.js を実行してみると特に問題無く動作します。
実行1
> node sample.js TSome { _isEmpty: false, value: 10 } ----- TSome { _isEmpty: false, value: '10 + 2 = 12' } TNone { _isEmpty: true, value: undefined } ----- 10 + 2 = 12 none
これで一応は動いたわけですが、コンパイル時にエラーが出るというのも望ましい状態ではないので、エラー部分を解決してみます。
他にも方法があるかもしれませんが、ここでは以下のように対応します。
- (1)
Property 'value' in type 'TNone' ・・・ 'Option<never>'
のエラーに対して tsc 実行時に--strictNullChecks
オプションを指定して対応 - (2)
Cannot find name 'Iterable'
等のエラーに対して@types/node
をインストールして対応
strictNullChecks は tsc の実行時オプションで指定する以外にも設定ファイル tsconfig.json
で設定する事もできるので、ここでは tsconfig.json ファイルを使います。
(1) tsconfig.json
{ "compilerOptions": { "strictNullChecks": true } }
次に @types/node をインストールします。
@types/node には Node.js で実行するための型定義(Node.js 依存の API 等)が TypeScript 用に定義されています。
(2) @types/node インストール
> npm install --save-dev @types/node
この状態で tsc を実行すると先程のようなエラーは出なくなりました。(tsconfig.json を適用するため tsc コマンドへ引数を指定していない点に注意)
コンパイル2
> tsc
実行結果にも差異はありません。
実行2
> node sample.js TSome { _isEmpty: false, value: 10 } ----- TSome { _isEmpty: false, value: '10 + 2 = 12' } TNone { _isEmpty: true, value: undefined } ----- 10 + 2 = 12 none
最終的な package.json の内容は以下の通りです。
package.json
{ "name": "sample", "version": "1.0.0", "devDependencies": { "@types/node": "^10.5.4", "typescript": "^2.9.2" }, "dependencies": { "funfix": "^7.0.1" } }
(b) FuseBox を利用する場合
次に、モジュールバンドラーの FuseBox を使用してみます。(以降は (a) とは異なるディレクトリで実施)
なお、ここでは npm の代わりに yarn
を使っていますが、npm でも特に問題はありません。
yarn のインストール例(npm 使用)
> npm install -g yarn
(b-1) 型チェック無し
まずは typescript, fuse-box, funfix をそれぞれインストールしておきます。
typescript と fuse-box インストール
> yarn add --dev typescript fuse-box
funfix インストール
> yarn add funfix
FuseBox ではビルド定義を JavaScript のコードで記載します。 とりあえずは必要最小限の設定を行いました。
bundle で指定した名称が init の $name
に適用されるため、*.ts
のコンパイル結果と依存モジュールの内容をバンドルして bundle.js
へ出力する事になります。
なお、>
でロード時に実行する(コードを記載した)ファイルを指定します。
fuse.js (FuseBox ビルド定義)
const {FuseBox} = require('fuse-box') const fuse = FuseBox.init({ output: '$name.js' }) fuse.bundle('bundle').instructions('> *.ts') fuse.run()
上記スクリプトを実行してビルド(TypeScript のコンパイルとバンドル)を行います。
ビルド
> node fuse.js --- FuseBox 3.4.0 --- → Generating recommended tsconfig.json: ・・・\sample_fusebox1\tsconfig.json → Typescript script target: ES7 -------------------------- Bundle "bundle" sample.js └── (1 files, 700 Bytes) default └── funfix-core 34.4 kB (1 files) └── funfix-effect 43.1 kB (1 files) └── funfix-exec 79.5 kB (1 files) └── funfix 1 kB (1 files) size: 158.7 kB in 765ms
初回実行時にデフォルト設定の tsconfig.json が作られました。(tsconfig.json が存在しない場合)
tsc の時のような型関係のエラーは出ていませんが、これは FuseBox がデフォルトで TypeScript の型チェックをしていない事が原因のようです。
型チェックを実施するには fuse-box-typechecker
プラグインを使う必要がありそうです。
実行
> node bundle.js TSome { _isEmpty: false, value: 10 } ----- TSome { _isEmpty: false, value: '10 + 2 = 12' } TNone { _isEmpty: true, value: undefined } ----- 10 + 2 = 12 none
package.json の内容は以下の通りです。
package.json
{ "name": "sample_fusebox1", "version": "1.0.0", "main": "bundle.js", "license": "MIT", "devDependencies": { "fuse-box": "^3.4.0", "typescript": "^2.9.2" }, "dependencies": { "funfix": "^7.0.1" } }
(b-2) 型チェック有り
TypeScript の型チェックを行うようにしてみます。
まずは、(b-1) と同じ構成に fuse-box-typechecker
プラグインを加えます。
fuse-box-typechecker を追加インストール
> yarn add --dev fuse-box-typechecker
次に、fuse.js へ fuse-box-typechecker プラグインの設定を追加します。
TypeChecker で型チェックにエラーがあった場合、例外が throw されるようにはなっていないため、ここではエラーがあった場合に Error を throw して fuse.run() を実行しないようにしてみました。
ただし、こうすると tsconfig.json を予め用意しておく必要があります。(TypeChecker に tsconfig.json が必要)
fuse.js (FuseBox ビルド定義)
const {FuseBox} = require('fuse-box') const {TypeChecker} = require('fuse-box-typechecker') // fuse-box-typechecker の設定 const testSync = TypeChecker({ tsConfig: './tsconfig.json' }) const fuse = FuseBox.init({ output: '$name.js' }) fuse.bundle('bundle').instructions('> *.ts') testSync.runPromise() .then(n => { if (n != 0) { // 型チェックでエラーがあった場合 throw new Error(n) } // 型チェックでエラーがなかった場合 return fuse.run() }) .catch(console.error)
これで、ビルド時に (a) と同様の型エラーが出るようになりました。
ビルド1
> node fuse.js ・・・ --- FuseBox 3.4.0 --- Typechecker plugin(promisesync) . Time:Sun Jul 29 2018 12:40:47 GMT+0900 (GMT+09:00) File errors: └── .\node_modules\funfix-core\dist\disjunctions.d.ts | ・・・\sample_fusebox2\node_modules\funfix-core\dist\disjunctions.d.ts (775,14) (Error:TS2416) Property 'value' in type 'TNone' is not assignable to the same property in base type 'Option<never>'. Type 'undefined' is not assignable to type 'never'. Errors:1 └── Options: 0 └── Global: 0 └── Syntactic: 0 └── Semantic: 1 └── TsLint: 0 Typechecking time: 4116ms Quitting typechecker ・・・
ここで、Iterable
の型エラーが出ていないのは fuse-box-typechecker のインストール時に @types/node もインストールされているためです。
(a) と同様に strictNullChecks の設定を tsconfig.json へ追記して、このエラーを解決します。
tsconfig.json へ strictNullChecks の設定を追加
{ "compilerOptions": { "module": "commonjs", "target": "ES7", ・・・ "strictNullChecks": true } }
これでビルドが成功するようになりました。
ビルド2
> node fuse.js ・・・ Typechecker name: undefined Typechecker basepath: ・・・\sample_fusebox2 Typechecker tsconfig: ・・・\sample_fusebox2\tsconfig.json --- FuseBox 3.4.0 --- Typechecker plugin(promisesync) . Time:Sun Jul 29 2018 12:44:57 GMT+0900 (GMT+09:00) All good, no errors :-) Typechecking time: 4103ms Quitting typechecker killing worker → Typescript config file: \tsconfig.json → Typescript script target: ES7 -------------------------- Bundle "bundle" sample.js └── (1 files, 700 Bytes) default └── funfix-core 34.4 kB (1 files) └── funfix-effect 43.1 kB (1 files) └── funfix-exec 79.5 kB (1 files) └── funfix 1 kB (1 files) size: 158.7 kB in 664ms
実行結果
> node bundle.js TSome { _isEmpty: false, value: 10 } ----- TSome { _isEmpty: false, value: '10 + 2 = 12' } TNone { _isEmpty: true, value: undefined } ----- 10 + 2 = 12 none
package.json の内容は以下の通りです。
package.json
{ "name": "sample_fusebox2", "version": "1.0.0", "main": "index.js", "license": "MIT", "devDependencies": { "fuse-box": "^3.4.0", "fuse-box-typechecker": "^2.10.0", "typescript": "^2.9.2" }, "dependencies": { "funfix": "^7.0.1" } }
FunctionalJava の DB モナド?
FunctionalJava における fj.control.db.DB
クラスの使い方を調べてみました。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20151013/
はじめに
処理内容を見る限り fj.control.db.DB
は Reader モナドをベースにしていますが、以下の点が異なります。
- 適用する状態が
java.sql.Connection
に固定されている
また、実行処理の run
メソッドが SQLException を throws するようになっています。
使い方は、概ね以下のようになると思います。
fj.control.db.DB 使用例
// 検索の場合は DbState.reader メソッドを使用 DbState dbWriter = DbState.writer("<DB接続URL>"); // DB 処理を構築 DB<?> q = DB.db(con -> ・・・)・・・; // 実行 dbWriter.run(q);
DbState
の run メソッド内で java.sql.Connection
を取得し、fj.control.db.DB
の run メソッドを実行します。
ただし、以下のような注意点があり、実際のところ既存のメソッドだけで処理を組み立てるのは難しいように思います。
- (a)
DB.db(F<java.sql.Connection,A> f)
メソッドでは SQLException を throw する処理から DB オブジェクトを作成できない - (b)
DbState
のrun
メソッドは SQLException を throw した場合のみロールバックする (他の例外ではロールバックしない) - (c) リソース(PreparedStatement や ResultSet 等)の解放
close
は基本的に自前で実施する必要あり ※
※ DbState の run メソッドを使えば、Connection の close はしてくれます
例えば、prepareStatement
メソッドは SQLException を throw するため、DB.db(con -> con.prepareStatement("select * from ・・・"))
のようにするとコンパイルエラーになります。
サンプル1
(a) と (c) に対処するためのヘルパーメソッド(以下)を用意して、サンプルコードを書いてみました。
DB<A> db(Try1<Connection, A, SQLException> func)
メソッドを定義- 更新・検索処理を実施する
DB
オブジェクトの生成メソッドcommand
とquery
をそれぞれ定義 (try-with-resources 文で PreparedStatement 等を close)
動作確認のため、以下のような更新処理を行う DB
オブジェクトを組み立てました。
- (1) product テーブルへ insert
- (2) (1) で自動採番された id を取得 (OptionalInt へ設定)
- (3) OptionalInt から id の値(数値)を取り出し
- (4) (3) の値を使って product_variation テーブルへ 2レコードを insert
なお、(b) によって SQLException 以外ではロールバックしないため、仮に (3) で NoSuchElementException が throw された場合にロールバックされません。
src/main/java/sample/DbSample1.java
package sample; import fj.Unit; import fj.control.db.DB; import fj.control.db.DbState; import fj.function.Try1; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.OptionalInt; public class DbSample1 { public static void main(String... args) throws Exception { DbState dbWriter = DbState.writer(args[0]); final String insertVariationSql = "insert into product_variation (product_id, color, size) " + "values (?, ?, ?)"; // 更新処理の組み立て DB<Integer> q1 = command("insert into product (name, price) values (?, ?)", "sample1", 1500) // (1) .bind(v -> query("select last_insert_id()", DbSample1::scalarValue)) // (2) .map(OptionalInt::getAsInt) // (3) .bind(id -> command(insertVariationSql, id, "Black", "L") .bind(_v -> command(insertVariationSql, id, "White", "S"))); // 更新処理の実行 System.out.println(dbWriter.run(q1)); DbState dbReader = DbState.reader(args[0]); final String selectSql = "select name, color, size from product p " + "join product_variation v on v.product_id = p.id"; // 検索処理の組み立て DB<Unit> q2 = query(selectSql, rs -> { while (rs.next()) { System.out.println(rs.getString("name") + ", " + rs.getString("color") + ", " + rs.getString("size")); } return Unit.unit(); }); // 検索処理の実行 dbReader.run(q2); } private static OptionalInt scalarValue(ResultSet rs) throws SQLException { return rs.next() ? OptionalInt.of(rs.getInt(1)) : OptionalInt.empty(); } // DB.db の代用メソッド private static <A> DB<A> db(Try1<Connection, A, SQLException> func) { return new DB<A>() { public A run(Connection con) throws SQLException { return func.f(con); } }; } // 更新用 private static DB<Integer> command(String sql, Object... params) { return db(con -> { try (PreparedStatement ps = createStatement(con, sql, params)) { return ps.executeUpdate(); } }); } // 検索用 private static <T> DB<T> query(String sql, Try1<ResultSet, T, SQLException> handler, Object... params) { return db(con -> { try ( PreparedStatement ps = createStatement(con, sql, params); ResultSet rs = ps.executeQuery() ) { return handler.f(rs); } }); } private static PreparedStatement createStatement(Connection con, String sql, Object... params) throws SQLException { PreparedStatement ps = con.prepareStatement(sql); for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } return ps; } }
実行
Gradle で実行しました。
build.gradle
apply plugin: 'application' mainClassName = 'sample.DbSample1' repositories { jcenter() } dependencies { compile 'org.functionaljava:functionaljava:4.4' runtime 'mysql:mysql-connector-java:5.1.36' } run { if (project.hasProperty('args')) { args project.args } }
実行結果
> gradle run -Pargs="jdbc:mysql://localhost:3306/sample1?user=root" ・・・ 1 sample1, Black, L sample1, White, S
サンプル2
(b) への対策として、SQLException 以外を throw してもロールバックする run
メソッドを用意しました。
src/main/java/sample/DbSample2.java
package sample; ・・・ import fj.function.TryEffect1; ・・・ public class DbSample2 { public static void main(String... args) throws Exception { Connector connector = DbState.driverManager(args[0]); final String insertVariationSql = "insert into product_variation (product_id, color, size) " + "values (?, ?, ?)"; DB<Integer> q1 = command("insert into product (name, price) values (?, ?)", "sample2", 2000) .bind(v -> query("select last_insert_id()", DbSample2::scalarValue)) .map(OptionalInt::getAsInt) .bind(id -> command(insertVariationSql, id, "Green", "L") .bind(_v -> command(insertVariationSql, id, "Blue", "S"))); // 更新処理の実行 System.out.println(run(connector, q1)); final String selectSql = "select name, color, size from product p " + "join product_variation v on v.product_id = p.id"; DB<Unit> q2 = query(selectSql, rs -> { while (rs.next()) { System.out.println(rs.getString("name") + ", " + rs.getString("color") + ", " + rs.getString("size")); } return Unit.unit(); }); // 検索処理の実行 runReadOnly(connector, q2); } // 検索用の実行処理(常にロールバック) private static <A> A runReadOnly(Connector connector, DB<A> dba) throws SQLException { return run(connector, dba, Connection::rollback); } // 更新用の実行処理 private static <A> A run(Connector connector, DB<A> dba) throws SQLException { return run(connector, dba, Connection::commit); } private static <A> A run(Connector connector, DB<A> dba, TryEffect1<Connection, SQLException> trans) throws SQLException { try (Connection con = connector.connect()) { con.setAutoCommit(false); try { A result = dba.run(con); trans.f(con); return result; } catch (Throwable e) { con.rollback(); throw e; } } } ・・・ }
実行
build.gradle
・・・
mainClassName = 'sample.DbSample2'
・・・
実行結果
> gradle run -Pargs="jdbc:mysql://localhost:3306/sample2?user=root" ・・・ 1 sample2, Green, L sample2, Blue, S
Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現3
Java のアノテーションプロセッサを使って下記と同等の機能を実現する試みの第三弾です。
前回 のものを改良し、ようやく下記のような構文を実現しました。
Optional<String> res = opt$do -> { let a = Optional.of("a"); let b = Optional.of("b"); let c = opt$do -> { let c1 = Optional.of("c1"); let c2 = Optional.of("c2"); return c1 + "-" + c2; }; return a + b + "/" + c; };
ソースは http://github.com/fits/try_samples/tree/master/blog/20150516/
はじめに
環境
下記のような環境を使ってビルド・実行しています。
- JavaSE Development Kit 8u45 (1.8.0_45)
- Gradle 2.4
構文
前回 からの変更点は以下の通りです。
- 対象のラムダ式(
JCLambda
)を完全に置換し、Supplier を不要にした - let で入れ子に対応
対象のラムダ式を別の式 (JCMethodInvocation
) で完全に置換し、Supplier
を無くした事でまともな構文になったと思います。
変換前 (アノテーションプロセッサ処理前)
Optional<String> res = opt$do -> { let a = Optional.of("a"); let b = Optional.of("b"); let c = opt$do -> { let c1 = Optional.of("c1"); let c2 = Optional.of("c2"); return c1 + "-" + c2; }; return a + b + "/" + c; };
変換後 (アノテーションプロセッサ処理後)
Optional<String> res = opt.bind( Optional.of("a"), (a) -> opt.bind( Optional.of("b"), (b) -> opt.bind( opt.bind( Optional.of("c1"), (c1) -> opt.bind( Optional.of("c2"), (c2) -> opt.unit(c1 + "-" + c2) ) ), (c) -> opt2.unit(a + b + "/" + c) ) ) );
また、変数への代入だけではなく、メソッドの引数にも上記構文を使えるようにしました。
メソッド引数としての使用例
System.out.println(opt$do -> { let a = Optional.of("a"); let b = Optional.of("b"); return "***" + b + a; });
アノテーションプロセッサの実装
Processor の実装
前回 とほぼ同じですが、DoExprVisitor
の extends 元を com.sun.tools.javac.tree.TreeScanner
へ変えたので、accept
メソッドの呼び出し箇所が多少変わっています。
なお、JCTree
へキャストしていますが、JCCompilationUnit
へキャストしても問題ありません。
src/main/java/sample/DoExprProcessor.java
package sample; import java.util.Set; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.util.Trees; import com.sun.source.util.TreePath; import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.util.Context; @SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedAnnotationTypes("*") public class DoExprProcessor extends AbstractProcessor { private Trees trees; private Context context; @Override public void init(ProcessingEnvironment procEnv) { trees = Trees.instance(procEnv); context = ((JavacProcessingEnvironment)procEnv).getContext(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { roundEnv.getRootElements().stream().map(this::toUnit).forEach(this::processUnit); return false; } private CompilationUnitTree toUnit(Element el) { TreePath path = trees.getPath(el); return path.getCompilationUnit(); } private void processUnit(CompilationUnitTree cu) { if (cu instanceof JCTree) { ((JCTree)cu).accept(new DoExprVisitor(context)); // 変換内容を出力 System.out.println(cu); } } }
TreeVisitor の実装
前回 からの変更点は以下の通りです。
- (a) コード生成部分を別クラス化
- (b) 対象のラムダ式 (
JCLambda
) を全置換 - (c) メソッド引数への対応
- (d) extends 元を
com.sun.tools.javac.tree.TreeScanner
へ変更 (前回まではcom.sun.source.util.TreeScanner
)
(b) を実現するため changeNode
へ置換処理 (JCLambda
を JCMethodInvocation
へ差し替える事になる) を設定するようにしました。
主な処理内容は次のようになっています。
- (1) 変数定義(
JCVariableDecl
)やメソッド実行(JCMethodInvocation
)の箇所で該当部分を差し替えるための処理をchangeNode
へ設定 - (2) ラムダの内容からソースコードを生成 (対象外なら何もしない)
- (3) ソースコードを
JCExpression
へパースして (実体はJCMethodInvocation
)pos
の値を調整 - (4)
changeNode
を実行しラムダ箇所を差し替え
src/main/java/sample/DoExprVisitor.java
package sample; import com.sun.tools.javac.parser.ParserFactory; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.*; import com.sun.tools.javac.tree.TreeScanner; import com.sun.tools.javac.util.Context; import java.util.function.BiConsumer; import java.util.stream.Stream; public class DoExprVisitor extends TreeScanner { private ParserFactory parserFactory; private BiConsumer<JCLambda, JCExpression> changeNode = (lm, ne) -> {}; private DoExprBuilder builder = new DoExprBuilder(); public DoExprVisitor(Context context) { parserFactory = ParserFactory.instance(context); } @Override public void visitVarDef(JCVariableDecl node) { if (node.init != null) { // (b) (1) changeNode = (lm, ne) -> { // 変数への代入式を置換 if (node.init == lm) { node.init = ne; } }; } super.visitVarDef(node); } // (c) @Override public void visitApply(JCMethodInvocation node) { if (node.args != null && node.args.size() > 0) { // (b) (1) changeNode = (lm, ne) -> { // メソッドの引数部分を置換 if (node.args.contains(lm)) { Stream<JCExpression> newArgs = node.args.stream().map(a -> (a == lm)? ne: a); node.args = com.sun.tools.javac.util.List.from(newArgs::iterator); } }; } super.visitApply(node); } @Override public void visitLambda(JCLambda node) { // (a) (2) builder.build(node).ifPresent(expr -> { // (3) JCExpression ne = parseExpression(expr); fixPos(ne, node.pos); // (b) (4) ラムダ部分を差し替え changeNode.accept(node, ne); }); super.visitLambda(node); } // pos 値の修正 private void fixPos(JCExpression ne, final int basePos) { ne.accept(new TreeScanner() { @Override public void scan(JCTree tree) { if(tree != null) { tree.pos += basePos; super.scan(tree); } } }); } // 生成したソースコードをパース private JCExpression parseExpression(String doExpr) { return parserFactory.newParser(doExpr, false, false, false).parseExpression(); } }
コード生成処理の実装
src/main/java/sample/DoExprBuilder.java
package sample; import com.sun.tools.javac.tree.JCTree.*; import java.util.HashMap; import java.util.Map; import java.util.Optional; public class DoExprBuilder { private static final String DO_TYPE = "$do"; private static final String VAR_PREFIX = "#{"; private static final String VAR_SUFFIX = "}"; // let 用のコードテンプレート private static final String LET_CODE = "#{var}.bind(#{rExpr}, #{lExpr} -> #{body})"; // return 用のコードテンプレート private static final String RETURN_CODE = "#{var}.unit( #{expr} )"; private Map<Class<? extends JCStatement>, CodeGenerator<JCStatement>> builderMap = new HashMap<>(); public DoExprBuilder() { // let 用のコード生成 builderMap.put(JCVariableDecl.class, (n, v, b) -> generateCodeForLet(cast(n), v, b)); // return 用のコード生成 builderMap.put(JCReturn.class, (n, v, b) -> generateCodeForReturn(cast(n), v, b)); } public Optional<String> build(JCLambda node) { return getDoVar(node).map(var -> createExpression((JCBlock)node.body, var)); } private String createExpression(JCBlock block, String var) { String res = ""; for (JCStatement st : block.stats.reverse()) { res = builderMap.getOrDefault(st.getClass(), this::generateNoneCode).generate(st, var, res); } return res; } private String generateNoneCode(JCStatement node, String var, String body) { return body; } // let 用のソースコード生成 private String generateCodeForLet(JCVariableDecl node, String var, String body) { String res = body; if ("let".equals(node.vartype.toString())) { Map<String, String> params = createParams(var); params.put("body", res); params.put("lExpr", node.name.toString()); params.put("rExpr", node.init.toString()); // 入れ子への対応 if (node.init instanceof JCLambda) { JCLambda lm = cast(node.init); getDoVar(lm).ifPresent(childVar -> params.put("rExpr", createExpression((JCBlock) lm.body, childVar))); } res = buildTemplate(LET_CODE, params); } return res; } // return 用のソースコード生成 private String generateCodeForReturn(JCReturn node, String var, String body) { Map<String, String> params = createParams(var); params.put("expr", node.expr.toString()); return buildTemplate(RETURN_CODE, params); } // 処理変数名の抽出 private Optional<String> getDoVar(JCLambda node) { if (node.params.size() == 1) { String name = node.params.get(0).name.toString(); if (name.endsWith(DO_TYPE)) { return Optional.of(name.replace(DO_TYPE, "")); } } return Optional.empty(); } private Map<String, String> createParams(String var) { Map<String, String> params = new HashMap<>(); params.put("var", var); return params; } // テンプレート処理 private String buildTemplate(String template, Map<String, String> params) { String res = template; for(Map.Entry<String, String> param : params.entrySet()) { res = res.replace(VAR_PREFIX + param.getKey() + VAR_SUFFIX, param.getValue()); } return res; } @SuppressWarnings("unchecked") private <S, T> T cast(S obj) { return (T)obj; } private interface CodeGenerator<T> { String generate(T node, String var, String body); } }
Service Provider 設定ファイルやビルド定義は 前回 と同じものです。
Service Provider 設定ファイル
src/main/resources/META-INF/services/javax.annotation.processing.Processor
sample.DoExprProcessor
build.gradle
apply plugin: 'java' def enc = 'UTF-8' tasks.withType(AbstractCompile)*.options*.encoding = enc dependencies { compile files("${System.properties['java.home']}/../lib/tools.jar") }
ビルド
ビルド実行
> gradle build :compileJava :processResources UP-TO-DATE :classes :jar :assemble :compileTestJava UP-TO-DATE :processTestResources UP-TO-DATE :testClasses UP-TO-DATE :test UP-TO-DATE :check UP-TO-DATE :build BUILD SUCCESSFUL
ビルド結果として build/libs/java_do_expr.jar3
が生成されます。
動作確認
下記のサンプルコードを使ってアノテーションプロセッサの動作確認を行います。
example/DoExprSample.java
import java.util.function.Function; import java.util.function.Supplier; import java.util.Optional; public class DoExprSample { public static void main(String... args) { Optional<Integer> o1 = Optional.of(2); Optional<Integer> o2 = Optional.of(3); Opt<Integer> opt = new Opt<>(); Optional<Integer> res = opt$do -> { let a = o1; let b = o2; let c = Optional.of(4); return a + b + c * 2; }; // Optional[13] System.out.println(res); Opt<String> opt2 = new Opt<>(); Optional<String> res2 = opt2$do -> { let a = Optional.of("a"); let b = Optional.of("b"); let c = opt2$do -> { let c1 = Optional.of("c1"); let c2 = Optional.of("c2"); return c1 + "-" + c2; }; return a + b + "/" + c; }; // Optional[ab/c1-c2] System.out.println(res2); // Optional[***ba] System.out.println(opt2$do -> { let a = Optional.of("a"); let b = Optional.of("b"); return "***" + b + a; }); } static class Opt<T> { public Optional<T> bind(Optional<T> x, Function<T, Optional<T>> f) { return x.flatMap(f); } public Optional<T> unit(T v) { return Optional.ofNullable(v); } } }
java_do_expr3.jar を使って上記ソースファイルをコンパイルします。
出力内容(変換後のソースコード)を見る限りは変換できているようです。
コンパイル
> javac -cp ../build/libs/java_do_expr3.jar DoExprSample.java ・・・ public class DoExprSample { ・・・ public static void main(String... args) { Optional<Integer> o1 = Optional.of(2); Optional<Integer> o2 = Optional.of(3); Opt<Integer> opt = new Opt<>(); Optional<Integer> res = opt.bind(o1, (a)->opt.bind(o2, (b)->opt.bind(Optional.of(4), (c)->opt.unit(a + b + c * 2)))); System.out.println(res); Opt<String> opt2 = new Opt<>(); Optional<String> res2 = opt2.bind(Optional.of("a"), (a)->opt2.bind(Optional.of("b"), (b)->opt2.bind(opt2.bind(Optional.of("c1"), (c1)->opt2.bind(Optional.of("c2"), (c2)->opt2.unit(c1 + "-" + c2))), (c)->opt2.unit(a + b + "/" + c)))); System.out.println(res2); System.out.println(opt2.bind(Optional.of("a"), (a)->opt2.bind(Optional.of("b"), (b)->opt2.unit("***" + b + a)))); } ・・・ }
DoExprSample
を実行すると正常に動作しました。
実行結果
> java DoExprSample Optional[13] Optional[ab/c1-c2] Optional[***ba]
Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現2
前回 に引き続き、今回も Java のアノテーションプロセッサを使って下記と同等機能を実現します。
今回は、F# のコンピュテーション式を模した下記のような構文 (前回断念したもの) を使用します。
Supplier<Optional<Integer>> res = opt$do -> { let a = o1; let b = o2; return a + b; };
ソースは http://github.com/fits/try_samples/tree/master/blog/20150513/
改良版は 「Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現3」 を参照
はじめに
基本的な変換方法は 前回 と同じですが、かなりシンプルになっていると思います。
変数名$do
の $do は変換対象としてマーキングするために付けています。
変換前 (アノテーションプロセッサ処理前)
Supplier<Optional<Integer>> res = opt$do -> { let a = o1; let b = o2; return a + b; };
変換後 (アノテーションプロセッサ処理後)
Supplier<Optional<Integer>> res = () -> opt.bind(o1, a -> opt.bind(o2, b -> opt.unit(a + b)));
アノテーションプロセッサの実装
Processor の実装
src/main/java/sample/DoExprProcessor.java
package sample; import java.util.Set; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.util.Trees; import com.sun.source.util.TreePath; import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.util.Context; @SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedAnnotationTypes("*") public class DoExprProcessor extends AbstractProcessor { private Trees trees; private Context context; @Override public void init(ProcessingEnvironment procEnv) { trees = Trees.instance(procEnv); context = ((JavacProcessingEnvironment)procEnv).getContext(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { roundEnv.getRootElements().stream().map(this::toUnit).forEach(this::processUnit); return false; } private CompilationUnitTree toUnit(Element el) { TreePath path = trees.getPath(el); return path.getCompilationUnit(); } private void processUnit(CompilationUnitTree cu) { // AST 変換 cu.accept(new DoExprVisitor(context), null); // 変換後のソースを出力 System.out.println(cu); } }
TreeVisitor の実装
基本的な変換内容は 前回 と同じですが、下記の点が異なります。
- (1) 対象処理を変換したソースコードを作って
JCExpression
へパース - (2) 生成した
JCExpression
内の全pos
の値を修正 - (3)
JCLambda
の body を差し替え
(2) が重要で、pos
(ソースコード内の位置) の値を調整しておかないと変換後の AST をコンパイルする段階でエラーになります。 (前回失敗した理由)
新しく生成した JCExpression
の木構造をたどって全要素の pos
を変更するために com.sun.tools.javac.tree.TreeScanner
の scan
メソッドをオーバーライドして使っています。
また、今回の構文ではラムダの paramKind
が IMPLICIT
となりますので(前回はラムダ引数の型を指定していたので EXPLICIT
だった)、ラムダの引数を消去した際に paramKind
を EXPLICIT
へ変更しています。
src/main/java/sample/DoExprVisitor.java
package sample; import com.sun.source.tree.*; import com.sun.source.util.TreeScanner; import com.sun.tools.javac.parser.ParserFactory; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.*; import com.sun.tools.javac.util.Context; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.stream.Stream; public class DoExprVisitor extends TreeScanner<Void, Void> { private static final String DO_TYPE = "$do"; private ParserFactory parserFactory; private Map<String, TemplateBuilder> builderMap = new HashMap<>(); public DoExprVisitor(Context context) { parserFactory = ParserFactory.instance(context); // let 用の変換内容 builderMap.put("let", new TemplateBuilder("${var}.bind(${rExpr}, ${lExpr} -> ${body})", this::createBindParams)); // return 用の変換内容 builderMap.put("return", new TemplateBuilder("${var}.unit( ${expr} )", this::createBasicParams)); } @Override public Void visitLambdaExpression(LambdaExpressionTree node, Void p) { if (node instanceof JCLambda) { JCLambda lm = (JCLambda)node; if (lm.params.size() == 1) { getDoVar(lm.params.get(0)).ifPresent(var -> { // ラムダの引数を消去 lm.params = com.sun.tools.javac.util.List.nil(); lm.paramKind = JCLambda.ParameterKind.EXPLICIT; // (1) 対象処理を変換したソースコードを作って JCExpression へパース JCExpression ne = parseExpression(createExpression((JCBlock)lm.body, var)); // (2) 生成した JCExpression 内の全 pos の値を修正 fixPos(ne, lm.pos); // (3) JCLambda の body を差し替え lm.body = ne; }); } } return super.visitLambdaExpression(node, p); } // pos の値を修正する private void fixPos(JCExpression ne, int basePos) { ne.accept(new com.sun.tools.javac.tree.TreeScanner() { @Override public void scan(JCTree tree) { if(tree != null) { tree.pos += basePos; super.scan(tree); } } }); } // 対象処理を変換したソースコード (Expression) を生成 private String createExpression(JCBlock block, String var) { Stream<String> revExpr = block.stats.reverse().stream().map(s -> s.toString().replaceAll(";", "")); return revExpr.reduce("", (acc, v) -> { int spacePos = v.indexOf(" "); String action = v.substring(0, spacePos); if (builderMap.containsKey(action)) { acc = builderMap.get(action).build(var, acc, v.substring(spacePos + 1)); } return acc; }); } // 生成したソースコード (Expression) を JavacParser で JCExpression へ変換 private JCExpression parseExpression(String doExpr) { return parserFactory.newParser(doExpr, false, false, false).parseExpression(); } private Optional<String> getDoVar(JCVariableDecl param) { String name = param.name.toString(); return name.endsWith(DO_TYPE)? Optional.of(name.replace(DO_TYPE, "")): Optional.empty(); } private Map<String, String> createBindParams(String var, String body, String expr) { Map<String, String> params = createBasicParams(var, body, expr); String[] vexp = expr.split("="); params.put("lExpr", vexp[0]); params.put("rExpr", vexp[1]); return params; } private Map<String, String> createBasicParams(String var, String body, String expr) { Map<String, String> params = new HashMap<>(); params.put("var", var); params.put("body", body); params.put("expr", expr); return params; } private interface ParamCreator { Map<String, String> create(String var, String body, String expr); } private class TemplateBuilder { private static final String VAR_PREFIX = "\\$\\{"; private static final String VAR_SUFFIX = "\\}"; private String template; private ParamCreator paramCreator; TemplateBuilder(String template, ParamCreator paramCreator) { this.template = template; this.paramCreator = paramCreator; } public String build(String var, String body, String expr) { return buildTemplate(template, paramCreator.create(var, body, expr)); } private String buildTemplate(String template, Map<String, String> params) { return params.entrySet().stream().reduce(template, (acc, v) -> acc.replaceAll(VAR_PREFIX + v.getKey() + VAR_SUFFIX, v.getValue()), (a, b) -> a); } } }
Service Provider 設定ファイルやビルド定義も 前回 と同じものです。
Service Provider 設定ファイル
src/main/resources/META-INF/services/javax.annotation.processing.Processor
sample.DoExprProcessor
build.gradle
apply plugin: 'java' def enc = 'UTF-8' tasks.withType(AbstractCompile)*.options*.encoding = enc dependencies { compile files("${System.properties['java.home']}/../lib/tools.jar") }
ビルド
ビルド実行
> gradle build :compileJava :processResources UP-TO-DATE :classes :jar :assemble :compileTestJava UP-TO-DATE :processTestResources UP-TO-DATE :testClasses UP-TO-DATE :test UP-TO-DATE :check UP-TO-DATE :build BUILD SUCCESSFUL
ビルド結果として build/libs/java_do_expr.jar2
が生成されました。
動作確認
下記のサンプルコードを使ってアノテーションプロセッサの動作確認を行います。
example/DoExprSample.java
import java.util.function.Function; import java.util.function.Supplier; import java.util.Optional; public class DoExprSample { public static void main(String... args) { Optional<Integer> o1 = Optional.of(2); Optional<Integer> o2 = Optional.of(3); Opt<Integer> opt = new Opt<>(); // アノテーションプロセッサで変換する処理1 Supplier<Optional<Integer>> res = opt$do -> { let a = o1; let b = o2; let c = Optional.of(4); return a + b + c * 2; }; // Optional[13] System.out.println(res.get()); Opt<String> opt2 = new Opt<>(); // アノテーションプロセッサで変換する処理2 Supplier<Optional<String>> res2 = opt2$do -> { let a = Optional.of("a"); let b = Optional.of("b"); return a + b; }; // Optional["ab"] System.out.println(res2.get()); } // Optional 用の bind・unit メソッド実装クラス static class Opt<T> { public Optional<T> bind(Optional<T> x, Function<T, Optional<T>> f) { return x.flatMap(f); } public Optional<T> unit(T v) { return Optional.ofNullable(v); } } }
java_do_expr2.jar を使って上記ソースファイルをコンパイルします。
出力内容(変換後のソースコード)を見る限り正常に変換できているようです。
コンパイル
> javac -cp ../build/libs/java_do_expr2.jar DoExprSample.java ・・・ public class DoExprSample { ・・・ public static void main(String... args) { Optional<Integer> o1 = Optional.of(2); Optional<Integer> o2 = Optional.of(3); Opt<Integer> opt = new Opt<>(); Supplier<Optional<Integer>> res = ()->opt.bind(o1, (a)->opt.bind(o2, (b)->opt.bind(Optional.of(4), (c)->opt.unit(a + b + c * 2)))); System.out.println(res.get()); Opt<String> opt2 = new Opt<>(); Supplier<Optional<String>> res2 = ()->opt2.bind(Optional.of("a"), (a)->opt2.bind(Optional.of("b"), (b)->opt2.unit(a + b))); System.out.println(res2.get()); } ・・・ }
DoExprSample
を実行すると正常に動作しました。
実行結果
> java DoExprSample Optional[13] Optional[ab]
Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現
「アノテーションプロセッサで AST 変換 - Lombok を参考にして変数の型をコンパイル時に変更」の応用編です。
前回は変数の型を var
から java.lang.Object
へ変更しただけでしたが、今回は下記と同等な機能の簡易版をアノテーションプロセッサで実現してみます。
ソースは http://github.com/fits/try_samples/tree/master/blog/20150511/
改良版は 「Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現3」 を参照
はじめに
前回 と同様にアノテーションプロセッサにおける下記の特徴を利用し、コンパイル前に AST (抽象構文木) を書き換えます。
変換例
今回は、以下のような変換をアノテーションプロセッサ内で実施します。 Haskell の do 記法というよりは F# のコンピュテーション式に近くなっています。
変換前 (アノテーションプロセッサ処理前)
Supplier<Optional<Integer>> res = ($do<Optional, Integer> opt) -> { let a = o1; let b = o2; return a + b; };
変換後 (アノテーションプロセッサ処理後)
Supplier<Optional<Integer>> res = ()->{ return opt.bind(o1, new java.util.function.Function<Integer, Optional<Integer>>(){ @Override public Optional<Integer> apply(Integer a) { return opt.bind(o2, new java.util.function.Function<Integer, Optional<Integer>>(){ @Override public Optional<Integer> apply(Integer b) { return opt.unit(a + b); } }); } }); };
変換内容
変換前の構文と変換方法を簡単に説明します。
ラムダの引数部分は変換に必要な情報を渡すために使用し、$do
という未定義のクラスは変換対象かどうかをマーキングする目的で使います。
処理変数(変換例の opt
)には bind・unit メソッドを持つオブジェクトインスタンスの変数名を指定します。
また、型推論は難しそうだったので、コンテナ型 (モナドの型) と要素の型を $do
クラスの型引数で指定するようにしています。
変換前
・・・ = ($do<コンテナ型, 要素型> 処理変数) -> { let 変数1 = 式1; ・・・ return 結果式; };
変換は以下のように実施します。
ラムダの引数部分 ($do
の箇所) を全て消去します。 (変換に必要な情報を渡しているだけなので)
(a) ($do<コンテナ型, 要素型> 処理変数) -> {}
の変換内容
・・・ = () -> {
・・・
}
let の部分は bind メソッドを使った処理へ変換します。
(b) let 変数1 = 式1;
の変換内容
return 処理変数.bind(式1, new java.util.function.Function<要素型, コンテナ型<要素型>>(){ @Override public コンテナ型<要素型> apply(要素型 変数1) { ・・・ } });
return の部分は unit メソッドを使った処理へ変換します。
(c) return 結果式;
の変換内容
return 処理変数.unit( 結果式 );
備考
本当は以下のようなシンプルな仕様で実現したかったのですが、変換後のコンパイル時にエラーが発生してしまい、うまく解決できなかったので今回は断念しました。
変換前 (失敗版)
Supplier<Optional<Integer>> res = opt$do -> { let a = o1; let b = o2; return a + b; };
変換後 (失敗版)
Supplier<Optional<Integer>> res = () -> opt.bind(o1, a -> opt.bind(o2, b -> opt.unit(a + b)));
変換後のコンパイルエラー例 (Java 1.8.0_45)
java.lang.AssertionError: Value of x -1 at com.sun.tools.javac.util.Assert.error(Assert.java:133) at com.sun.tools.javac.util.Assert.check(Assert.java:94) at com.sun.tools.javac.util.Bits.incl(Bits.java:200) at com.sun.tools.javac.comp.Flow$AbstractAssignAnalyzer.visitLambda(Flow.java:2254) at com.sun.tools.javac.tree.JCTree$JCLambda.accept(JCTree.java:1624)
ただし、変換後のソースをファイルへ出力し javac で普通にコンパイルすれば問題なく成功しますので、実現不可能では無いと思います。
と書きましたが、生成した箇所の pos の値を全て調整し直せば、 上記の構文で実現できることが判明しました。 (Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現2 参照)
アノテーションプロセッサの実装
それでは本題に入ります。
Processor の実装
まずは、アノテーションプロセッサの本体を実装します。
こちらは 前回 の SampleProcessor2.java
とほぼ同じ内容です。
変換後のソースを確認するため accept
後の CompilationUnitTree
を println
するようにしています。
src/main/java/sample/DoExprProcessor.java
package sample; import java.util.Set; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.util.Trees; import com.sun.source.util.TreePath; import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.util.Context; @SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedAnnotationTypes("*") public class DoExprProcessor extends AbstractProcessor { private Trees trees; private Context context; @Override public void init(ProcessingEnvironment procEnv) { trees = Trees.instance(procEnv); context = ((JavacProcessingEnvironment)procEnv).getContext(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { roundEnv.getRootElements().stream().map(this::toUnit).forEach(this::processUnit); return false; } private CompilationUnitTree toUnit(Element el) { TreePath path = trees.getPath(el); return path.getCompilationUnit(); } private void processUnit(CompilationUnitTree cu) { // AST 変換 cu.accept(new DoExprVisitor(context), null); // 変換後のソースを出力 System.out.println(cu); } }
TreeVisitor の実装
次に、変換処理を実装します。
ラムダを変更するため TreeScanner
を extends し visitLambdaExpression
メソッドをオーバーライドします。
LambdaExpressionTree
だとラムダの内容を変更できないので JCLambda
へキャストし、以下のような処理を実施します。
- (1) 変換対象かどうかをチェック (引数の型に $do を使っているかどうか等)
- (2) ラムダの引数を消去
- (3) ラムダの処理内容(let や return)を元に変換後のソースを生成
- (4) (3) を
JavacParser
を使ってJCStatement
へ変換 - (5) ラムダの処理内容を (4) の結果で差し替え (
JCLambda
のbody.stats
の値を変更)
通常は、JCStatement
を直接構築して差し替えると思うのですが、JCStatement
を自前で構築するのは大変そうだったので、部分的なソースコードを作り (3) 、それを JavacParser
にパースさせる事で JCStatement
を得ています (4)。
また、JCTree 内で使用されている List クラスは java.util.List
ではなく com.sun.tools.javac.util.List
なので注意。
src/main/java/sample/DoExprVisitor.java
package sample; import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.util.TreeScanner; import com.sun.tools.javac.parser.ParserFactory; import com.sun.tools.javac.tree.JCTree.*; import com.sun.tools.javac.util.Context; import java.util.HashMap; import java.util.Map; import java.util.function.BiFunction; import java.util.stream.Stream; public class DoExprVisitor extends TreeScanner<Void, Void> { private static final String DO_TYPE = "$do"; // let 変換用のソースコード private static final String BIND_CODE = " return ${var}.bind(${rExpr}, new java.util.function.Function<${vType}, ${mType}<${vType}>>(){" + " @Override public ${mType}<${vType}> apply(${vType} ${lExpr}){ ${body} }" + " });"; // return 変換用のソースコード private static final String UNIT_CODE = " return ${var}.unit( ${expr} );"; private ParserFactory parserFactory; private Map<String, TemplateBuilder> builderMap = new HashMap<>(); public DoExprVisitor(Context context) { parserFactory = ParserFactory.instance(context); // (b) let 用の変換内容 builderMap.put("let", new TemplateBuilder(BIND_CODE, this::createBindParams)); // (c) return 用の変換内容 builderMap.put("return", new TemplateBuilder(UNIT_CODE, this::createUnitParams)); } @Override public Void visitLambdaExpression(LambdaExpressionTree node, Void p) { if (node instanceof JCLambda) { JCLambda lm = (JCLambda)node; // (1) 変換対象かどうかをチェック if (isSingleTypeApplyParam(lm)) { JCVariableDecl param = lm.params.get(0); // (1) 変換対象かどうかをチェック if (isDoType(param)) { // (2) ラムダの引数を消去 lm.params = com.sun.tools.javac.util.List.nil(); JCBlock block = (JCBlock)lm.body; // 変換後の処理内容(Statement)を作成 (3) (4) JCStatement newStats = parseStatement(createStatement(block, createBaseParams(param))); // (5) ラムダの処理内容を差し替え block.stats = com.sun.tools.javac.util.List.of(newStats); } } } return super.visitLambdaExpression(node, p); } // (3) ラムダの処理内容を変換したソースコードを生成 private String createStatement(JCBlock block, Map<String, String> params) { // ラムダの内容 (JCStatement のリスト) を逆順化して個別に文字列化 Stream<String> revStats = block.stats.reverse().stream().map(s -> s.toString().replaceAll(";", "")); // 逆順化したリストを順次変換 return revStats.reduce("", (acc, v) -> { int spacePos = v.indexOf(" "); String action = v.substring(0, spacePos); if (builderMap.containsKey(action)) { acc = builderMap.get(action).build(params, acc, v.substring(spacePos + 1)); } return acc; }); } // (4) 生成したソースコードを JavacParser で JCStatement へ変換 private JCStatement parseStatement(String doStat) { return parserFactory.newParser(doStat, false, false, false).parseStatement(); } // (1) private boolean isDoType(JCVariableDecl param) { String type = ((JCTypeApply)param.vartype).clazz.toString(); return DO_TYPE.equals(type); } // (1) private boolean isSingleTypeApplyParam(JCLambda lm) { return lm.params.size() == 1 && lm.params.get(0).vartype instanceof JCTypeApply; } private Map<String, String> createBaseParams(JCVariableDecl param) { Map<String, String> params = new HashMap<>(); params.put("var", param.name.toString()); JCTypeApply paramType = (JCTypeApply)param.vartype; params.put("mType", paramType.arguments.get(0).toString()); params.put("vType", paramType.arguments.get(1).toString()); return params; } private Map<String, String> createBindParams(String body, String expr) { Map<String, String> params = createUnitParams(body, expr); String[] divexp = expr.split("="); params.put("lExpr", divexp[0]); params.put("rExpr", divexp[1]); return params; } private Map<String, String> createUnitParams(String body, String expr) { Map<String, String> params = new HashMap<>(); params.put("body", body); params.put("expr", expr); return params; } // テンプレート処理を実施するためのクラス private class TemplateBuilder { private static final String VAR_PREFIX = "\\$\\{"; private static final String VAR_SUFFIX = "\\}"; private String template; private BiFunction<String, String, Map<String, String>> paramCreator; TemplateBuilder(String template, BiFunction<String, String, Map<String, String>> paramCreator) { this.template = template; this.paramCreator = paramCreator; } public String build(Map<String, String> params, String body, String expr) { return buildTemplate( buildTemplate(template, params), paramCreator.apply(body, expr)); } private String buildTemplate(String template, Map<String, String> params) { return params.entrySet().stream().reduce(template, (acc, v) -> acc.replaceAll(VAR_PREFIX + v.getKey() + VAR_SUFFIX, v.getValue()), (a, b) -> a); } } }
JCLambda の変換イメージ例
JCLambda の大まかな変換イメージを書くと以下のようになります。
ソースコード例
($do<Optional, String> opt2) -> { let a = Optional.of("a"); let b = Optional.of("b"); return a + b; }
上記のソースを JCLambda 化すると以下のようになります。
実際はもっと複雑ですが、適当に簡略化しています。 (init や expr の内容も実際は JCMethodInvocation
等が入れ子になっています)
変換前 JCLambda の内容
JCLambda( params = [ JCVariableDecl( name = opt2, vartype = $do<Optional, String> ) ], paramKind = EXPLICIT, body = JCBlock( stats = [ JCVariableDecl( name = a, vartype = let, init = Optional.of("a") ), JCVariableDecl( name = b, vartype = let, init = Optional.of("a") ), JCReturn( expr = a + b ) ] ) )
DoExprVisitor
では上記を以下のように変更します。
変換後 JCLambda の内容
JCLambda(
params = [],
paramKind = EXPLICIT,
body = JCBlock(
stats = [
JCReturn(
expr = opt2.bind(Optional.of("a"), ・・・)
)
]
)
)
今回は body に設定されている JCBlock
をそのまま使いましたが、body の値を直接変更する方法も考えられます。
Service Provider 設定ファイル
アノテーションプロセッサで sample.DoExprProcessor
を使用するように Service Provider の設定ファイルを用意します。
src/main/resources/META-INF/services/javax.annotation.processing.Processor
sample.DoExprProcessor
ビルド
ビルド定義ファイルは以下の通り。
build.gradle
apply plugin: 'java' def enc = 'UTF-8' tasks.withType(AbstractCompile)*.options*.encoding = enc dependencies { compile files("${System.properties['java.home']}/../lib/tools.jar") }
ビルド実行
> gradle build :compileJava :processResources UP-TO-DATE :classes :jar :assemble :compileTestJava UP-TO-DATE :processTestResources UP-TO-DATE :testClasses UP-TO-DATE :test UP-TO-DATE :check UP-TO-DATE :build BUILD SUCCESSFUL
ビルド結果として build/libs/java_do_expr.jar
が生成されました。
動作確認
最後に、下記のサンプルコードを使ってアノテーションプロセッサの動作確認を行います。
example/DoExprSample.java
import java.util.function.Function; import java.util.function.Supplier; import java.util.Optional; public class DoExprSample { public static void main(String... args) { Optional<Integer> o1 = Optional.of(2); Optional<Integer> o2 = Optional.of(3); Opt<Integer> opt = new Opt<>(); // アノテーションプロセッサで変換する処理1 Supplier<Optional<Integer>> res = ($do<Optional, Integer> opt) -> { let a = o1; let b = o2; let c = Optional.of(4); return a + b + c * 2; }; // Optional[13] System.out.println(res.get()); Opt<String> opt2 = new Opt<>(); // アノテーションプロセッサで変換する処理2 Supplier<Optional<String>> res2 = ($do<Optional, String> opt2) -> { let a = Optional.of("a"); let b = Optional.of("b"); return a + b; }; // Optional["ab"] System.out.println(res2.get()); } // Optional 用の bind・unit メソッド実装クラス static class Opt<T> { public Optional<T> bind(Optional<T> x, Function<T, Optional<T>> f) { return x.flatMap(f); } public Optional<T> unit(T v) { return Optional.ofNullable(v); } } }
java_do_expr.jar を使って上記ソースファイルをコンパイルします。
出力内容(変換後のソースコード)を見る限り正常に変換できているようです。
コンパイル
> javac -cp ../build/libs/java_do_expr.jar DoExprSample.java ・・・ public class DoExprSample { ・・・ public static void main(String... args) { Optional<Integer> o1 = Optional.of(2); Optional<Integer> o2 = Optional.of(3); Opt<Integer> opt = new Opt<>(); Supplier<Optional<Integer>> res = ()->{ return opt.bind(o1, new java.util.function.Function<Integer, Optional<Integer>>(){ @Override() public Optional<Integer> apply(Integer a) { return opt.bind(o2, new java.util.function.Function<Integer, Optional<Integer>>(){ @Override() public Optional<Integer> apply(Integer b) { return opt.bind(Optional.of(4), new java.util.function.Function<Integer, Optional<Integer>>(){ @Override() public Optional<Integer> apply(Integer c) { return opt.unit(a + b + c * 2); } }); } }); } }); }; System.out.println(res.get()); Opt<String> opt2 = new Opt<>(); Supplier<Optional<String>> res2 = ()->{ return opt2.bind(Optional.of("a"), new java.util.function.Function<String, Optional<String>>(){ @Override() public Optional<String> apply(String a) { return opt2.bind(Optional.of("b"), new java.util.function.Function<String, Optional<String>>(){ @Override() public Optional<String> apply(String b) { return opt2.unit(a + b); } }); } }); }; System.out.println(res2.get()); } ・・・ }
DoExprSample
を実行すると正常に動作しました。
実行結果
> java DoExprSample Optional[13] Optional[ab]
成功するまで次を試すような処理へ Either モナドを適用 - FunctionalJava
成功するまで次の処理を試していくような処理に対して Either モナドを適用してみました。
使用した環境は下記の通りです。
- Java SE 8u20
- FunctionalJava 4.2 beta1
ソースは http://github.com/fits/try_samples/tree/master/blog/20140825/
はじめに
Either モナドは 2つの異なる値 (Left と Right) を扱う場合に使用し、一般的には失敗(エラー)を伴う処理に対して下記のような使い方をします。
Leftの値 | Rightの値 |
---|---|
失敗時のエラー内容(例外) | 成功時の値 |
ただし今回は、下記のように処理が成功するまで元の値が Left へ保持されるような使い方をしてみました。
Leftの値 | Rightの値 |
---|---|
元の値 | 成功時の値 |
具体的には、下記のような処理を試します。
- 実行時引数で指定した日付文字列に対して、Date オブジェクトへのパースが成功するまで次のパース処理を試していく
Either モナドを使わなかった場合
まずは Either モナドを使わず、普通に Java で実装してみました。
下記 (1) ~ (5) のパース処理を順に試して、例外が発生せず null では無い値を返した時点でパース処理を終了します。
- (1) ISO-8601 タイムゾーン無しの日付文字列(例
2014-08-25T13:20:00
)をパース - (2) ISO-8601 の日付文字列(例
2014-08-25T13:20:00+09:00
)をパース - (3) ISO-8601 タイムゾーン付きの日付文字列(例
2014-08-25T13:20:00+09:00[Asia/Tokyo]
)をパース - (4) "yyyy-MM-dd HH:mm:ss" をパース
- (5) "now" の場合に Date オブジェクトを返す
java.time
のクラスを使う必要も無かったのですが、ついでに試してみました。
DateParse.java
import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZonedDateTime; import java.time.ZoneOffset; import java.util.Date; import java.util.function.Function; public class DateParse { public static void main(String... args) { // SimpleDateFormat を使ったパース Function<String, Function<String, Date>> simpleDate = df -> s -> { try { return new SimpleDateFormat(df).parse(s); } catch (Exception e) { throw new RuntimeException(e); } }; Date res = parseDate( args[0], s -> Date.from(LocalDateTime.parse(s).toInstant(ZoneOffset.UTC)), //(1) s -> Date.from(OffsetDateTime.parse(s).toInstant()), // (2) s -> Date.from(ZonedDateTime.parse(s).toInstant()), // (3) simpleDate.apply("yyyy-MM-dd HH:mm:ss"), // (4) s -> "now".equals(s)? new Date(): null // (5) ); System.out.println("------"); System.out.println(res); } // 日付文字列のパース @SafeVarargs public static Date parseDate(String date, Function<String, Date>... funcList) { for (Function<String, Date> func : funcList) { try { Date res = func.apply(date); if (res != null) { return res; } } catch (Exception ex) { System.out.println("* " + ex.getMessage()); } } return null; } }
実行結果1 - (1) で成功(オフセット指定無しのため JST では +9時間される)
> java DateParse "2014-08-25T13:20:00" ------ Mon Aug 25 22:20:00 JST 2014
実行結果2 - (2) で成功
> java DateParse "2014-08-25T13:20:00+09:00" * Text '2014-08-25T13:20:00+09:00' could not be parsed, unparsed text found at index 19 ------ Mon Aug 25 13:20:00 JST 2014
実行結果3 - (3) で成功
> java DateParse "2014-08-25T13:20:00+09:00[Asia/Tokyo]" * Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 19 * Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 25 ------ Mon Aug 25 13:20:00 JST 2014
実行結果4 - (4) で成功
> java DateParse "2014-08-25 13:20:00" * Text '2014-08-25 13:20:00' could not be parsed at index 10 * Text '2014-08-25 13:20:00' could not be parsed at index 10 * Text '2014-08-25 13:20:00' could not be parsed at index 10 ------ Mon Aug 25 13:20:00 JST 2014
実行結果5 - (5) で成功
> java DateParse "now" * Text 'now' could not be parsed at index 0 * Text 'now' could not be parsed at index 0 * Text 'now' could not be parsed at index 0 * java.text.ParseException: Unparseable date: "now" ------ Mon Aug 25 11:10:42 JST 2014
実行結果6 - 全失敗
> java DateParse "2014-08-25" * Text '2014-08-25' could not be parsed at index 10 * Text '2014-08-25' could not be parsed at index 10 * Text '2014-08-25' could not be parsed at index 10 * java.text.ParseException: Unparseable date: "2014-08-25" ------ null
Either モナドを使った場合
同様の処理を FunctionalJava の Either を使って実装します。
Left にパース前の値(String)、Right にパース後の値(Date)を格納できるように Either の型を Either<String, Date>
とします。
今回は Left の値 (String) に対して順次パース処理を試すようにしたいので、Either.left()
で取得した Either.LeftProjection
オブジェクトへパース処理を bind
しています。
bind の引数には 「普通の値を取って Either を返す」 処理 (下記では F<String, Either<String, Date>>
) を与える必要があるので、下記では eitherK
というメソッドを定義し、通常のパース処理 (文字列を取って Date を返す F<String, Date>
) を変換してから bind するようにしています。
なお、java.util.function.Function は使わず、全面的に FunctionalJava の fj.F
を使うようにしました。
EitherDateParse.java
import fj.F; import fj.data.Either; import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZonedDateTime; import java.time.ZoneOffset; import java.util.Date; public class EitherDateParse { public static void main(String... args) { F<String, F<String, Date>> simpleDate = df -> s -> { try { return new SimpleDateFormat(df).parse(s); } catch (Exception e) { throw new RuntimeException(e); } }; Either<String, Date> res = parseDate( Either.left(args[0]), s -> Date.from(LocalDateTime.parse(s).toInstant(ZoneOffset.UTC)), // (1) s -> Date.from(OffsetDateTime.parse(s).toInstant()), // (2) s -> Date.from(ZonedDateTime.parse(s).toInstant()), // (3) simpleDate.f("yyyy-MM-dd HH:mm:ss"), // (4) s -> "now".equals(s)? new Date(): null // (5) ); System.out.println("------"); System.out.println(res); } @SafeVarargs public static Either<String, Date> parseDate(Either<String, Date> date, F<String, Date>... funcList) { for (F<String, Date> func : funcList) { date = date.left().bind( eitherK(func) ); } return date; } // F<S, T> の処理が例外発生か null であれば Left、そうでなければ Right を返す処理へ変換 private static <S, T> F<S, Either<S, T>> eitherK(final F<S, T> func) { return s -> { try { T res = func.f(s); return (res == null)? Either.left(s): Either.right(res); } catch (Exception ex) { System.out.println("* " + ex.getMessage()); return Either.left(s); } }; } }
実行結果は、Left や Right に包まれている部分を除けば Either を使わなかった場合と基本的に同じです。 (全失敗の場合は異なります)
実行結果1 - (1) で成功(オフセット指定無しのため JST では +9時間される)
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25T13:20:00" ------ Right(Mon Aug 25 22:20:00 JST 2014)
実行結果2 - (2) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25T13:20:00+09:00" * Text '2014-08-25T13:20:00+09:00' could not be parsed, unparsed text found at index 19 ------ Right(Mon Aug 25 13:20:00 JST 2014)
実行結果3 - (3) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25T13:20:00+09:00[Asia/Tokyo]" * Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 19 * Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 25 ------ Right(Mon Aug 25 13:20:00 JST 2014)
実行結果4 - (4) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25 13:20:00" * Text '2014-08-25 13:20:00' could not be parsed at index 10 * Text '2014-08-25 13:20:00' could not be parsed at index 10 * Text '2014-08-25 13:20:00' could not be parsed at index 10 ------ Right(Mon Aug 25 13:20:00 JST 2014)
実行結果5 - (5) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "now" * Text 'now' could not be parsed at index 0 * Text 'now' could not be parsed at index 0 * Text 'now' could not be parsed at index 0 * java.text.ParseException: Unparseable date: "now" ------ Right(Mon Aug 25 11:59:01 JST 2014)
実行結果6 - 全失敗
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25" * Text '2014-08-25' could not be parsed at index 10 * Text '2014-08-25' could not be parsed at index 10 * Text '2014-08-25' could not be parsed at index 10 * java.text.ParseException: Unparseable date: "2014-08-25" ------ Left(2014-08-25)
Either モナドを使った場合2 - 機能追加
次に下記のような機能を追加してみました。
- (a) パース全失敗の場合は RuntimeException を throw する
- (b) パース成功の場合は、その次の日の 0時 0分 0秒 の Date オブジェクトと共に 2要素の vector (
V2
) へ格納する
Either モナドを使えば if 文などでいちいち条件判定しなくても変換処理等を合成できるのが利点だと思います。
今回は、Either の結果出力に System.out.println() を直接使わず fj.Show.println()
を使うようにしてみました。
EitherDateParse2.java
・・・ import fj.data.vector.V; import fj.data.vector.V2; import fj.Show; import org.apache.commons.lang3.time.DateUtils; ・・・ public class EitherDateParse2 { public static void main(String... args) { ・・・ Either<String, Date> res = parseDate( ・・・ ); // (a) パースが全失敗の場合は RuntimeException を throw res.left().bind( s -> { throw new RuntimeException("failed parse"); }); // (b) パース成功の場合は、次の日の 0時0分0秒 の結果と共に V2 へ格納して返す Either<String, V2<Date>> res2 = res.right().bind( d -> Either.right( V.v(d, DateUtils.truncate(DateUtils.addDays(d, 1), Calendar.DATE)) ) ); System.out.println("------"); Show.<String, V2<Date>>eitherShow(Show.anyShow(), Show.v2Show(Show.anyShow())).println(res2); } ・・・ }
実行結果 - 成功した場合
> java -cp .;functionaljava-4.2-beta-1.jar;commons-lang3-3.3.2.jar EitherDateParse2 "2014-08-25 13:20:00" * Text '2014-08-25 13:20:00' could not be parsed at index 10 * Text '2014-08-25 13:20:00' could not be parsed at index 10 * Text '2014-08-25 13:20:00' could not be parsed at index 10 ------ Right(<Mon Aug 25 13:20:00 JST 2014,Tue Aug 26 00:00:00 JST 2014>)
実行結果 - 全失敗した場合
> java -cp .;functionaljava-4.2-beta-1.jar;commons-lang3-3.3.2.jar EitherDateParse2 "2014-08-25" * Text '2014-08-25' could not be parsed at index 10 * Text '2014-08-25' could not be parsed at index 10 * Text '2014-08-25' could not be parsed at index 10 * java.text.ParseException: Unparseable date: "2014-08-25" Exception in thread "main" java.lang.RuntimeException: failed parse at EitherDateParse2.lambda$main$19(EitherDateParse2.java:37) ・・・