Java で Scala っぽい Option 型のパターンマッチングと map、flatMap を実現してみる

Scala の Option 型を Java で実装してみたらどうなるかなという実験をしたのでメモっとく。[列挙型と switch 文の進化形！？Java で代数的データ型とパターンマッチングを実現してみる](https://qiita.com/takuya0301/items/94645b741e5da6f53d77) の続き。

# 実験用の Gradle プロジェクトを生成する

Lombok を使いたいので JShell ではなく Gradle プロジェクトで実験する。下記のコマンドで Gradle プロジェクトを生成しよう。

```shell-session
$ mkdir option
$ cd option
$ gradle init \
    --type java-library \
    --dsl groovy \
    --test-framework junit \
    --project-name option \
    --package com.example
```

`build.gradle` ファイルを更新して Lombok を導入する。

```diff
--- a/build.gradle
+++ b/build.gradle
@@ -9,6 +9,7 @@
 plugins {
     // Apply the java-library plugin to add support for Java Library
     id 'java-library'
+    id "io.freefair.lombok" version "4.1.6"
 }
 
 repositories {
```

# パターンマッチングできる Option 型を実装する

Option 型を下記のように Java で実装してみる。見通しをよくするためシールドクラスパターン (Sealed Class Pattern) を使用していない。

`Some` クラスが保持する型 `T` とパターンマッチングの結果型 `R` があり、実装する中で結構混乱した。コードを読む人は注意してみてほしい。

```java:src/main/java/com/example/Option.java
package com.example;

import lombok.Value;

public interface Option<T> {
    @Value
    class Some<T> implements Option<T> {
        T value;

        public <R> R match(CaseBlock<T, R> caseBlock) {
            return caseBlock._case(this);
        }
    }

    @Value
    class None<T> implements Option<T> {
        public <R> R match(CaseBlock<T, R> caseBlock) {
            return caseBlock._case(this);
        }
    }

    interface CaseBlock<T, R> {
        R _case(Some<T> some);
        R _case(None<T> none);
    }

    <R> R match(CaseBlock<T, R> caseBlock);
}
```

Option 型を使用するテストを書いてみる。パターンマッチングの動作確認以上の意味はないテストになる。

```java:src/test/java/com/example/OptionTypeTest.java
package com.example;

import org.junit.Test;

import static org.junit.Assert.*;

public class OptionTest {
    @Test
    public void testSomeType() {
        Option<Integer> some = new Option.Some<>(1);

        var actual = some.match(new Option.CaseBlock<>() {
            @Override
            public Integer _case(Option.Some<Integer> some) {
                return some.getValue();
            }

            @Override
            public Integer _case(Option.None<Integer> none) {
                return 0;
            }
        });

        assertEquals(1, actual);
    }

    @Test
    public void testNoneType() {
        Option<Integer> none = new Option.None<>();

        var actual = none.match(new Option.CaseBlock<>() {
            @Override
            public Integer _case(Option.Some<Integer> some) {
                return some.getValue();
            }

            @Override
            public Integer _case(Option.None<Integer> none) {
                return 0;
            }
        });

        assertEquals(0, actual);
    }
}
```

# map と flatMap できる Option 型を実装する

`map` メソッドと `flatMap` メソッドを下記のように実装する。

```java:src/main/java/com/example/Option.java
package com.example;

import lombok.Value;

import java.util.function.Function;

public interface Option<T> {
    @Value
    class Some<T> implements Option<T> {
        T value;

        public <R> R match(CaseBlock<T, R> caseBlock) {
            return caseBlock._case(this);
        }
    }

    @Value
    class None<T> implements Option<T> {
        public <R> R match(CaseBlock<T, R> caseBlock) {
            return caseBlock._case(this);
        }
    }

    interface CaseBlock<T, R> {
        R _case(Some<T> some);
        R _case(None<T> none);
    }

    <R> R match(CaseBlock<T, R> caseBlock);

    default <R> Option<R> map(Function<T, R> f) {
        return this.match(new CaseBlock<>() {
            @Override
            public Option<R> _case(Some<T> some) {
                return new Some<>(f.apply(some.getValue()));
            }

            @Override
            public Option<R> _case(None<T> none) {
                return new None<>();
            }
        });
    }

    default <R> Option<R> flatMap(Function<T, Option<R>> f) {
        return this.match(new CaseBlock<>() {
            @Override
            public Option<R> _case(Some<T> some) {
                return f.apply(some.getValue());
            }

            @Override
            public Option<R> _case(None<T> none) {
                return new None<>();
            }
        });
    }
}
```

Essential Scala に map と flatMap の違いについて次のような説明がある。

> We use map when we want to transform the value within the context to a new value, while keeping the context the same. We use flatMap when we want to transform the value and provide a new context. 

> map は、コンテキストに含まれる値を新しい値に変換したいときに使用し、その間も同じコンテキストを維持する。flatMap は、値を変換し、新しいコンテキストを与えたいときに使用する。

Option 型には Some と None という値のコンテキスト[^1]がある。map を適用すると Some は Some のまま、None は None のままになる。flatMap を適用すると Some は Some か None になり、None は None のままになる。map は失敗しない関数を適用でき、flatMap は失敗するかもしれない関数（結果型が Option 型になる関数）を適用できると考えるといいと思う。

Option 型の `map` メソッドと `flatMap` メソッドを使用するテストを書いてみよう。

ここで、`Option<Integer>` を返す、失敗するかもしれないメソッド、`mightFail1` メソッドと `mightFail2` メソッド、`mightFail3` メソッドがある。この3つのメソッドを用い、Scala の for 内包表記を意識した書き方で、`map` メソッドと `flatMap` メソッドをテストしてみる。

```java:src/test/java/com/example/OptionMapAndFlatMapTest.java
package com.example;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class OptionMapAndFlatMapTest {
    @Test
    public void testSomeResultOfMapAndFlatMap() {
        // for {
        //   a <- mightFail1
        //   b <- mightFail2
        // } yield a + b
        var actual = mightFail1().flatMap(a ->
                     mightFail2().map    (b ->
                     a + b));

        assertEquals(new Option.Some<>(3), actual);
    }

    @Test
    public void testNoneResultOfMap() {
        // for {
        //   a <- mightFail1
        //   b <- mightFail2
        //   c <- mightFail3
        // } yield a + b + c
        var actual = mightFail1().flatMap(a ->
                     mightFail2().flatMap(b ->
                     mightFail3().map    (c ->
                     a + b + c)));

        assertEquals(new Option.None<>(), actual);
    }

    @Test
    public void testNoneResultOfFlatMap() {
        // for {
        //   a <- mightFail3
        //   b <- mightFail2
        //   c <- mightFail1
        // } yield a + b + c
        var actual = mightFail3().flatMap(a ->
                     mightFail2().flatMap(b ->
                     mightFail1().map    (c ->
                     a + b + c)));

        assertEquals(new Option.None<>(), actual);
    }

    private Option<Integer> mightFail1() {
        return new Option.Some<>(1);
    }

    private Option<Integer> mightFail2() {
        return new Option.Some<>(2);
    }

    private Option<Integer> mightFail3() {
        return new Option.None<>();
    }
}
```

Option 型のコンテキスト[^2]の中で3つのメソッドを利用した処理を記述している。ここで重要なのは、「失敗するかもしれない」というコンテキストを意識せずに処理を書けていることだ。すべてのメソッドが成功した場合、ないしは、いずれかのメソッドが失敗した場合という場合分けを Option 型のコンテキストに押し付け、実現したい処理だけを書けていることになる。いわゆるモナドってやつに Option 型がなったわけなんだよね。たぶん。

---

今回は Java で Scala っぽい Option 型のパターンマッチングと map、flatMap を実現してみた。あくまで勉強目的の実験という感じで、実用性はないと思うのだけど、頭を整理するために記事にした。実用的なコードだと変性や型境界についての記述も必要になると思うしね。まあ何かの参考になれば幸い。

[^1]: Essential Scala では、Option 型における Some や None という値をコンテキストとして示している。正確な表現ではないと思うが、本稿では「値のコンテキスト」と呼称する。
[^2]: Option 型におけるモナドとしてのコンテキストを語るときは「失敗するかもしれない」というコンテキストを示している。正確な表現ではないと思うが、本稿では「型のコンテキスト」と呼称する。