Added flatMap/monad for the Either type
All checks were successful
LambdaTonic / build (push) Successful in 1m7s
All checks were successful
LambdaTonic / build (push) Successful in 1m7s
This commit is contained in:
parent
91f7bc6c37
commit
6f00cf7f92
48
README.md
48
README.md
@ -57,7 +57,7 @@ you can install it either by using Maven:
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.ceticamarco</groupId>
|
<groupId>io.github.ceticamarco</groupId>
|
||||||
<artifactId>LambdaTonic</artifactId>
|
<artifactId>LambdaTonic</artifactId>
|
||||||
<version>0.0.3</version>
|
<version>0.0.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -76,10 +76,10 @@ functionalities.
|
|||||||
```java
|
```java
|
||||||
<T> Either<L, T> map(Function<R, T> fn);
|
<T> Either<L, T> map(Function<R, T> fn);
|
||||||
```
|
```
|
||||||
The `map` method applies a function(`fn`) to the values inside the data type,
|
|
||||||
returning a new data type if and only if the `Either<L, R>` type is instantiated
|
The `map` method applies a function (`fn`) to the value inside the current instance
|
||||||
to the `Right<R>` type. The `map` method adheres to the
|
if and only if the instance is a `Right`.
|
||||||
functor laws(identity and composition of morphisms), which allows the `Either<L, R>` data type
|
The `map` method adheres to the functor laws, which allow the `Either<L, R>` data type
|
||||||
to be classified as a functor.
|
to be classified as a functor.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
@ -138,6 +138,44 @@ public class Main {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `flatMap`
|
||||||
|
|
||||||
|
### Description
|
||||||
|
```java
|
||||||
|
<T> Either<L, T> flatMap(Function<R, Either<L, T>> mapper);
|
||||||
|
```
|
||||||
|
The `flatMap` method applies a function (`mapper`) that returns a new `Either`
|
||||||
|
to the value inside the current instance, and it returns the resulting
|
||||||
|
`Either` only if the instance is a `Right`. The `flatMap` method adheres to the monad laws, which
|
||||||
|
allow the `Either<L, R>` data type to be classified as a monad.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
```java
|
||||||
|
public class Main {
|
||||||
|
public static Either<Error, Double> division(double dividend, double divisor) {
|
||||||
|
// Return an error whether the divisor is zero
|
||||||
|
if(divisor == 0) {
|
||||||
|
return new Left<>(new Error("Cannot divide by zero"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise return the result of the division
|
||||||
|
return new Right<>(dividend / divisor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// Apply a function that returns a monad to a monad, returns a monad
|
||||||
|
Either<Error, Double> numEither = new Right<Error, Double>(10.0)
|
||||||
|
.flatMap(x -> division(x, 2.0));
|
||||||
|
|
||||||
|
|
||||||
|
switch (numEither) {
|
||||||
|
case Left<Error, Double> err -> { System.out.println(err.value().getMessage()); }
|
||||||
|
case Right<Error, Double> val -> { System.out.printf("10 / 2 = %f\n", val.value()); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- `isLeft`
|
- `isLeft`
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
|
4
pom.xml
4
pom.xml
@ -7,7 +7,7 @@
|
|||||||
<groupId>io.github.ceticamarco</groupId>
|
<groupId>io.github.ceticamarco</groupId>
|
||||||
<artifactId>LambdaTonic</artifactId>
|
<artifactId>LambdaTonic</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<version>0.0.3</version>
|
<version>0.0.4</version>
|
||||||
|
|
||||||
<developers>
|
<developers>
|
||||||
<developer>
|
<developer>
|
||||||
@ -51,7 +51,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.sonatype.central</groupId>
|
<groupId>org.sonatype.central</groupId>
|
||||||
<artifactId>central-publishing-maven-plugin</artifactId>
|
<artifactId>central-publishing-maven-plugin</artifactId>
|
||||||
<version>0.5.0</version>
|
<version>0.6.0</version>
|
||||||
<extensions>true</extensions>
|
<extensions>true</extensions>
|
||||||
<configuration>
|
<configuration>
|
||||||
<publishingServerId>central</publishingServerId>
|
<publishingServerId>central</publishingServerId>
|
||||||
|
@ -34,21 +34,25 @@ public sealed interface Either<L, R> permits Left, Right {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* Defines a functor. That is, a data type that supports
|
* Defines the behavior of a functor for the <i>Either</i> data type, which supports
|
||||||
* a mapping operation defined by the map method.
|
* the transformation of values through the <i>map</i> method.
|
||||||
* <br /><br />
|
* <br /><br />
|
||||||
* This method
|
* This method applies a function (<i>fn</i>) to the value inside the current instance
|
||||||
* applies a function(<i>fn</i>) to the values inside the data type,
|
* if and only if the instance is a <i>Right</i>. The result of applying the function
|
||||||
* returning a new data type(i.e., a new functor) if and only if the Either
|
* is then wrapped in a new <i>Right</i> and returned. If the instance is a <i>Left</i>,
|
||||||
* type is instantiated to the <i>Right</i> subtype. Otherwise it leaves the functor
|
* it is returned unchanged, without applying the function.
|
||||||
* unchanged.
|
|
||||||
* <br /><br />
|
* <br /><br />
|
||||||
* The type of the resulting functor is the return type specified on the <i>fn</i>
|
* The return type of this method depends on the return type of the <i>fn</i> function,
|
||||||
* function
|
* ensuring that the result is still an <i>Either</i> functor with the same <i>Left</i> type.
|
||||||
* </p>
|
* </p>
|
||||||
* @param fn The function to applies to the Either data type
|
*
|
||||||
* @return An <i>Either</i> functor
|
* @param fn The function to apply if this instance is a <i>Right</i>; the function
|
||||||
* @param <T> The return type of the <i>fn</i> function
|
* takes the current <i>Right</i> value and returns a transformed value.
|
||||||
|
* @return A new <i>Either</i> instance, which is a <i>Right</i> containing the result
|
||||||
|
* of applying the <i>fn</i> function to the current <i>Right</i> value, or the original
|
||||||
|
* <i>Left</i> if this instance is a <i>Left</i>.
|
||||||
|
* @param <T> The type of the value inside the resulting <i>Right</i> after applying the
|
||||||
|
* <i>fn</i> function.
|
||||||
*/
|
*/
|
||||||
<T> Either<L, T> map(Function<R, T> fn);
|
<T> Either<L, T> map(Function<R, T> fn);
|
||||||
|
|
||||||
@ -65,6 +69,30 @@ public sealed interface Either<L, R> permits Left, Right {
|
|||||||
*/
|
*/
|
||||||
<T, K> Either<T, K> bimap(Function<L, T> onLeft, Function<R, K> onRight);
|
<T, K> Either<T, K> bimap(Function<L, T> onLeft, Function<R, K> onRight);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Defines the behavior of a monad for the <i>Either</i> data type, which supports
|
||||||
|
* the composition of computations through the <i>flatMap</i> method.
|
||||||
|
* <br /><br />
|
||||||
|
* This method applies a function (<i>mapper</i>) that returns a new <i>Either</i>
|
||||||
|
* to the value inside the current instance, and it returns the resulting
|
||||||
|
* <i>Either</i> only if the instance is a <i>Right</i>. If the instance is
|
||||||
|
* a <i>Left</i>, the original <i>Left</i> is propagated without applying
|
||||||
|
* the <i>mapper</i> function.
|
||||||
|
* <br /><br />
|
||||||
|
* The return type of this method depends on the return type of the <i>mapper</i> function,
|
||||||
|
* ensuring that the result is still an <i>Either</i> monad with the same <i>Left</i> type.
|
||||||
|
* </p>
|
||||||
|
* @param mapper The function to apply if this instance is a <i>Right</i>; the function
|
||||||
|
* takes the current <i>Right</i> value and returns a new <i>Either</i>.
|
||||||
|
* @return A new <i>Either</i> instance, which is the result of applying the <i>mapper</i>
|
||||||
|
* function to the <i>Right</i> value, or the original <i>Left</i> if this instance
|
||||||
|
* is a <i>Left</i>.
|
||||||
|
* @param <T> The type of the value inside the resulting <i>Right</i> after applying the
|
||||||
|
* <i>mapper</i> function.
|
||||||
|
*/
|
||||||
|
<T> Either<L, T> flatMap(Function<R, Either<L, T>> mapper);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* Returns the content of <i>Right</i> or a default value
|
* Returns the content of <i>Right</i> or a default value
|
||||||
|
@ -33,6 +33,11 @@ public record Left<L, R>(L value) implements Either<L, R> {
|
|||||||
return new Left<>(onLeft.apply(this.value));
|
return new Left<>(onLeft.apply(this.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Either<L, T> flatMap(Function<R, Either<L, T>> mapper) {
|
||||||
|
return new Left<>(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R fromRight(R defaultValue) {
|
public R fromRight(R defaultValue) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
|
@ -33,6 +33,11 @@ public record Right<L, R>(R value) implements Either<L, R> {
|
|||||||
return new Right<>(onRight.apply(this.value));
|
return new Right<>(onRight.apply(this.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Either<L, T> flatMap(Function<R, Either<L, T>> mapper) {
|
||||||
|
return mapper.apply(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R fromRight(R defaultValue) {
|
public R fromRight(R defaultValue) {
|
||||||
return this.value;
|
return this.value;
|
||||||
|
@ -52,6 +52,20 @@ public class LeftTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMonadMapLeft() {
|
||||||
|
Function<Integer, Function<Integer, Either<Error, Integer>>> division = x -> y ->
|
||||||
|
y == 0 ? new Left<>(new Error("Cannot divide by zero")) : new Right<>(x / y);
|
||||||
|
|
||||||
|
Either<Error, Integer> res = new Right<Error, Integer>(10)
|
||||||
|
.flatMap(x -> division.apply(x).apply(0));
|
||||||
|
|
||||||
|
switch (res) {
|
||||||
|
case Left<Error, Integer> left -> assertEquals(left.value().getMessage(), "Cannot divide by zero");
|
||||||
|
case Right<Error, Integer> _ -> { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFunctorBiMapLeft() {
|
public void testFunctorBiMapLeft() {
|
||||||
Function<Integer, Integer> f = x -> x + 1;
|
Function<Integer, Integer> f = x -> x + 1;
|
||||||
@ -62,6 +76,7 @@ public class LeftTests {
|
|||||||
assertEquals(actual.fromLeft(-1), 20);
|
assertEquals(actual.fromLeft(-1), 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Functor Laws
|
||||||
@Test
|
@Test
|
||||||
public void testLeftFunctorIdentityMorphism() {
|
public void testLeftFunctorIdentityMorphism() {
|
||||||
// Applying the map function with the identity function,
|
// Applying the map function with the identity function,
|
||||||
@ -86,6 +101,47 @@ public class LeftTests {
|
|||||||
assertEquals(composition, FGMapped);
|
assertEquals(composition, FGMapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Monad Laws
|
||||||
|
@Test
|
||||||
|
public void testMonadLeftIdentity() {
|
||||||
|
// f :: a -> Either e b
|
||||||
|
Function<Integer, Either<Error, Integer>> f = x -> new Right<>(x + 1);
|
||||||
|
|
||||||
|
// Create a new Left instance
|
||||||
|
Either<Error, Integer> leftValue = new Left<>(new Error("An error occurred"));
|
||||||
|
|
||||||
|
// leftValue.flatmap(f) == leftValue
|
||||||
|
Either<Error, Integer> result = leftValue.flatMap(f);
|
||||||
|
|
||||||
|
assertEquals(leftValue, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMonadRightIdentity() {
|
||||||
|
Either<Error, Integer> left = new Left<>(new Error("An error occurred"));
|
||||||
|
|
||||||
|
// flatMap(m) == m
|
||||||
|
Either<Error, Integer> result = left.flatMap(Right::new);
|
||||||
|
|
||||||
|
assertEquals(left, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMonadAssociativity() {
|
||||||
|
// f :: a -> Either e b
|
||||||
|
// g :: a -> Either e b
|
||||||
|
Function<Integer, Either<Error, Integer>> f = x -> new Right<>(x + 1);
|
||||||
|
Function<Integer, Either<Error, Integer>> g = x -> new Right<>(x * 2);
|
||||||
|
|
||||||
|
// m.flatMap(f).flatmap(g) == m.flatMap(x -> f.apply(x).flatMap(g))
|
||||||
|
Either<Error, Integer> leftSide = this.numEither.flatMap(f).flatMap(g);
|
||||||
|
Either<Error, Integer> rightSide = this.numEither.flatMap(x -> f.apply(x).flatMap(g));
|
||||||
|
|
||||||
|
// In both cases, the Left should be left untouched without invoking neither `f` nor `g`
|
||||||
|
assertEquals(this.numEither, leftSide);
|
||||||
|
assertEquals(this.numEither, rightSide);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFromRightOnLeft() {
|
public void testFromRightOnLeft() {
|
||||||
assertEquals(this.numEither.fromRight(-1), -1);
|
assertEquals(this.numEither.fromRight(-1), -1);
|
||||||
|
@ -52,6 +52,20 @@ public class RightTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMonadMapRight() {
|
||||||
|
Function<Integer, Function<Integer, Either<Error, Integer>>> division = x -> y ->
|
||||||
|
y == 0 ? new Left<>(new Error("Cannot divide by zero")) : new Right<>(x / y);
|
||||||
|
|
||||||
|
Either<Error, Integer> res = new Right<Error, Integer>(10)
|
||||||
|
.flatMap(x -> division.apply(x).apply(5));
|
||||||
|
|
||||||
|
switch (res) {
|
||||||
|
case Left<Error, Integer> _ -> { }
|
||||||
|
case Right<Error, Integer> right -> assertEquals(right.value(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFunctorBiMapRight() {
|
public void testFunctorBiMapRight() {
|
||||||
Function<Integer, Integer> f = x -> x + 1;
|
Function<Integer, Integer> f = x -> x + 1;
|
||||||
@ -62,6 +76,7 @@ public class RightTests {
|
|||||||
assertEquals(actual.fromRight("-1"), "QUERY EXECUTED SUCCESSFULLY");
|
assertEquals(actual.fromRight("-1"), "QUERY EXECUTED SUCCESSFULLY");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Functor Laws
|
||||||
@Test
|
@Test
|
||||||
public void testRightFunctorIdentityMorphism() {
|
public void testRightFunctorIdentityMorphism() {
|
||||||
// Applying the map function with the identity function,
|
// Applying the map function with the identity function,
|
||||||
@ -86,6 +101,43 @@ public class RightTests {
|
|||||||
assertEquals(composition, FGMapped);
|
assertEquals(composition, FGMapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Monad Laws
|
||||||
|
@Test
|
||||||
|
public void testMonadLeftIdentity() {
|
||||||
|
// f :: a -> Either e b
|
||||||
|
Function<Integer, Either<Error, Integer>> f = x -> new Right<>(x + 1);
|
||||||
|
|
||||||
|
// rightValue.flatMap(f) == f.apply(rightValue)
|
||||||
|
Either<Error, Integer> actual = new Right<Error, Integer>(10).flatMap(f);
|
||||||
|
Either<Error, Integer> expected = f.apply(10);
|
||||||
|
|
||||||
|
assertEquals(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMonadRightIdentity() {
|
||||||
|
Either<Error, Integer> right = new Right<>(10);
|
||||||
|
|
||||||
|
// flatMap(m) == m
|
||||||
|
Either<Error, Integer> result = right.flatMap(Right::new);
|
||||||
|
|
||||||
|
assertEquals(right, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMonadAssociativity() {
|
||||||
|
// f :: a -> Either e b
|
||||||
|
// g :: a -> Either e b
|
||||||
|
Function<Integer, Either<Error, Integer>> f = x -> new Right<>(x + 1);
|
||||||
|
Function<Integer, Either<Error, Integer>> g = x -> new Right<>(x * 2);
|
||||||
|
|
||||||
|
// m.flatMap(f).flatmap(g) == m.flatMap(x -> f.apply(x).flatMap(g))
|
||||||
|
Either<Error, Integer> leftSide = this.numEither.flatMap(f).flatMap(g);
|
||||||
|
Either<Error, Integer> rightSide = this.numEither.flatMap(x -> f.apply(x).flatMap(g));
|
||||||
|
|
||||||
|
assertEquals(leftSide, rightSide);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFromRightOnRight() {
|
public void testFromRightOnRight() {
|
||||||
assertEquals(this.numEither.fromRight(-1), 4);
|
assertEquals(this.numEither.fromRight(-1), 4);
|
||||||
|
Loading…
Reference in New Issue
Block a user