Try to realize Scala-like Option type pattern matching and map, flatMap in Java

I've experimented with what would happen if I implemented Scala's Option type in Java, so make a note of it. Evolved forms of enums and switch statements! ?? Trying to achieve algebraic data types and pattern matching in Java.

Generate an experimental Gradle project

I want to use Lombok, so I will experiment with the Gradle project instead of JShell. Let's generate a Gradle project with the following command.

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

Update the build.gradle file to install Lombok.

--- 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 {

Implement Option type that can pattern match

Let's implement Option type in Java as follows. The Sealed Class Pattern is not used for better visibility.

There is a type T held by the Some class and a result type R of pattern matching, which was quite confusing during implementation. Those who read the code should be careful.

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);
}

Write a test that uses the Option type. It is a test that has no meaning other than checking the operation of pattern matching.

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);
    }
}

Implement Option type that can map and flatMap

Implement the map method and the flatMap method as follows.

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 explains the difference between map and flatMap as follows:

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.

Use map when you want to convert a value contained in a context to a new value, and keep the same context in the meantime. flatMap is used when you want to convert a value and give it a new context.

The Option type has a context [^ 1] with values Some and None. When you apply the map, Some remains Some and None remains None. When flatMap is applied, Some becomes Some or None and None remains None. Think of map as being able to apply functions that don't fail, and flatMap being able to apply functions that might fail (functions that result in an Option type).

Let's write a test that uses the Option type map and flatMap methods.

Here are the methods that may fail, which return ʻOption , the mightFail1 method, the mightFail2method, and themightFail3method. Using these three methods, let's test themap method and the flatMap` method in a way that is conscious of Scala's for inclusion notation.

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<>();
    }
}

The process using three methods is described in the context of Option type [^ 2]. The important thing here is that you can write the process without being aware of the context of "may fail". If all the methods succeed, or if one of the methods fails, the case is imposed on the Option type context, and only the processing that you want to realize can be written. The so-called monad has become an Option type. maybe.


This time, I tried to realize Scala-like Option type pattern matching, map, and flatMap in Java. It's just an experiment for study purposes, and I don't think it's practical, but I wrote this article to keep my mind in order. I think that practical code would require a description of denaturation and type boundaries. I hope it will be helpful for you.

[^ 1]: Essential Scala shows the values Some and None in Option type as context. I don't think it's an exact representation, but in this article we call it the "value context." [^ 2]: When talking about the context as a monad in Option type, it indicates the context that "may fail". I don't think it's an exact representation, but in this article we call it "type context".

Recommended Posts

Try to realize Scala-like Option type pattern matching and map, flatMap in Java
Difference between Stream map and flatMap
Toward understanding of map and flatmap in Stream (1)
Use swift Filter and Map
[Ruby] Simplify each using map and inject
Try to realize Scala-like Option type pattern matching and map, flatMap in Java
Try functional type in Java! ①
Try to implement Yubaba in Java
[Android] Convert Map to JSON using GSON in Kotlin and Java
Try to solve Project Euler in Java
Try to implement n-ary addition in Java
Try to create a bulletin board in Java
Try to link Ruby and Java with Dapr
Toward understanding of map and flatmap in Stream (1)
[Java] How to get the key and value stored in Map by iterative processing
Java classes and instances to understand in the figure
[Java] Things to note about type inference extended in Java 10
How to convert A to a and a to A using AND and OR in Java
Try to solve a restricted FizzBuzz problem in Java
Gzip-compress byte array in Java and output to file
Type determination in Java
Store in Java 2D map and turn with for statement
Introduction to Effective java by practicing and learning (Builder pattern)
Implement Thread in Java and try using anonymous class, lambda
[Java] Change language and locale to English in JVM options
What happened in "Java 8 to Java 11" and how to build an environment
How to call and use API in Java (Spring Boot)
Reasons to use Servlet and JSP separately in Java development
I tried to convert a string to a LocalDate type in Java
How to develop and register a Sota app in Java
Differences in how to handle strings between Java and Perl