{"id":16321597,"url":"https://github.com/joselion/maybe","last_synced_at":"2025-03-20T22:31:17.227Z","repository":{"id":37833947,"uuid":"243691624","full_name":"JoseLion/maybe","owner":"JoseLion","description":"A monadic wrapper with a type-safe API to handle throwing operations in a functional way","archived":false,"fork":false,"pushed_at":"2024-07-13T19:59:19.000Z","size":7411,"stargazers_count":13,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-16T15:11:51.431Z","etag":null,"topics":["error-handling","exception-handling","functional-java","functional-programming","hacktoberfest","hacktoberfest2022","java","maybe","monad"],"latest_commit_sha":null,"homepage":"https://joselion.github.io/maybe/","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JoseLion.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"JoseLion"}},"created_at":"2020-02-28T06:12:32.000Z","updated_at":"2025-01-03T08:23:46.000Z","dependencies_parsed_at":"2024-01-01T00:38:23.790Z","dependency_job_id":"142d47b1-b099-4ee7-a46e-37ad5b0fcc27","html_url":"https://github.com/JoseLion/maybe","commit_stats":null,"previous_names":[],"tags_count":31,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JoseLion%2Fmaybe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JoseLion%2Fmaybe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JoseLion%2Fmaybe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JoseLion%2Fmaybe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JoseLion","download_url":"https://codeload.github.com/JoseLion/maybe/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244135898,"owners_count":20403797,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["error-handling","exception-handling","functional-java","functional-programming","hacktoberfest","hacktoberfest2022","java","maybe","monad"],"created_at":"2024-10-10T22:48:17.788Z","updated_at":"2025-03-20T22:31:16.819Z","avatar_url":"https://github.com/JoseLion.png","language":"Java","funding_links":["https://github.com/sponsors/JoseLion"],"categories":[],"sub_categories":[],"readme":"[![CI](https://github.com/JoseLion/maybe/actions/workflows/ci.yml/badge.svg)](https://github.com/JoseLion/maybe/actions/workflows/ci.yml)\r\n[![CodeQL](https://github.com/JoseLion/maybe/actions/workflows/codeql.yml/badge.svg)](https://github.com/JoseLion/maybe/actions/workflows/codeql.yml)\r\n[![Release](https://github.com/JoseLion/maybe/actions/workflows/release.yml/badge.svg)](https://github.com/JoseLion/maybe/actions/workflows/release.yml)\r\n[![Pages](https://github.com/JoseLion/maybe/actions/workflows/pages.yml/badge.svg)](https://github.com/JoseLion/maybe/actions/workflows/pages.yml)\r\n[![Maven Central](https://img.shields.io/maven-central/v/io.github.joselion/maybe?logo=sonatype)](https://central.sonatype.com/artifact/io.github.joselion/maybe)\r\n[![javadoc](https://javadoc.io/badge2/io.github.joselion/maybe/javadoc.svg)](https://javadoc.io/doc/io.github.joselion/maybe)\r\n[![codecov](https://codecov.io/gh/JoseLion/maybe/branch/main/graph/badge.svg)](https://codecov.io/gh/JoseLion/maybe)\r\n[![License](https://img.shields.io/github/license/JoseLion/maybe)](https://github.com/JoseLion/maybe/blob/main/LICENSE)\r\n[![Known Vulnerabilities](https://snyk.io/test/github/JoseLion/maybe/badge.svg)](https://snyk.io/test/github/JoseLion/maybe)\r\n\r\n# Maybe - Safely handle exceptions\r\n\r\n`Maybe\u003cT\u003e` is a monadic wrapper similar to `java.util.Optional`, but with a different intention. By leveraging `Optional\u003cT\u003e` benefits, it provides a functional API that safely allows to work with operations that throw checked (and unchecked) exceptions.\r\n\r\nThe motivation of `Maybe\u003cT\u003e` is to help developers avoid imperative _try/catch_ blocks while promoting safe exception handling which lives by functional programming principles.\r\n\r\n## Features\r\n\r\n* Type-safe differentiation between resolving a value vs. runnning effects.\r\n* Rich and intuitive API based on `java.util.Optional`.\r\n* Full interoperability with `java.util.Optional`.\r\n* Includes a safe `Either\u003cL, R\u003e` type where only one side can be present at a time.\r\n* Method reference friendly - The API provides methods with overloads that makes it easier to use [method reference](https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html) syntax.\r\n\r\n## Presentations\r\n\r\n- [JCon 2021 - Handling exception, the functional way](https://youtu.be/vaRjOukcIDA)\r\n\r\n## Compatibility\r\n\r\nAs of **`v3.3.0`**, this library is compatible with JDK11+ by using [Multi-Release JARs](https://openjdk.org/jeps/238). However, using at least JDK17 to enjoy the new Java enhancements and features is highly recommended.\r\n\r\nFor example, the JDK17+ version of `Either\u003cL, R\u003e` uses a combination of [sealed classes](https://docs.oracle.com/en/java/javase/17/language/sealed-classes-and-interfaces.html) and [records classes](https://docs.oracle.com/en/java/javase/17/language/records.html), effectively making it an [algebraic data type](https://en.wikipedia.org/wiki/Algebraic_data_type) in Java. It means that `Either\u003cL, R\u003e` is an interface no other class can implement. The only implementations are the `Left` and `Right` records, which live within the interface. In short, it's a composite type created by combining other types. \r\n\r\n\u003e If you need a JDK8-compatible version of `Maybe\u003cT\u003e`, you can use v2 instead. However, much like Java 8, v2 has reached its end-of-life, so it will not get any more features, patches, or updates.\r\n\r\n## Breaking Changes (from v2 to v3)\r\n\r\n- Due to changes on GitHub policies (and by consequence on Maven), it's no longer allowed to use `com.github` as a valid group ID prefix. To honor that and maintain consistency, **as of v3**, the artifact ID was renamed to `io.github.joselion.maybe`. If you want to use a version **before v3**, you can still find it using `com.github.joselion:maybe` artifact.\r\n- A `SolveHandler` can no longer be empty. It either has the solved value or an error.\r\n- The method `SolveHandler#filter` was removed to avoid the posibility of an inconsitent empty handler.\r\n- The `WrapperException` type was removed. Errors now propagate downstream with the API.\r\n- The method `EffectHandler#toMaybe` was removed as it didn't make sense for effects.\r\n- All `*Checked.java` functions were renamed to `Throwing*.java`\r\n  - For example, `FunctionChecked\u003cT, R, E\u003e` was renamed to `ThrowingFunction\u003cT, R, E\u003e`\r\n\r\n## Install\r\n\r\n[![Maven Central](https://img.shields.io/maven-central/v/io.github.joselion/maybe?logo=sonatype)](https://central.sonatype.com/artifact/io.github.joselion/maybe)\r\n\r\nMaybe is available in [Maven Central](https://central.sonatype.com/artifact/io.github.joselion/maybe). You can checkout the latest version with the badge above.\r\n\r\n**Gradle**\r\n\r\n```gradle\r\nimplementation('io.github.joselion:maybe:x.y.z')\r\n```\r\n\r\n**Maven**\r\n\r\n```xml\r\n\u003cdependency\u003e\r\n  \u003cgroupId\u003eio.github.joselion\u003c/groupId\u003e\r\n  \u003cartifactId\u003emaybe\u003c/artifactId\u003e\r\n  \u003cversion\u003ex.y.z\u003c/version\u003e\r\n\u003c/dependency\u003e\r\n```\r\n\r\n## Basics\r\n\r\nWe'd use `Maybe\u003cT\u003e` for 3 different cases:\r\n- **Solve:** When we need to obtain a value from a throwing operation.\r\n- **Effects:** When we need to run effects that may throw exception(s), so no value is returned.\r\n- **Closeables:** When we need to use a closeable resources on another operation (as in `try-with-resource` blocks)\r\n\r\nWe can create simple instances of Maybe using `Maybe.of(value)` or `Maybe.empty()` so we can chain throwing operations to it that will create the **handlers**. We also provide the convenience static methods `.from(ThrowingSupplier)` and `.from(ThrowingRunnable)` to create **handlers** directly from lambda expressions. Given the built-in lambda expression do not allow checked exception, we provide a few basic functional interfaces like `ThrowingFunction\u003cT, R, E\u003e`, that are just like the built-in ones, but with a `throws E` declaration. You can find them all in the [util packages][util-package-ref] of the library.\r\n\r\n### Solve handler\r\n\r\nOnce a solver operation runs we'll get a [SolveHandler][solve-handler-ref] instance. This is the API that lets you handle the possible exception and produce a final value, or chain more operations to it.\r\n\r\n```java\r\nfinal Path path = Paths.get(\"foo.txt\");\r\n\r\nfinal List\u003cString\u003e fooLines = Maybe.from(() -\u003e Files.readAllLines(path))\r\n  .doOnError(error -\u003e log.error(\"Fail to read the file\", error)) // where `error` has type IOException\r\n  .orElse(List.of());\r\n\r\n// or we could use method reference\r\n\r\nfinal List\u003cString\u003e fooLines = Maybe.of(path)\r\n  .solve(Files::readAllLines)\r\n  .doOnError(error -\u003e log.error(\"Fail to read the file\", error)) // where `error` has type IOException\r\n  .orElseGet(List::of); // the else value is lazy now\r\n```\r\n\r\nThe method `.readAllLines(..)` on example above reads from a file, which may throw a `IOException`. With the solver API we can run an effect if the exception was thrown. The we use `.orElse(..)` to safely unwrap the resulting value or another one in case of failure.\r\n\r\n### Effect handler\r\n\r\nWhen an effect operation runs we'll get a [EffectHandler][effect-handler-ref] instences. Likewise, this is the API to handle any possinble exception the effect may throw. This handler is very similar to the [SolveHandler][solve-handler-ref], but given an effect will never solve a value, it does not have any of the methods related to manipulating or unwrapping the value.\r\n\r\n```java\r\nMaybe\r\n  .from(() -\u003e {\r\n    final String to = ...\r\n    final String from = ...\r\n    final String message = ...\r\n    \r\n    MailService.send(message, to, from);\r\n  })\r\n  .doOnError(error -\u003e { // the `error` has type `MessagingException`\r\n    MailService.report(error.getMessage());\r\n  });\r\n```\r\n\r\nIn the example above the `.send(..)` methods may throw a `MessagingException`. With the effect API we handle the error running another effect, i.e. reporting the error to another service.\r\n\r\n### Closeable handler\r\n\r\nMaybe also offers a way to work with [AutoCloseable](https://docs.oracle.com/javase/8/docs/api/java/lang/AutoCloseable.html) resources in a similar way the `try-with-resource` statement does, but with a more functional approach. We do this by creating a [CloseableHandler][closeable-handler-ref] instance from an autoclosable value, which will hold on to the value to close it at the end. The resource API lets you solve values or run effects using a closable resource, so we can ultimately handle the throwing operation with either the [SolveHandler][solve-handler-ref] or the [EffectHandler][effect-handler-ref].\r\n\r\n```java\r\nMaybe.withResource(myResource)\r\n  .solve(res -\u003e {\r\n    // Return something using `res`\r\n  });\r\n\r\nMaybe.withResource(myResource)\r\n  .effect(res -\u003e {\r\n    // do somthing with `res`\r\n  });\r\n```\r\n\r\nIn many cases, the resource you need will also throw an exception when we obtain it. We encourage you to first handle the exception that obtaining the resource may throw, and then map the value to a [CloseableHandler][closeable-handler-ref] to handle the next operation. For this [SolveHandler][solve-handler-ref] provides a `.mapToResource(..)` method so you can map solved values to resources.\r\n\r\n```java\r\npublic Properties parsePropertiesFile(final String filePath) {\r\n  return Maybe.of(filePath)\r\n    .solve(FileInputStream::new)\r\n    .catchError(err -\u003e /* Handle the error */)\r\n    .mapToResource(Function.identity())\r\n    .solve(inputStream -\u003e {\r\n      final Properties props = new Properties();\r\n      props.load(inputStream);\r\n\r\n      return props;\r\n    })\r\n    .orElseGet(Properties::new);\r\n}\r\n```\r\n\r\n\u003e We know the first solved value extends from `AutoCloseable`, but the compiler doesn't. We need to explicitly map the value with `Function.identity()` so the compiler can safely ensure that the resource can be closed.\r\n\r\n### Catching multiple exceptions\r\n\r\nSome operation may throw multiple type of exceptions. We can choose how to handle each one using one of the `.catchError(..)` matcher overloads. This method can be chained one after another, meaning the first one to match the exception type will handle the error. However, the compiler cannot ensure exhaustive matching of the error types (for now), so we'll always need to handle a default case with a terminal operator.\r\n\r\n```java\r\nMaybe.of(path)\r\n  .solve(Files::readAllLines) // throws IOException\r\n  .catchError(FileNotFoundException.class, err -\u003e ...)\r\n  .catchError(FileSystemException.class, err -\u003e ...)\r\n  .catchError(EOFException.class, err -\u003e ...)\r\n  .orElse(err -\u003e ...);\r\n```\r\n\r\n### Ambiguous overloads\r\n\r\nThe API has overloads where parameters can be passed as lambda expressions. Even though the parameter types are not ambiguous, there's one particular case where a lambda expression may not be inferred as the expected type:\r\n\r\n```java\r\n() -\u003e {\r\n  throw new Exception(\"...\");\r\n}\r\n\r\narg -\u003e {\r\n  throw new Exception(\"...\");\r\n} \r\n```\r\n\r\nIf the lambda expression finishes throwing an exception, the compile will not be able to tell the difference between a type that returns a value and a type that doesn't. For example, the first lambda expression above can be either a `ThrowingRunnable\u003cException\u003e` or a `ThrowingSupplier\u003cObject, Exception\u003e`. Similarly, the second expression can either be a `ThrowingConsumer\u003cObject, Exception\u003e` or a `ThrowingFunction\u003cObject, Object, Exception\u003e`.\r\n\r\nIf you ever run into ambiguity issues in the Maybe API, you have 2 easy options to solve the problem:\r\n\r\n1. (Recommended) Explicity setting the generic type(s) of the method will deambiguate the overload you expect to be used.\r\n\r\n```java\r\nMaybe.\u003cIOException\u003efrom(() -\u003e { // param is a `ThrowingRunnable\u003cIOException\u003e`\r\n  throw new IOException(\"...\");\r\n});\r\n\r\nMaybe.\u003cString, IOException\u003efrom(() -\u003e { // param is a `ThrowingSupplier\u003cString, IOException\u003e`\r\n  throw new IOException(\"...\");\r\n});\r\n```\r\n\r\n2. Casting the lambda expression to the specific `@FunctionalInterface` type you want to use.\r\n\r\n```java\r\nMaybe.from((ThrowingRunnable\u003cIOException\u003e) () -\u003e {\r\n  throw new IOException(\"...\");\r\n});\r\n\r\nMaybe.from((ThrowingSupplier\u003cString, IOException\u003e) () -\u003e {\r\n  throw new IOException(\"...\");\r\n});\r\n```\r\n\r\n## The `Either\u003cL, R\u003e` type\r\n\r\nAn awesome extra of `Maybe`, is that it provides a useful [Either\u003cL, R\u003e][either-ref] type which guarantees that the only one of the sides (left `L` or right `R`) is present per instance. That is possible thanks to:\r\n\r\n1. `Either\u003cL, R\u003e` is a [sealed interface](https://docs.oracle.com/en/java/javase/15/language/sealed-classes-and-interfaces.html). It cannot be implemented by any class nor anonimously instantiated in any way.\r\n2. There only exist 2 implementations of `Either\u003cL, R\u003e`: `Either.Left` and `Either.Right`. In those implementations, only one field is used to store the instance value.\r\n3. It's not possible to create an `Either\u003cL, R\u003e` instance of a `null` value.\r\n\r\nThe `Either\u003cL, R\u003e` makes a lot of sense when resolving values from throwing operations. At the end of the day, you can end up with either the solved value (`Rigth`) or the thrown exception (`Left`). You can convert from a `SolveHandler\u003cT, E\u003e` to an `Either\u003cE, T\u003e` usong the `SolveHandler#toEither` terminal operator.\r\n\r\nTo use `Either` on its own, use the factory methods to create an instance and the API to handle/unwrap the value:\r\n\r\n```java\r\npublic Either\u003cString, Integer\u003e fizzOrNumber(final int value) {\r\n  return value % 7 == 0\r\n    ? Either.ofLeft(\"fizz\")\r\n    : Either.ofRight(value);\r\n}\r\n\r\npublic static void main (final String[] args) {\r\n  final var sum = IntStream.range(1, 25)\r\n    .boxed()\r\n    .map(this::fizzOrNumber)\r\n    .map(either -\u003e\r\n      either\r\n        .onLeft(fizz -\u003e log.info(\"Multiple of 7: {}\", fizz))\r\n        .onRight(value -\u003e log.info(\"Value: {}\", value))\r\n        .unwrap(\r\n          fizz -\u003e 0,\r\n          Function.identity()\r\n        )\r\n    )\r\n    .reduce(0, Integer::sum);\r\n\r\n  log.info(\"The sum of non-fizz values is: {}\", sum);\r\n}\r\n```\r\n\r\nTake a look at the [documentation][either-ref] to see all the methods available in the `Either\u003cL, R\u003e` API.\r\n\r\n## Optional interoperability\r\n\r\nThe API provides full interoperability with Java's `Optional`. You can use `Maybe.from(Optional)` overload to create an instance from an optional value, or you can use the terminal operator `.toOptional()` to unwrap the value to an optional too. However, there's a change you might want to create a `Maybe\u003cT\u003e` withing the Optional API or another library like [Project Reactor](https://projectreactor.io/), like from a `.map(..)` method. To make this esier the API provides overloads to that create partial applications, and when fully applied return the specific handler.\r\n\r\nSo instead of having nested lambdas like this:\r\n\r\n```java\r\nOptional.ofNullable(rawValue)\r\n  .map(str -\u003e Maybe.from(() -\u003e Base64.getDecoder().decode(str)))\r\n  .map(decoded -\u003e decoded.catchError(...));\r\n```\r\n\r\nYou can use the partial application overload and use method reference syntax:\r\n\r\n```java\r\nOptional.ofNullable(rawValue)\r\n  .map(Maybe.partial(Base64.getDecoder()::decode))\r\n  .map(decoded -\u003e decoded.catchError(...));\r\n```\r\n\r\n## API Reference\r\n\r\nYou can find more details of the API in the [latest Javadocs](https://javadoc.io/doc/io.github.joselion/maybe).If you need to check the Javadocs of an older version you can also use the full URL as shown below. Just replace `\u003cx.y.z\u003e` with the version you want to see:\r\n\r\n```\r\nhttps://javadoc.io/doc/io.github.joselion/maybe/\u003cx.y.z\u003e\r\n```\r\n\r\n## Something's missing?\r\n\r\nSuggestions are always welcome! Please create an [issue](https://github.com/JoseLion/maybe/issues/new) describing the request, feature, or bug. I'll try to look into it as soon as possible 🙂\r\n\r\n## Contributions\r\n\r\nContributions are very welcome! To do so, please fork this repository and open a Pull Request to the `main` branch.\r\n\r\n## License\r\n\r\n[Apache License 2.0](https://github.com/JoseLion/maybe/blob/main/LICENSE)\r\n\r\n\u003c!-- References --\u003e\r\n[solve-handler-ref]: https://javadoc.io/doc/io.github.joselion/maybe/latest/com/github/joselion/maybe/SolveHandler.html\r\n[effect-handler-ref]: https://javadoc.io/doc/io.github.joselion/maybe/latest/com/github/joselion/maybe/EffectHandler.html\r\n[closeable-handler-ref]: https://javadoc.io/doc/io.github.joselion/maybe/latest/com/github/joselion/maybe/CloseableHandler.html\r\n[util-package-ref]: https://javadoc.io/doc/io.github.joselion/maybe/latest/com/github/joselion/maybe/util/package-summary.html\r\n[either-ref]: https://javadoc.io/doc/io.github.joselion/maybe/latest/com/github/joselion/maybe/Either.html\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoselion%2Fmaybe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoselion%2Fmaybe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoselion%2Fmaybe/lists"}