Kotlin の関数型プログラミング用ライブラリ Λrrow を試してみる

Kotlin で ScalaScalazCats のような関数型プログラミング用のライブラリを探していたところ、以下を見つけたので試してみました。

ソースは 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 の SomeNone の作成にはいくつかの方法が用意されています。

Monad の拡張関数として定義されている binding を使うと 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> クラス内に定義されています。

EitherContextPartiallyApplied オブジェクトは ForEither 関数 で取得できるので、これを使って 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

funfixJavaScript, TypeScript, Flow の関数型プログラミング用ライブラリで、Fantasy LandStatic 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) DbStaterun メソッドは 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 オブジェクトの生成メソッド commandquery をそれぞれ定義 (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アノテーションプロセッサを使って下記と同等の機能を実現する試みの第三弾です。

  • Haskell の do 記法
  • Scala の for 内包表記
  • F# のコンピュテーション式

前回 のものを改良し、ようやく下記のような構文を実現しました。

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 へ置換処理 (JCLambdaJCMethodInvocation へ差し替える事になる) を設定するようにしました。

主な処理内容は次のようになっています。

  • (1) 変数定義(JCVariableDecl)やメソッド実行(JCMethodInvocation)の箇所で該当部分を差し替えるための処理を changeNode へ設定
  • (2) ラムダの内容からソースコードを生成 (対象外なら何もしない)
  • (3) ソースコードJCExpression へパースして (実体は JCMethodInvocationpos の値を調整
  • (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アノテーションプロセッサを使って下記と同等機能を実現します。

  • Haskell の do 記法
  • Scala の for 内包表記
  • F# のコンピュテーション式

今回は、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.TreeScannerscan メソッドをオーバーライドして使っています。

また、今回の構文ではラムダの paramKindIMPLICIT となりますので(前回はラムダ引数の型を指定していたので EXPLICIT だった)、ラムダの引数を消去した際に paramKindEXPLICIT へ変更しています。

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 へ変更しただけでしたが、今回は下記と同等な機能の簡易版をアノテーションプロセッサで実現してみます。

  • Haskell の do 記法
  • Scala の for 内包表記
  • F# のコンピュテーション式

ソースは 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 後の CompilationUnitTreeprintln するようにしています。

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) の結果で差し替え (JCLambdabody.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 モナドを適用してみました。

使用した環境は下記の通りです。

ソースは 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要素の vectorV2) へ格納する

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)
        ・・・