Added flatMap/monad for the Either type
All checks were successful
LambdaTonic / build (push) Successful in 1m7s

This commit is contained in:
Marco Cetica 2024-10-24 09:24:19 +02:00
parent 91f7bc6c37
commit 6f00cf7f92
Signed by: marco
GPG Key ID: 45060A949E90D0FD
7 changed files with 203 additions and 19 deletions

View File

@ -57,7 +57,7 @@ you can install it either by using Maven:
<dependency>
<groupId>io.github.ceticamarco</groupId>
<artifactId>LambdaTonic</artifactId>
<version>0.0.3</version>
<version>0.0.4</version>
</dependency>
```
@ -76,10 +76,10 @@ functionalities.
```java
<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
to the `Right<R>` type. The `map` method adheres to the
functor laws(identity and composition of morphisms), which allows the `Either<L, R>` data type
The `map` method applies a function (`fn`) to the value inside the current instance
if and only if the instance is a `Right`.
The `map` method adheres to the functor laws, which allow the `Either<L, R>` data type
to be classified as a functor.
### 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`
### Description

View File

@ -7,7 +7,7 @@
<groupId>io.github.ceticamarco</groupId>
<artifactId>LambdaTonic</artifactId>
<packaging>jar</packaging>
<version>0.0.3</version>
<version>0.0.4</version>
<developers>
<developer>
@ -51,7 +51,7 @@
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.5.0</version>
<version>0.6.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>central</publishingServerId>

View File

@ -34,21 +34,25 @@ public sealed interface Either<L, R> permits Left, Right {
/**
* <p>
* Defines a functor. That is, a data type that supports
* a mapping operation defined by the map method.
* Defines the behavior of a functor for the <i>Either</i> data type, which supports
* the transformation of values through the <i>map</i> method.
* <br /><br />
* This method
* applies a function(<i>fn</i>) to the values inside the data type,
* returning a new data type(i.e., a new functor) if and only if the Either
* type is instantiated to the <i>Right</i> subtype. Otherwise it leaves the functor
* unchanged.
* This method applies a function (<i>fn</i>) to the value inside the current instance
* if and only if the instance is a <i>Right</i>. The result of applying the function
* is then wrapped in a new <i>Right</i> and returned. If the instance is a <i>Left</i>,
* it is returned unchanged, without applying the function.
* <br /><br />
* The type of the resulting functor is the return type specified on the <i>fn</i>
* function
* The return type of this method depends on the return type of the <i>fn</i> function,
* ensuring that the result is still an <i>Either</i> functor with the same <i>Left</i> type.
* </p>
* @param fn The function to applies to the Either data type
* @return An <i>Either</i> functor
* @param <T> The return type of the <i>fn</i> function
*
* @param fn The function to apply if this instance is a <i>Right</i>; the 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);
@ -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);
/**
* <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>
* Returns the content of <i>Right</i> or a default value

View File

@ -33,6 +33,11 @@ public record Left<L, R>(L value) implements Either<L, R> {
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
public R fromRight(R defaultValue) {
return defaultValue;

View File

@ -33,6 +33,11 @@ public record Right<L, R>(R value) implements Either<L, R> {
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
public R fromRight(R defaultValue) {
return this.value;

View File

@ -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
public void testFunctorBiMapLeft() {
Function<Integer, Integer> f = x -> x + 1;
@ -62,6 +76,7 @@ public class LeftTests {
assertEquals(actual.fromLeft(-1), 20);
}
// Functor Laws
@Test
public void testLeftFunctorIdentityMorphism() {
// Applying the map function with the identity function,
@ -86,6 +101,47 @@ public class LeftTests {
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
public void testFromRightOnLeft() {
assertEquals(this.numEither.fromRight(-1), -1);

View File

@ -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
public void testFunctorBiMapRight() {
Function<Integer, Integer> f = x -> x + 1;
@ -62,6 +76,7 @@ public class RightTests {
assertEquals(actual.fromRight("-1"), "QUERY EXECUTED SUCCESSFULLY");
}
// Functor Laws
@Test
public void testRightFunctorIdentityMorphism() {
// Applying the map function with the identity function,
@ -86,6 +101,43 @@ public class RightTests {
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
public void testFromRightOnRight() {
assertEquals(this.numEither.fromRight(-1), 4);