From cc867bde82579e0f40348936dbbec4aed5c094bb Mon Sep 17 00:00:00 2001 From: Marco Cetica Date: Thu, 19 Sep 2024 14:53:01 +0200 Subject: [PATCH] Added documentation and CI support --- .gitea/workflows/LambdaTonic.yml | 17 + LICENSE | 21 ++ README.md | 297 ++++++++++++++++++ .../com/ceticamarco/lambdatonic/Either.java | 4 +- 4 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 .gitea/workflows/LambdaTonic.yml create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitea/workflows/LambdaTonic.yml b/.gitea/workflows/LambdaTonic.yml new file mode 100644 index 0000000..be57549 --- /dev/null +++ b/.gitea/workflows/LambdaTonic.yml @@ -0,0 +1,17 @@ +name: LambdaTonic +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Execute unit tests + run: | + dnf update -y + dnf install maven -y + mvn test diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7e1c2ee --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Cetica Marco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..888d578 --- /dev/null +++ b/README.md @@ -0,0 +1,297 @@ +# λTonic 🥃 ![](https://github.com/ceticamarco/lambdatonic/actions/workflows/LambdaTonic.yml/badge.svg) + +**λTonic**(_LambdaTonic_) is functional library designed for modern Java(+21). + +This library introduces a new algebraic data type called `Either`; +that is, an immutable sum type that _discriminates_ between two values, `Left` and `Right`, +representing the failure and the success values, respectively. + +The `Either` data type is implemented using a **sealed interface**, while the `Left` +and the `Right` are **record classes** that adopt the `Either` protocol. Both the `Left` +and the `Right` data types can be used inside a `switch` statement using Java pattern matching. + +## Overview +The `Either` algebraic data type can be used orthogonally over exceptions to propagate +an error from a function. Consider the following scenario: +```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) { + // Try to divide 15 by 3 + Either divResult = division(15, 3); + switch (divResult) { + case Left err -> System.err.println(err.value().getMessage()); + case Right val -> System.out.printf("15 / 3 = %f\n", val.value()); + } + + // Try to divide 2 by 0 + var div2Result = division(2, 0); + switch (div2Result) { + case Left err -> System.err.println(err.value().getMessage()); + case Right val -> System.out.printf("2 / 0 = %f\n", val.value()); + } + } +} +``` + +In this example we have defined a `division` method that takes two arguments and perform a division +on them. To handle the case when the `divisor` parameter is equal to zero, we return a new `Left` +instance of the `Either` type, while in any other case, we return a new `Right` instance of the +`Either` type. In the caller method(i.e., the `main`) we can then execute a custom statement using +Java's builtin pattern matching. + +## API Usage +The `Either` data type supports a broad spectrum of features, below there is a list of all supported +functionalities. + +- `map` + +### Description +```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 +to be classified as a functor. + +### Usage +The `map` method can be used to apply a computation to the value inside a functor: +```java +public class Main { + // ... + public static void main(String[] args) { + var resDivision = division(15, 3); + var resSquared = resDivision.map(x -> x * x); + + switch (resSquared) { + case Left err -> System.err.println(err.value().getMessage()); + case Right val -> System.out.println(val.value()); // prints 25.0 + } + } +} +``` + +- `bimap` + +### Description +```java + Either bimap(Function onLeft, Function onRight); +``` + +The `bimap` method applies the `onLeft` method to the `Left` subtype or the +`onRight` to the `Right`. + +### Usage +```java +public class Main { + public static void main(String[] args) { + var sc = new Scanner(System.in); + + // Read from stdin + System.out.print("Enter a divisor: "); + var input = sc.nextInt(); + + // Divide a fixed dividend by user input divisor + var divRes = division(15, input); + + // Apply a function regardless of the type of Either + // On the left we uppercase the error message + // On the right we square the result + var bimapRes = divRes.bimap( + err -> new Error(err.toString().toUpperCase()), + val -> val * val + ); + + switch (bimapRes) { + case Left err -> System.err.println(err.value().getMessage()); + case Right val -> System.out.println(val.value()); + } + } +} +``` + +- `isLeft` + +### Description +```java +boolean isLeft(); +``` + +`isLeft` returns true whether `Either` is instantiated to the `Left`, false otherwise + +### Usage +```java +public class Main { + public static void main(String[] args) { + var sc = new Scanner(System.in); + + // Read from stdin + System.out.print("Enter a divisor: "); + var input = sc.nextInt(); + + // Divide a fixed dividend by user input divisor + var divRes = division(15, input); + + if(divRes.isLeft()) { + System.out.println("Cannot divide by zero"); + } else { + System.out.println(divRes.fromRight(-1.0)); + } + } +} +``` + +- `isRight` + +### Description +```java +boolean isRight(); +``` + +`isRight` returns true whether `Either` is instantiated to the `Right`, false otherwise + +### Usage +```java +public class Main { + public static void main(String[] args) { + var sc = new Scanner(System.in); + + // Read from stdin + System.out.print("Enter a divisor: "); + var input = sc.nextInt(); + + // Divide a fixed dividend by user input divisor + var divRes = division(15, input); + + if(divRes.isRight()) { + System.out.println(divRes.fromRight(-1.0)); + } else { + System.out.println("Cannot divide by zero"); + } + } +} +``` + +- `fromLeft` + +### Description +```java +L fromLeft(L defaultValue); +``` + +`fromLeft` returns the content of the `Left` value or a default value + +### Usage +```java +public class Main { + public static void main(String[] args) { + var sc = new Scanner(System.in); + + // Read from stdin + System.out.print("Enter a divisor: "); + var input = sc.nextInt(); + + // Divide a fixed dividend by user input divisor + var divRes = division(15, input); + + // Prints out the error message or nothing + System.out.println(divRes.fromLeft(new Error("")).getMessage()); + } +} +``` + +- `fromRight` + +### Description +```java +L fromLeft(L defaultValue); +``` + +`fromRight` returns the content of the `Right` value or a default value + +### Usage +```java +public class Main { + public static void main(String[] args) { + var sc = new Scanner(System.in); + + // Read from stdin + System.out.print("Enter a divisor: "); + var input = sc.nextInt(); + + // Divide a fixed dividend by user input divisor + var divRes = division(15, input); + + // Prints out the actual value or nothing + System.out.println(divRes.fromRight(0.0)); + } +} +``` + +- `toOptional` + +### Description +```java +Optional toOptional(); +``` + +`toOptional` converts an `Either` data type to a `java.util.Optional`, +where the `Right` becomes a non-null `Optional` and the `Left` +becomes a null `Optional`. + +### Usage +```java +public class Main { + public static void main(String[] args) { + var sc = new Scanner(System.in); + + // Read from stdin + System.out.print("Enter a divisor: "); + var input = sc.nextInt(); + + // Divide a fixed dividend by user input divisor + var divRes = division(15, input).toOptional(); + + // Prints out the actual value or nothing + divRes.ifPresent(System.out::println); + } +} +``` + +- `swap` + +### Description +```java +Either swap(); +``` + +`swap` returns an `Either` type with `Left` and `Right` swapped. + +### Usage +```java +public class Main { + public static void main(String[] args) { + Either val = new Left<>("generic error"); + Either res = val.swap(); + + System.out.println(res.isLeft()); // Prints false + System.out.println(res.isRight()); // Prints true + } +} +``` + +## License +This software is released under the MIT license. +You can find a copy of the license with this repository or by visiting +the [following page](https://choosealicense.com/licenses/mit/). \ No newline at end of file diff --git a/src/main/java/com/ceticamarco/lambdatonic/Either.java b/src/main/java/com/ceticamarco/lambdatonic/Either.java index ad7feba..83db06c 100644 --- a/src/main/java/com/ceticamarco/lambdatonic/Either.java +++ b/src/main/java/com/ceticamarco/lambdatonic/Either.java @@ -39,8 +39,8 @@ public sealed interface Either permits Left, Right { *

* 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 the Either - * type is instantiated with the Right subtype. Otherwise it leaves the functor + * 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. *

* The type of the resulting functor is the return type specified on the fn