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.
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 {
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 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 the
mightFail3method. Using these three methods, let's test the
map 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