From 6f00cf7f921a21a8c45f6d91deb99be817d97f28 Mon Sep 17 00:00:00 2001 From: Marco Cetica Date: Thu, 24 Oct 2024 09:24:19 +0200 Subject: [PATCH] Added flatMap/monad for the Either type --- README.md | 48 ++++++++++++++-- pom.xml | 4 +- .../com/ceticamarco/lambdatonic/Either.java | 52 +++++++++++++---- .../com/ceticamarco/lambdatonic/Left.java | 5 ++ .../com/ceticamarco/lambdatonic/Right.java | 5 ++ .../ceticamarco/lambdatonic/LeftTests.java | 56 +++++++++++++++++++ .../ceticamarco/lambdatonic/RightTests.java | 52 +++++++++++++++++ 7 files changed, 203 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 51a551c..1eb323c 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ you can install it either by using Maven: io.github.ceticamarco LambdaTonic - 0.0.3 + 0.0.4 ``` @@ -76,10 +76,10 @@ functionalities. ```java Either map(Function 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` type is instantiated -to the `Right` type. The `map` method adheres to the -functor laws(identity and composition of morphisms), which allows the `Either` 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` data type to be classified as a functor. ### Usage @@ -138,6 +138,44 @@ public class Main { } ``` +- `flatMap` + +### Description +```java + Either flatMap(Function> 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` data type to be classified as a monad. + +### Usage +```java +public class Main { + public static Either 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 numEither = new Right(10.0) + .flatMap(x -> division(x, 2.0)); + + + switch (numEither) { + case Left err -> { System.out.println(err.value().getMessage()); } + case Right val -> { System.out.printf("10 / 2 = %f\n", val.value()); } + } + } +} +``` + - `isLeft` ### Description diff --git a/pom.xml b/pom.xml index 42cd991..db00387 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ io.github.ceticamarco LambdaTonic jar - 0.0.3 + 0.0.4 @@ -51,7 +51,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.5.0 + 0.6.0 true central diff --git a/src/main/java/com/ceticamarco/lambdatonic/Either.java b/src/main/java/com/ceticamarco/lambdatonic/Either.java index 83db06c..5d3ac16 100644 --- a/src/main/java/com/ceticamarco/lambdatonic/Either.java +++ b/src/main/java/com/ceticamarco/lambdatonic/Either.java @@ -34,21 +34,25 @@ public sealed interface Either permits Left, Right { /** *

- * 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 Either data type, which supports + * the transformation of values through the map method. *

- * This method - * applies a function(fn) 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 Right subtype. Otherwise it leaves the functor - * unchanged. + * This method applies a function (fn) to the value inside the current instance + * if and only if the instance is a Right. The result of applying the function + * is then wrapped in a new Right and returned. If the instance is a Left, + * it is returned unchanged, without applying the function. *

- * The type of the resulting functor is the return type specified on the fn - * function + * The return type of this method depends on the return type of the fn function, + * ensuring that the result is still an Either functor with the same Left type. *

- * @param fn The function to applies to the Either data type - * @return An Either functor - * @param The return type of the fn function + * + * @param fn The function to apply if this instance is a Right; the function + * takes the current Right value and returns a transformed value. + * @return A new Either instance, which is a Right containing the result + * of applying the fn function to the current Right value, or the original + * Left if this instance is a Left. + * @param The type of the value inside the resulting Right after applying the + * fn function. */ Either map(Function fn); @@ -65,6 +69,30 @@ public sealed interface Either permits Left, Right { */ Either bimap(Function onLeft, Function onRight); + /** + *

+ * Defines the behavior of a monad for the Either data type, which supports + * the composition of computations through the flatMap method. + *

+ * This 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. If the instance is + * a Left, the original Left is propagated without applying + * the mapper function. + *

+ * The return type of this method depends on the return type of the mapper function, + * ensuring that the result is still an Either monad with the same Left type. + *

+ * @param mapper The function to apply if this instance is a Right; the function + * takes the current Right value and returns a new Either. + * @return A new Either instance, which is the result of applying the mapper + * function to the Right value, or the original Left if this instance + * is a Left. + * @param The type of the value inside the resulting Right after applying the + * mapper function. + */ + Either flatMap(Function> mapper); + /** *

* Returns the content of Right or a default value diff --git a/src/main/java/com/ceticamarco/lambdatonic/Left.java b/src/main/java/com/ceticamarco/lambdatonic/Left.java index d74faac..e7bf6c8 100644 --- a/src/main/java/com/ceticamarco/lambdatonic/Left.java +++ b/src/main/java/com/ceticamarco/lambdatonic/Left.java @@ -33,6 +33,11 @@ public record Left(L value) implements Either { return new Left<>(onLeft.apply(this.value)); } + @Override + public Either flatMap(Function> mapper) { + return new Left<>(this.value); + } + @Override public R fromRight(R defaultValue) { return defaultValue; diff --git a/src/main/java/com/ceticamarco/lambdatonic/Right.java b/src/main/java/com/ceticamarco/lambdatonic/Right.java index fe07414..86a554b 100644 --- a/src/main/java/com/ceticamarco/lambdatonic/Right.java +++ b/src/main/java/com/ceticamarco/lambdatonic/Right.java @@ -33,6 +33,11 @@ public record Right(R value) implements Either { return new Right<>(onRight.apply(this.value)); } + @Override + public Either flatMap(Function> mapper) { + return mapper.apply(this.value); + } + @Override public R fromRight(R defaultValue) { return this.value; diff --git a/src/test/java/com/ceticamarco/lambdatonic/LeftTests.java b/src/test/java/com/ceticamarco/lambdatonic/LeftTests.java index 8eeeefe..98b4664 100644 --- a/src/test/java/com/ceticamarco/lambdatonic/LeftTests.java +++ b/src/test/java/com/ceticamarco/lambdatonic/LeftTests.java @@ -52,6 +52,20 @@ public class LeftTests { } } + @Test + public void testMonadMapLeft() { + Function>> division = x -> y -> + y == 0 ? new Left<>(new Error("Cannot divide by zero")) : new Right<>(x / y); + + Either res = new Right(10) + .flatMap(x -> division.apply(x).apply(0)); + + switch (res) { + case Left left -> assertEquals(left.value().getMessage(), "Cannot divide by zero"); + case Right _ -> { } + } + } + @Test public void testFunctorBiMapLeft() { Function 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> f = x -> new Right<>(x + 1); + + // Create a new Left instance + Either leftValue = new Left<>(new Error("An error occurred")); + + // leftValue.flatmap(f) == leftValue + Either result = leftValue.flatMap(f); + + assertEquals(leftValue, result); + } + + @Test + public void testMonadRightIdentity() { + Either left = new Left<>(new Error("An error occurred")); + + // flatMap(m) == m + Either result = left.flatMap(Right::new); + + assertEquals(left, result); + } + + @Test + public void testMonadAssociativity() { + // f :: a -> Either e b + // g :: a -> Either e b + Function> f = x -> new Right<>(x + 1); + Function> g = x -> new Right<>(x * 2); + + // m.flatMap(f).flatmap(g) == m.flatMap(x -> f.apply(x).flatMap(g)) + Either leftSide = this.numEither.flatMap(f).flatMap(g); + Either 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); diff --git a/src/test/java/com/ceticamarco/lambdatonic/RightTests.java b/src/test/java/com/ceticamarco/lambdatonic/RightTests.java index 4bab9ff..7d32ec9 100644 --- a/src/test/java/com/ceticamarco/lambdatonic/RightTests.java +++ b/src/test/java/com/ceticamarco/lambdatonic/RightTests.java @@ -52,6 +52,20 @@ public class RightTests { } } + @Test + public void testMonadMapRight() { + Function>> division = x -> y -> + y == 0 ? new Left<>(new Error("Cannot divide by zero")) : new Right<>(x / y); + + Either res = new Right(10) + .flatMap(x -> division.apply(x).apply(5)); + + switch (res) { + case Left _ -> { } + case Right right -> assertEquals(right.value(), 2); + } + } + @Test public void testFunctorBiMapRight() { Function 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> f = x -> new Right<>(x + 1); + + // rightValue.flatMap(f) == f.apply(rightValue) + Either actual = new Right(10).flatMap(f); + Either expected = f.apply(10); + + assertEquals(expected, actual); + } + + @Test + public void testMonadRightIdentity() { + Either right = new Right<>(10); + + // flatMap(m) == m + Either result = right.flatMap(Right::new); + + assertEquals(right, result); + } + + @Test + public void testMonadAssociativity() { + // f :: a -> Either e b + // g :: a -> Either e b + Function> f = x -> new Right<>(x + 1); + Function> g = x -> new Right<>(x * 2); + + // m.flatMap(f).flatmap(g) == m.flatMap(x -> f.apply(x).flatMap(g)) + Either leftSide = this.numEither.flatMap(f).flatMap(g); + Either rightSide = this.numEither.flatMap(x -> f.apply(x).flatMap(g)); + + assertEquals(leftSide, rightSide); + } + @Test public void testFromRightOnRight() { assertEquals(this.numEither.fromRight(-1), 4);