├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── co │ └── unruly │ └── control │ ├── ConsumableFunction.java │ ├── ErrorThrowingLambdas.java │ ├── HigherOrderFunctions.java │ ├── Lazy.java │ ├── Lists.java │ ├── Optionals.java │ ├── PartialApplication.java │ ├── Piper.java │ ├── Predicates.java │ ├── ThrowingLambdas.java │ ├── Unit.java │ ├── casts │ └── Equality.java │ ├── matchers │ ├── FailureMatcher.java │ ├── ResultMatchers.java │ └── SuccessMatcher.java │ ├── pair │ ├── Comprehensions.java │ ├── Maps.java │ ├── Pair.java │ ├── PairListCollector.java │ ├── PairReducingCollector.java │ ├── Pairs.java │ ├── Quad.java │ └── Triple.java │ ├── result │ ├── Combiners.java │ ├── Introducers.java │ ├── Match.java │ ├── MonadicAliases.java │ ├── Recover.java │ ├── Resolvers.java │ ├── Result.java │ ├── ResultCollector.java │ ├── Transformers.java │ └── TypeOf.java │ └── validation │ ├── FailedValidation.java │ ├── ForwardingList.java │ ├── Validator.java │ └── Validators.java └── test └── java ├── co └── unruly │ └── control │ ├── ThrowingLambdasTest.java │ ├── ZipTest.java │ ├── pair │ └── PairTest.java │ ├── result │ ├── CastsTest.java │ ├── MatchTest.java │ ├── PiperTest.java │ ├── ResultsTest.java │ └── TryTest.java │ └── validation │ └── ValidatorTest.java └── examples ├── ConciseEquals.java ├── ExceptionsInStreamsHandling.java ├── FlatMapVariance.java ├── FunctionalErrorHandling.java └── NovelErrorHandling.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | *.iml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | sudo: false 7 | addons: 8 | apt: 9 | packages: 10 | - oracle-java8-installer -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the Blogging and Open-Source Software team at boss@unrulygroup.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Unruly 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # co.unruly.control 2 | 3 | [![Build Status](https://travis-ci.org/unruly/control.svg?branch=master)](https://travis-ci.org/unruly/control) 4 | [![Release Version](https://img.shields.io/maven-central/v/co.unruly/control.svg)](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22co.unruly%22%20AND%20a%3A%22control%22) 5 | [![Javadocs](https://www.javadoc.io/badge/co.unruly/control.svg)](https://www.javadoc.io/doc/co.unruly/control) 6 | 7 | :warning: **This repo had been archived. Development continues at https://github.com/writeoncereadmany/control** :warning: 8 | 9 | 10 | Control is a collection of functional control-flow primitives and utilities, built around a 11 | [`Result`](https://javadoc.io/page/co.unruly/control/latest/co/unruly/control/result/Result.html) type. 12 | 13 | ## Installation 14 | 15 | ### Using Maven 16 | 17 | Add this dependency to your `pom.xml`. 18 | 19 | ```xml 20 | 21 | co.unruly 22 | control 23 | 0.8.12 24 | 25 | ``` 26 | 27 | ## Documentation 28 | 29 | - [JavaDoc](https://www.javadoc.io/doc/co.unruly/control) 30 | 31 | ## Examples 32 | 33 | ### Result 34 | 35 | What is a `Result`? It's a representation of the outcome of an operation *which may have failed*. Kind of like `java.util.Optional`. 36 | 37 | Like `Optional`, it wraps a value, and like `Optional`, it doesn't allow you to use that value directly. 38 | After all, if it's representing a failed operation, there may not *be* a value. 39 | In order to extract a value, you have to define how to handle the failure cases. 40 | 41 | However, an `Optional` is either *present*, and contains a value, or *absent*, conveying no information. 42 | A `Result`, however, is either a *success*, containing a value, or a *failure*, containing information about the failure. 43 | 44 | 45 | #### Creating a Result from a value: Introducers 46 | 47 | The simplest way to create a `Result` is simply to instantiate either a success or failure value, as appropriate. 48 | For example, the following code creates a successful `Result`, followed by a failed `Result`. 49 | 50 | ```java 51 | Result firstResult = Result.success(42); 52 | Result secondResult = Result.failure("What is six times nine?"); 53 | ``` 54 | 55 | There are also many common repeated patterns of operations which can fail. 56 | For example, you may have some code which creates `Optional`s, which you would like 57 | to convert to `Result`s in order to better track failure causes. 58 | 59 | For this, [`co.unruly.control.result.Introducers`](https://javadoc.io/page/co.unruly/control/latest/co/unruly/control/result/Introducers.html) 60 | contains a selection of useful functions which yield `Result`s: 61 | 62 | ```java 63 | public Result asResult(Optional maybeNumber) { 64 | return with(maybeNumber, fromOptional(() -> "No number found. :(")); 65 | } 66 | ``` 67 | 68 | #### Composing operations on a Result: Transformers 69 | 70 | `Result` is most useful when modelling an operation as a sequence of smaller operations, each using 71 | the output of the last step, and some of which can fail. The most common operations here are `onSuccess()` 72 | and `attempt()`. 73 | 74 | Invoking `onSuccess(f)` on a `Result` will apply `f` to the value in the `Result` if it's a success, yielding a 75 | new success. If the original `Result` was a failure, `f` will not be invoked and the original failing `Result` will 76 | be returned. 77 | 78 | If you have a function `f` which can fail - i.e. a function which returns a `Result` - then invoking `attempt(f)` will 79 | apply `f` to the value in the `Result` if it's a success, yielding a new `Result` which may be either a success or 80 | failure. If the original `Result` was a failure, `f` will not be invoked and the original failing `Result` will 81 | be returned. 82 | 83 | This allows us to chain together a sequence of calls to `onSuccess` and `attempt` to specify what to do on the 84 | happy path, and cascade any failure cases together to be handled once. 85 | 86 | For example, if we want to write a bestselling novel, then there are various steps towards getting it published. 87 | We need to have an idea, secure an advance, write the manuscript, get it edited, get it published, and rocket up the bestseller charts. 88 | 89 | Getting the idea, securing an advance and finishing the manuscript are definitely steps which can fail. 90 | However, given success in the other steps, editing and publishing are mechanical steps we can have confidence will succeed, 91 | and once those are complete we can then release the novel and (eventually) count the total sales. 92 | 93 | If any of the steps fail, no novel is published, and we can look at the error message from whenever the process failed. 94 | 95 | We could therefore model the process as follows: 96 | 97 | ```java 98 | public static String describeNovelSales(Author author, Publisher publisher, Editor editor, Retailer retailer) { 99 | return author.getIdea() 100 | .then(attempt(publisher::getAdvance)) 101 | .then(attempt(author::writeNovel)) 102 | .then(onSuccess(editor::editNovel)) 103 | .then(onSuccess(publisher::publishNovel)) 104 | .then(onSuccess(retailer::sellNovel)) 105 | .then(onSuccess(sales -> format("%s sold %d copies", sales.novel, sales.copiesSold))) 106 | .then(collapse()); 107 | } 108 | ``` 109 | 110 | Whilst `onSuccess()` and `attempt()` are the most common ways to transform a `Result` into another `Result`, 111 | other use cases exist. A collection of such functions exists in 112 | [`co.unruly.control.result.Transformers`](https://javadoc.io/page/co.unruly/control/latest/co/unruly/control/result/Transformers.html). 113 | 114 | #### Extracting a value from a Result: Resolvers 115 | 116 | The simplest way to extract a value from a `Result` is to simply describe what to do with a failure value. 117 | For example, the following method takes a `Result` of either an `Integer` 118 | or a `String` describing the failure, and returns the wrapped integer if it was a success, 119 | or -1 if it was a failure: 120 | 121 | ```java 122 | public Integer extractValue(Result count) { 123 | return count.then(ifFailed(x -> -1)); 124 | } 125 | ``` 126 | 127 | Sometimes, the success and failure types are the same. In that case, you can simply 128 | collapse both cases to a single value: 129 | 130 | ```java 131 | public static void main(String ...args) { 132 | printResult(Result.success("This was a triumph!")); 133 | printResult(Result.failure("The cake is a lie")); 134 | } 135 | 136 | 137 | public static String printResult(Result value) { 138 | System.out.println(value.then(collapse())); 139 | } 140 | ``` 141 | Which outputs: 142 | ``` 143 | > This was a triumph! 144 | > The cake is a lie 145 | ``` 146 | 147 | `ifFailed` and `collapse` can both be found in 148 | [`co.unruly.control.result.Resolvers`](https://javadoc.io/page/co.unruly/control/latest/co/unruly/control/result/Resolvers.html) 149 | , along with a selection of other functions to convert `Result`s into non-`Result` values. 150 | 151 | #### Functional API 152 | 153 | None of the operations described above are methods on the `Result` object. 154 | Instead, `Result` presents only two methods: `Result.either()` and `Result.then()`. 155 | 156 | ##### Result.either() 157 | 158 | `Result.either` takes two functions, one for the success case and one for the failure case. 159 | If the `Result` is a success, it applies the first function to its success value and returns the result. 160 | If it was a failure, instead it applies the second function. 161 | 162 | ```java 163 | public static void main(String ...args) { 164 | printResult(Result.success(99)); 165 | printResult(Result.failure("bananas")); 166 | } 167 | 168 | 169 | public static String printResult(Result value) { 170 | System.out.println(value.either( 171 | success -> success + " bottles of beer on the wall", 172 | failure -> "Yes sir, we have no " + failure 173 | )); 174 | } 175 | ``` 176 | 177 | Which outputs: 178 | ``` 179 | > 99 bottles of beer on the wall 180 | > Yes sir, we have no bananas 181 | ``` 182 | 183 | Note that the argument types of the functions must match the corresponding success 184 | and failure types, and both functions must return the same type. 185 | 186 | ##### Result.then() 187 | 188 | `Result.then` takes a function from a `Result` to a value, and then applies 189 | that function to the result. The following lines of code are (other than generics inference issues) equivalent: 190 | ```java 191 | ifFailed(x -> "Hello World").apply(result); 192 | result.then(ifFailed(x -> "Hello World")); 193 | ``` 194 | 195 | By structuring the API like this, instead of having a fixed set of methods available, we can 196 | create more specialised functions for domain-specific use cases and mix them seamlessly with 197 | standard operations, and customise how we interact with `Result`s. 198 | 199 | The functions included in this library are all higher-order functions, returning 200 | instances of `Function`. This includes functions to create `Result`s from non-`Result` 201 | values - both for consistency, and compatibility with idiomatic `Stream` usage. 202 | As non-`Result` values don't implement `then()`, and there are both readability and 203 | generics issues, we also include `Piper`. 204 | 205 | ##### Piper 206 | 207 | This approach is slightly more cumbersome than it could be, as Java's ability to infer 208 | generic types is not particularly great. For example, the following code doesn't compile: 209 | 210 | ```java 211 | public Result asResult(Optional maybeNumber) { 212 | return fromOptional(() -> "No number found. :(").apply(maybeNumber); 213 | } 214 | ``` 215 | This fails to compile because the type of `fromOptional` is inferred to be 216 | `Function, Result`, because the type inference 217 | engine can't infer the success type based on subsequent operations (namely, the type 218 | passed to `apply()`). 219 | 220 | Enter [`Piper`](https://javadoc.io/page/co.unruly/control/latest/co/unruly/control/Piper.html). 221 | A `Piper` is simply a box for a value: 222 | 223 | ```java 224 | Piper pipe = Piper.pipe(42); 225 | ``` 226 | 227 | You can then chain functions on the pipe: 228 | 229 | ```java 230 | Piper.pipe(42) // yields a Pipe containing 42 231 | .then(x -> x + 10) // yields a Pipe containing 52 232 | .then(x -> x * 2) // yields a Pipe containing 104 233 | .resolve() // returns 104 234 | ``` 235 | 236 | This allows us to rewrite our failing-to-compile example from above as: 237 | 238 | ```java 239 | public Result asResult(Optional maybeNumber) { 240 | return pipe(maybeNumber) // Pipe of Optional 241 | .then(fromOptional(() -> "No number found. :(")) // Pipe of Result 242 | .resolve(); // Result 243 | } 244 | ``` 245 | 246 | For short examples like this, there's also a `resolveWith()` method: 247 | 248 | ```java 249 | public Result asResult(Optional maybeNumber) { 250 | return pipe(maybeNumber) 251 | .resolveWith(fromOptional(() -> "No number found. :(")); 252 | } 253 | ``` 254 | 255 | `Piper.then()` functions in the same way as `Result.then()`: it applies the provided function 256 | to its contents. This means we can build a functional pipeline which includes both regular 257 | values and Results: 258 | 259 | ```java 260 | Result result = pipe("a=1234;") // Piper 261 | .then(pattern::matcher) // Piper 262 | .then(ifIs(Matcher::find, m -> m.group(1))) // Piper> 263 | .then(onFailure(__ -> "Could not find group to match")) // Piper> 264 | .then(attempt(tryTo(Integer::parseInt, ex -> ex.getMessage()))) // Piper> 265 | .resolve(); // Result 266 | ``` 267 | 268 | Note that the use of `Piper.then()` is identical to the way we'd use `Result.then()`, except now we 269 | can directly chain from non-failable states into failable states. 270 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | co.unruly 5 | control 6 | 0.8.14-SNAPSHOT 7 | jar 8 | 9 | control 10 | 11 | A collection of functional utilities around error handling, wrapping a successfully returned value or 12 | error 13 | 14 | 15 | https://github.com/unruly/control 16 | 17 | 18 | 1.8 19 | 1.8 20 | 1.8 21 | UTF-8 22 | -Xdoclint:none 23 | 24 | 25 | 26 | 27 | Unruly Developers 28 | oss@unrulymedia.com 29 | 30 | 31 | 32 | 33 | 34 | MIT License 35 | http://www.opensource.org/licenses/mit-license.php 36 | 37 | 38 | 39 | 40 | scm:git:git@github.com:unruly/${project.artifactId}.git 41 | scm:git:git@github.com:unruly/${project.artifactId}.git 42 | git@github.com:unruly/${project.artifactId}.git 43 | HEAD 44 | 45 | 46 | 47 | 48 | 49 | 50 | org.hamcrest 51 | java-hamcrest 52 | 2.0.0.0 53 | 54 | 55 | 56 | 57 | 58 | junit 59 | junit 60 | 4.12 61 | test 62 | 63 | 64 | org.mockito 65 | mockito-core 66 | 2.6.2 67 | test 68 | 69 | 70 | org.hamcrest 71 | hamcrest-junit 72 | 2.0.0.0 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-compiler-plugin 81 | 3.1 82 | 83 | ${java.version} 84 | ${java.version} 85 | 86 | 87 | 88 | org.apache.maven.plugins 89 | maven-source-plugin 90 | 2.2.1 91 | 92 | 93 | attach-sources 94 | 95 | jar-no-fork 96 | 97 | 98 | 99 | 100 | 101 | org.apache.maven.plugins 102 | maven-javadoc-plugin 103 | 2.10.4 104 | 105 | 106 | attach-javadocs 107 | 108 | jar 109 | 110 | 111 | 112 | 113 | 114 | org.apache.maven.plugins 115 | maven-release-plugin 116 | 2.5 117 | 118 | true 119 | false 120 | release 121 | deploy 122 | forked-path 123 | 124 | 125 | 126 | org.sonatype.plugins 127 | nexus-staging-maven-plugin 128 | 1.6.5 129 | true 130 | 131 | ossrh 132 | https://oss.sonatype.org/ 133 | true 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | release 142 | 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-gpg-plugin 147 | 1.6 148 | 149 | 150 | sign-artifacts 151 | verify 152 | 153 | sign 154 | 155 | 156 | false 157 | gpg 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | ossrh 170 | https://oss.sonatype.org/content/repositories/snapshots 171 | 172 | 173 | ossrh 174 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/ConsumableFunction.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.Function; 5 | 6 | /** 7 | * A Consumer which can also be used where a Function is required, returning its input value 8 | */ 9 | @FunctionalInterface 10 | public interface ConsumableFunction extends Function, Consumer { 11 | 12 | default A apply(final A value) { 13 | accept(value); 14 | return value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/ErrorThrowingLambdas.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control; 2 | 3 | import java.util.function.BiFunction; 4 | import java.util.function.Consumer; 5 | import java.util.function.Function; 6 | import java.util.function.Predicate; 7 | 8 | /** 9 | * A collection of functional interfaces which throw, and convenience functions to wrap them 10 | * so any thrown Throwables are converted to RuntimeExceptions so they can be used where 11 | * non-throwing functional interfaces are required 12 | * 13 | * Catching errors in the general case is not recommended, but there are specific errors 14 | * which are contextually reasonable to catch. Therefore, this wider capability exists 15 | * separately and should be used judiciously. 16 | */ 17 | public interface ErrorThrowingLambdas { 18 | 19 | /** 20 | * A Function which may throw a checked exception 21 | */ 22 | @FunctionalInterface 23 | interface ThrowingFunction { 24 | O apply(I input) throws X; 25 | 26 | default ThrowingFunction andThen(Function nextFunction) { 27 | return x -> nextFunction.apply(apply(x)); 28 | } 29 | 30 | default ThrowingFunction compose(Function nextFunction) { 31 | return x -> apply(nextFunction.apply(x)); 32 | } 33 | 34 | /** 35 | * Converts the provided function into a regular Function, where any thrown exceptions are 36 | * wrapped in a RuntimeException. 37 | */ 38 | static Function throwingRuntime(ThrowingFunction f) { 39 | return x -> { 40 | try { 41 | return f.apply(x); 42 | } catch (Throwable ex) { 43 | throw new RuntimeException(ex); 44 | } 45 | }; 46 | } 47 | } 48 | 49 | /** 50 | * A Consumer which may throw a checked exception 51 | */ 52 | @FunctionalInterface 53 | interface ThrowingConsumer { 54 | void accept(T item) throws X; 55 | 56 | /** 57 | * Converts the provided consumer into a regular Consumer, where any thrown exceptions are 58 | * wrapped in a RuntimeException. 59 | */ 60 | static Consumer throwingRuntime(ThrowingConsumer p) { 61 | return x -> { 62 | try { 63 | p.accept(x); 64 | } catch (Throwable ex) { 65 | throw new RuntimeException(ex); 66 | } 67 | }; 68 | } 69 | } 70 | 71 | /** 72 | * A BiFunction which may throw a checked exception 73 | */ 74 | @FunctionalInterface 75 | interface ThrowingBiFunction { 76 | R apply(A first, B second) throws X; 77 | 78 | /** 79 | * Converts the provided bifunction into a regular BiFunction, where any thrown exceptions 80 | * are wrapped in a RuntimeException 81 | */ 82 | static BiFunction throwingRuntime(ThrowingBiFunction f) { 83 | return (a, b) -> { 84 | try { 85 | return f.apply(a, b); 86 | } catch (Throwable ex) { 87 | throw new RuntimeException(ex); 88 | } 89 | }; 90 | } 91 | } 92 | 93 | /** 94 | * A Predicate which may throw a checked exception 95 | */ 96 | @FunctionalInterface 97 | interface ThrowingPredicate { 98 | boolean test(T item) throws X; 99 | 100 | /** 101 | * Converts the provided predicate into a regular Predicate, where any thrown exceptions 102 | * are wrapped in a RuntimeException 103 | */ 104 | static Predicate throwingRuntime(ThrowingPredicate p) { 105 | return x -> { 106 | try { 107 | return p.test(x); 108 | } catch (Throwable ex) { 109 | throw new RuntimeException(ex); 110 | } 111 | }; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/HigherOrderFunctions.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control; 2 | 3 | import co.unruly.control.pair.Pair; 4 | 5 | import java.util.*; 6 | import java.util.function.BiFunction; 7 | import java.util.function.Consumer; 8 | import java.util.function.Function; 9 | import java.util.function.Predicate; 10 | import java.util.stream.Stream; 11 | import java.util.stream.StreamSupport; 12 | 13 | import static java.util.function.Function.identity; 14 | import static java.util.stream.Collectors.toList; 15 | import static java.util.stream.Stream.iterate; 16 | 17 | public interface HigherOrderFunctions { 18 | 19 | /** 20 | * Takes a BiFunction, and reverses the order of the arguments 21 | */ 22 | static BiFunction flip(BiFunction f) { 23 | return (a, b) -> f.apply(b, a); 24 | } 25 | 26 | /** 27 | * Takes a list of functions (which take and return the same type) and composes 28 | * them into a single function, applying the provided functions in order 29 | */ 30 | static Function compose(Function... functions) { 31 | return compose(Stream.of(functions)); 32 | } 33 | 34 | /** 35 | * Takes a Stream of functions (which take and return the same type) and composes 36 | * them into a single function, applying the provided functions in order 37 | */ 38 | static Function compose(Stream> functions) { 39 | return functions.reduce(identity(), Function::andThen); 40 | } 41 | 42 | /** 43 | * Takes a list of predicates and composes them into a single predicate, which 44 | * passes when all passed-in predicates pass 45 | */ 46 | static Predicate compose(Predicate... functions) { 47 | return Stream.of(functions).reduce(__ -> true, Predicate::and); 48 | } 49 | 50 | /** 51 | * Turns a Consumer into a Function which applies the consumer and returns the input 52 | */ 53 | static Function peek(Consumer action) { 54 | return t -> { 55 | action.accept(t); 56 | return t; 57 | }; 58 | } 59 | 60 | static Stream> withIndices(Stream items) { 61 | return zip(iterate(0, x -> x + 1), items); 62 | } 63 | 64 | static Stream> zip(Stream a, Stream b) { 65 | return zip(a, b, Pair::of); 66 | } 67 | 68 | /** 69 | * Zips two streams together using the zipper function, resulting in a single stream of 70 | * items from each stream combined using the provided function. 71 | * 72 | * The resultant stream will have the length of the shorter of the two input streams. 73 | * 74 | * Sourced from https://stackoverflow.com/questions/17640754/zipping-streams-using-jdk8-with-lambda-java-util-stream-streams-zip 75 | */ 76 | static Stream zip(Stream a, Stream b, BiFunction zipper) { 77 | Objects.requireNonNull(zipper); 78 | Spliterator aSpliterator = Objects.requireNonNull(a).spliterator(); 79 | Spliterator bSpliterator = Objects.requireNonNull(b).spliterator(); 80 | 81 | // Zipping looses DISTINCT and SORTED characteristics 82 | int characteristics = aSpliterator.characteristics() & bSpliterator.characteristics() & 83 | ~(Spliterator.DISTINCT | Spliterator.SORTED); 84 | 85 | long zipSize = ((characteristics & Spliterator.SIZED) != 0) 86 | ? Math.min(aSpliterator.getExactSizeIfKnown(), bSpliterator.getExactSizeIfKnown()) 87 | : -1; 88 | 89 | Iterator aIterator = Spliterators.iterator(aSpliterator); 90 | Iterator bIterator = Spliterators.iterator(bSpliterator); 91 | Iterator cIterator = new Iterator() { 92 | @Override 93 | public boolean hasNext() { 94 | return aIterator.hasNext() && bIterator.hasNext(); 95 | } 96 | 97 | @Override 98 | public C next() { 99 | return zipper.apply(aIterator.next(), bIterator.next()); 100 | } 101 | }; 102 | 103 | Spliterator split = Spliterators.spliterator(cIterator, zipSize, characteristics); 104 | return (a.isParallel() || b.isParallel()) 105 | ? StreamSupport.stream(split, true) 106 | : StreamSupport.stream(split, false); 107 | } 108 | 109 | /** 110 | * Takes two lists, and returns a list of pairs forming the Cartesian product of those lists. 111 | */ 112 | static List> pairs(List as, List bs) { 113 | return as.stream().flatMap(a -> bs.stream().map(b -> Pair.of(a, b))).collect(toList()); 114 | } 115 | 116 | /** 117 | * Takes a value, and returns that same value, upcast to a suitable type. Inference is our friend here. 118 | */ 119 | static R upcast(T fv) { 120 | return fv; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/Lazy.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control; 2 | 3 | import java.util.Optional; 4 | import java.util.function.Supplier; 5 | 6 | /** 7 | * Generic lazy container: does not calculate the value until it's required. 8 | * Will only calculate the value once. 9 | * @param the type of object we're lazily instantiating 10 | */ 11 | public final class Lazy { 12 | 13 | private final Supplier source; 14 | private Optional value = Optional.empty(); 15 | 16 | public Lazy(Supplier source) { 17 | this.source = source; 18 | } 19 | 20 | public synchronized T get() { 21 | return value.orElseGet(this::calculateAndStore); 22 | } 23 | 24 | private T calculateAndStore() { 25 | T calculatedValue = source.get(); 26 | value = Optional.of(calculatedValue); 27 | return calculatedValue; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/Lists.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control; 2 | 3 | import co.unruly.control.pair.Pair; 4 | import co.unruly.control.result.Result; 5 | 6 | import java.util.List; 7 | 8 | import static co.unruly.control.result.Resolvers.split; 9 | 10 | public interface Lists { 11 | 12 | static Result, List> successesOrFailures(List> results) { 13 | Pair, List> successesAndFailures = results.stream().collect(split()); 14 | if(successesAndFailures.right.isEmpty()) { 15 | return Result.success(successesAndFailures.left); 16 | } else { 17 | return Result.failure(successesAndFailures.right); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/Optionals.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control; 2 | 3 | import java.util.Optional; 4 | import java.util.function.Consumer; 5 | import java.util.function.Function; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Stream; 8 | 9 | public interface Optionals { 10 | 11 | /** 12 | * Converts an Optional to a Stream. 13 | * If the Optional was present, you get a Stream of one item. 14 | * If it was absent, you get an empty stream. 15 | * 16 | * Flatmapping a Stream of Optionals over this method will return a Stream of the 17 | * contents of the present Optionals in the input stream. 18 | */ 19 | static Stream stream(Optional item) { 20 | return either(item, Stream::of, Stream::empty); 21 | } 22 | 23 | /** 24 | * If the provided optional is present, applies the first function to the wrapped value and returns it. 25 | * Otherwise, returns the value supplied by the second function 26 | */ 27 | static R either(Optional optional, Function onPresent, Supplier onAbsent) { 28 | return optional.map(onPresent).orElseGet(onAbsent); 29 | } 30 | 31 | /** 32 | * If the provided Optional is present, pass the wrapped value to the provided consumer 33 | * 34 | * This simply invokes Optional.ifPresent(), and exists for cases where side-effects are required 35 | * in both the present and absent cases on an Optional. The Optional API doesn't cover the latter case, 36 | * so this provides a mimicking calling convention to permit consistency. 37 | */ 38 | static void ifPresent(Optional optional, Consumer consume) { 39 | optional.ifPresent(consume); 40 | } 41 | 42 | /** 43 | * If the provided Optional is empty, invoke the provided Runnable 44 | */ 45 | static void ifAbsent(Optional optional, Runnable action) { 46 | if(!optional.isPresent()) { 47 | action.run(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/PartialApplication.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control; 2 | 3 | import co.unruly.control.pair.Triple.TriFunction; 4 | 5 | import java.util.function.BiFunction; 6 | import java.util.function.Function; 7 | import java.util.function.Supplier; 8 | 9 | /** 10 | * A collection of functions to partially apply arguments to functions, to simplify usage 11 | * in streams, optionals etc. 12 | */ 13 | public interface PartialApplication { 14 | 15 | /** 16 | * Binds the provided argument to the function, and returns a Supplier with that argument applied. 17 | * 18 | * bind(f, a) is equivalent to () -> f.apply(a) 19 | */ 20 | static Supplier bind(Function f, I input) { 21 | return () -> f.apply(input); 22 | } 23 | 24 | /** 25 | * Binds the provided argument to the function, and returns a new Function with that argument already applied. 26 | * 27 | * bind(f, a) is equivalent to b -> f.apply(a, b) 28 | */ 29 | static Function bind(BiFunction f, A firstParam) { 30 | return secondParam -> f.apply(firstParam, secondParam); 31 | } 32 | 33 | /** 34 | * Binds the provided arguments to the function, and returns a new Supplier with those arguments already applied. 35 | * 36 | * bind(f, a, b) is equivalent to () -> f.apply(a, b) 37 | */ 38 | static Supplier bind(BiFunction f, A firstParam, B secondParam) { 39 | return () -> f.apply(firstParam, secondParam); 40 | } 41 | 42 | /** 43 | * Binds the provided argument to the function, and returns a new BiFunction with that argument already applied. 44 | * 45 | * bind(f, a) is equivalent to (b, c) -> f.apply(a, b, c) 46 | */ 47 | static BiFunction bind(TriFunction f, A firstParam) { 48 | return (secondParam, thirdParam) -> f.apply(firstParam, secondParam, thirdParam); 49 | } 50 | 51 | /** 52 | * Binds the provided arguments to the function, and returns a new Function with those arguments already applied. 53 | * 54 | * bind(f, a, b) is equivalent to c -> f.apply(a, b, c) 55 | */ 56 | static Function bind(TriFunction f, A firstParam, B secondParam) { 57 | return thirdParam -> f.apply(firstParam, secondParam, thirdParam); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/Piper.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.Function; 5 | 6 | /** 7 | * Wraps a value in a Piper, allowing chaining of operations and segueing into other applicable types 8 | * (such as Result) cleanly. 9 | * 10 | * This is most useful when not streaming over values, starting off with a value which is not a Result. 11 | * 12 | * @param the type of wrapped value 13 | */ 14 | public class Piper { 15 | 16 | private final T element; 17 | 18 | public Piper(T element) { 19 | this.element = element; 20 | } 21 | 22 | /** 23 | * Applies the function to the piped value, returning a new pipe containing that value. 24 | */ 25 | public Piper then(Function function) { 26 | return new Piper<>(function.apply(element)); 27 | } 28 | 29 | /** 30 | * Applies the consumer to the current value of the piped value, returning a pipe containing 31 | * that value. 32 | */ 33 | public Piper peek(Consumer consumer) { 34 | return then(HigherOrderFunctions.peek(consumer)); 35 | } 36 | 37 | /** 38 | * Returns the final result of the piped value, with all the piped functions applied. 39 | */ 40 | public T resolve() { 41 | return element; 42 | } 43 | 44 | /** 45 | * Returns the final result of the piped value, with all the piped functions applied. 46 | */ 47 | public R resolveWith(Function f) { 48 | return f.apply(element); 49 | } 50 | 51 | /** 52 | * Creates a new Piper wrapping the provided element. 53 | */ 54 | public static Piper pipe(T element) { 55 | return new Piper(element); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/Predicates.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control; 2 | 3 | import java.util.function.Predicate; 4 | 5 | /** 6 | * Created by tomj on 31/03/2017. 7 | */ 8 | public interface Predicates { 9 | 10 | /** 11 | * Negates a predicate: mostly useful when our predicate is a method reference or lambda where we can't 12 | * call negate() on it directly, or where the code reads better by having the negation at the beginning 13 | * rather than the end. 14 | */ 15 | static Predicate not(Predicate test) { 16 | return test.negate(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/ThrowingLambdas.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control; 2 | 3 | import java.util.function.BiFunction; 4 | import java.util.function.Consumer; 5 | import java.util.function.Function; 6 | import java.util.function.Predicate; 7 | 8 | /** 9 | * A collection of functional interfaces which throw, and convenience functions to wrap them 10 | * so any thrown exceptions are converted to RuntimeExceptions so they can be used where 11 | * non-throwing functional interfaces are required 12 | */ 13 | public interface ThrowingLambdas { 14 | 15 | /** 16 | * A Function which may throw a checked exception 17 | */ 18 | @FunctionalInterface 19 | interface ThrowingFunction { 20 | O apply(I input) throws X; 21 | 22 | default ThrowingFunction andThen(Function nextFunction) { 23 | return x -> nextFunction.apply(apply(x)); 24 | } 25 | 26 | default ThrowingFunction compose(Function nextFunction) { 27 | return x -> apply(nextFunction.apply(x)); 28 | } 29 | 30 | /** 31 | * Converts the provided function into a regular Function, where any thrown exceptions are 32 | * wrapped in a RuntimeException. 33 | */ 34 | static Function throwingRuntime(ThrowingFunction f) { 35 | return x -> { 36 | try { 37 | return f.apply(x); 38 | } catch (Exception ex) { 39 | throw new RuntimeException(ex); 40 | } 41 | }; 42 | } 43 | } 44 | 45 | /** 46 | * A Consumer which may throw a checked exception 47 | */ 48 | @FunctionalInterface 49 | interface ThrowingConsumer { 50 | void accept(T item) throws X; 51 | 52 | /** 53 | * Converts the provided consumer into a regular Consumer, where any thrown exceptions are 54 | * wrapped in a RuntimeException. 55 | */ 56 | static Consumer throwingRuntime(ThrowingConsumer p) { 57 | return x -> { 58 | try { 59 | p.accept(x); 60 | } catch (Exception ex) { 61 | throw new RuntimeException(ex); 62 | } 63 | }; 64 | } 65 | } 66 | 67 | /** 68 | * A BiFunction which may throw a checked exception 69 | */ 70 | @FunctionalInterface 71 | interface ThrowingBiFunction { 72 | R apply(A first, B second) throws X; 73 | 74 | /** 75 | * Converts the provided bifunction into a regular BiFunction, where any thrown exceptions 76 | * are wrapped in a RuntimeException 77 | */ 78 | static BiFunction throwingRuntime(ThrowingBiFunction f) { 79 | return (a, b) -> { 80 | try { 81 | return f.apply(a, b); 82 | } catch (Exception ex) { 83 | throw new RuntimeException(ex); 84 | } 85 | }; 86 | } 87 | } 88 | 89 | /** 90 | * A Predicate which may throw a checked exception 91 | */ 92 | @FunctionalInterface 93 | interface ThrowingPredicate { 94 | boolean test(T item) throws X; 95 | 96 | /** 97 | * Converts the provided predicate into a regular Predicate, where any thrown exceptions 98 | * are wrapped in a RuntimeException 99 | */ 100 | static Predicate throwingRuntime(ThrowingPredicate p) { 101 | return x -> { 102 | try { 103 | return p.test(x); 104 | } catch (Exception ex) { 105 | throw new RuntimeException(ex); 106 | } 107 | }; 108 | } 109 | } 110 | 111 | static Predicate throwsWhen(ThrowingConsumer consumer) { 112 | return t -> { 113 | try { 114 | consumer.accept(t); 115 | return false; 116 | } catch (Exception ex) { 117 | return true; 118 | } 119 | }; 120 | } 121 | 122 | static Predicate doesntThrow(ThrowingConsumer consumer) { 123 | return t -> { 124 | try { 125 | consumer.accept(t); 126 | return true; 127 | } catch (Exception ex) { 128 | return false; 129 | } 130 | }; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/Unit.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.Function; 5 | 6 | /** 7 | * Unit type, which contains only one possible value. 8 | *
16 |      * {@code
17 |      * Map lettersInWords = mapOf(
18 |      *   entry("Hello", 5),
19 |      *   entry("Goodbye", 7)
20 |      * );
21 |      * }
22 |      * 
23 | * @param entries the map entries 24 | * @param the key type 25 | * @param the value type 26 | * @return a map containing all the provided key-value pairs 27 | */ 28 | @SafeVarargs 29 | static Map mapOf(Pair ...entries) { 30 | return Stream.of(entries).collect(toMap()); 31 | } 32 | 33 | /** 34 | * Collects a stream of pairs into a map 35 | * @param the left type of the pair, interpreted as the key type 36 | * @param the right type of the pair, interpreted as the value type 37 | * @return a Collector which collects a Stream of Pairs into a Map 38 | */ 39 | static Collector, ?, Map> toMap() { 40 | return Collectors.toMap(Pair::left, Pair::right); 41 | } 42 | 43 | /** 44 | * Creates a key-value pair. 45 | * 46 | * This is just an alias for Pair.of, that makes more sense in a map-initialisation context. 47 | * @param key the key 48 | * @param value the value 49 | * @param the key type 50 | * @param the value type 51 | * @return a key-value pair 52 | */ 53 | static Pair entry(K key, V value) { 54 | return Pair.of(key, value); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/pair/Pair.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.pair; 2 | 3 | import java.util.Objects; 4 | import java.util.function.BiFunction; 5 | import java.util.function.Function; 6 | 7 | /** 8 | * A basic tuple type 9 | */ 10 | public class Pair { 11 | 12 | public final L left; 13 | public final R right; 14 | 15 | public Pair(L left, R right) { 16 | this.left = left; 17 | this.right = right; 18 | } 19 | 20 | public static Pair of(L left, R right) { 21 | return new Pair<>(left, right); 22 | } 23 | 24 | /** 25 | * Gets the left element. Note that Pair also supports direct member access, but this is useful when you need 26 | * a method reference to extract one side of the pair. 27 | */ 28 | public L left() { 29 | return left; 30 | } 31 | 32 | /** 33 | * Gets the right element. Note that Pair also supports direct member access, but this is useful when you need 34 | * a method reference to extract one side of the pair. 35 | */ 36 | public R right() { 37 | return right; 38 | 39 | } 40 | 41 | /** 42 | * Applies the given function to this pair. 43 | */ 44 | public T then(Function, T> function) { 45 | return function.apply(this); 46 | } 47 | 48 | /** 49 | * Applies the given bifunction to this pair, using left for the first argument and right for the second 50 | */ 51 | public T then(BiFunction function) { 52 | return function.apply(this.left, this.right); 53 | } 54 | 55 | @Override 56 | public boolean equals(Object o) { 57 | if (this == o) return true; 58 | if (o == null || getClass() != o.getClass()) return false; 59 | Pair pair = (Pair) o; 60 | return Objects.equals(left, pair.left) && 61 | Objects.equals(right, pair.right); 62 | } 63 | 64 | @Override 65 | public int hashCode() { 66 | return Objects.hash(left, right); 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | return "Pair{" + 72 | "left=" + left + 73 | ", right=" + right + 74 | '}'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/pair/PairListCollector.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.pair; 2 | 3 | import java.util.ArrayList; 4 | import java.util.EnumSet; 5 | import java.util.List; 6 | import java.util.Set; 7 | import java.util.function.BiConsumer; 8 | import java.util.function.BinaryOperator; 9 | import java.util.function.Function; 10 | import java.util.function.Supplier; 11 | import java.util.stream.Collector; 12 | 13 | /** 14 | * Collects a stream of Pairs into a Pair of Lists. 15 | */ 16 | public class PairListCollector implements Collector, Pair, List>, Pair> { 17 | 18 | private final Function, FL> leftFinisher; 19 | private final Function, FR> rightFinisher; 20 | 21 | public PairListCollector(Function, FL> leftFinisher, Function, FR> rightFinisher) { 22 | this.leftFinisher = leftFinisher; 23 | this.rightFinisher = rightFinisher; 24 | } 25 | 26 | @Override 27 | public Supplier, List>> supplier() { 28 | return () -> Pair.of(new ArrayList(), new ArrayList()); 29 | } 30 | 31 | @Override 32 | public BiConsumer, List>, Pair> accumulator() { 33 | return (pairs, pair) -> { 34 | pairs.left.add(pair.left); 35 | pairs.right.add(pair.right); 36 | }; 37 | } 38 | 39 | @Override 40 | public BinaryOperator, List>> combiner() { 41 | return (first, second) -> { 42 | first.left.addAll(second.left); 43 | first.right.addAll(second.right); 44 | return first; 45 | }; 46 | } 47 | 48 | @Override 49 | public Function, List>, Pair> finisher() { 50 | return pair -> Pair.of(leftFinisher.apply(pair.left), rightFinisher.apply(pair.right)); 51 | } 52 | 53 | @Override 54 | public Set characteristics() { 55 | return EnumSet.noneOf(Characteristics.class); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/pair/PairReducingCollector.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.pair; 2 | 3 | import java.util.EnumSet; 4 | import java.util.Set; 5 | import java.util.function.BiConsumer; 6 | import java.util.function.BinaryOperator; 7 | import java.util.function.Function; 8 | import java.util.function.Supplier; 9 | import java.util.stream.Collector; 10 | 11 | /** 12 | * A collector which performs a reduction on a stream of Pairs. This is provided as an alternative to reduce() 13 | * as there is no concept of a Reducer in the streams library, but this does act as a reducer, retaining only 14 | * the reduced value as we reduce over the stream (as opposed to building up a collection, then reducing over that). 15 | */ 16 | public class PairReducingCollector implements Collector, PairReducingCollector.MutablePair, Pair> { 17 | 18 | private final L leftIdentity; 19 | private final R rightIdentity; 20 | private final BinaryOperator leftReducer; 21 | private final BinaryOperator rightReducer; 22 | 23 | public PairReducingCollector(L leftIdentity, R rightIdentity, BinaryOperator leftReducer, BinaryOperator rightReducer) { 24 | this.leftIdentity = leftIdentity; 25 | this.rightIdentity = rightIdentity; 26 | this.leftReducer = leftReducer; 27 | this.rightReducer = rightReducer; 28 | } 29 | 30 | @Override 31 | public Supplier> supplier() { 32 | return () -> new MutablePair<>(leftIdentity, rightIdentity); 33 | } 34 | 35 | @Override 36 | public BiConsumer, Pair> accumulator() { 37 | return (acc, item) -> { 38 | acc.left = leftReducer.apply(acc.left, item.left); 39 | acc.right = rightReducer.apply(acc.right, item.right); 40 | }; 41 | } 42 | 43 | @Override 44 | public BinaryOperator> combiner() { 45 | return (acc1, acc2) -> { 46 | acc1.left = leftReducer.apply(acc1.left, acc2.left); 47 | acc1.right = rightReducer.apply(acc1.right, acc2.right); 48 | return acc1; 49 | }; 50 | } 51 | 52 | @Override 53 | public Function, Pair> finisher() { 54 | return acc -> Pair.of(acc.left, acc.right); 55 | } 56 | 57 | @Override 58 | public Set characteristics() { 59 | return EnumSet.noneOf(Characteristics.class); 60 | } 61 | 62 | static class MutablePair { 63 | L left; 64 | R right; 65 | 66 | private MutablePair(L left, R right) { 67 | this.left = left; 68 | this.right = right; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/pair/Pairs.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.pair; 2 | 3 | import co.unruly.control.result.Result; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.Optional; 8 | import java.util.function.BiFunction; 9 | import java.util.function.BinaryOperator; 10 | import java.util.function.Function; 11 | import java.util.function.IntFunction; 12 | import java.util.stream.Collector; 13 | import java.util.stream.Collectors; 14 | import java.util.stream.Stream; 15 | 16 | import static co.unruly.control.result.Result.failure; 17 | import static co.unruly.control.result.Result.success; 18 | import static java.util.stream.Collectors.toList; 19 | 20 | /** 21 | * Convenience functions on Pairs 22 | */ 23 | public interface Pairs { 24 | 25 | /** 26 | * Applies the given function to the left element of a Pair, returning a new Pair with the result of that 27 | * function as the left element and the original right element untouched 28 | */ 29 | static Function, Pair> onLeft(Function leftMapper) { 30 | return pair -> Pair.of(leftMapper.apply(pair.left), pair.right); 31 | } 32 | 33 | /** 34 | * Applies the given function to the right element of a Pair, returning a new Pair with the result of that 35 | * function as the right element and the original left element untouched 36 | */ 37 | static Function, Pair> onRight(Function rightMapper) { 38 | return pair -> Pair.of(pair.left, rightMapper.apply(pair.right)); 39 | } 40 | 41 | /** 42 | * Applies the given function to both elements off a Pair, assuming that both elements are of the 43 | * same type 44 | */ 45 | static Function, Pair> onBoth(Function f) { 46 | return pair -> Pair.of(f.apply(pair.left), f.apply(pair.right)); 47 | } 48 | 49 | /** 50 | * Applies the given function to both elements off a Pair, yielding a non-Pair value 51 | */ 52 | static Function, T> merge(BiFunction f) { 53 | return pair -> pair.then(f); 54 | } 55 | 56 | /** 57 | * Merges a Pair of Lists of T into a single List of T, with the left items at the front of the list. 58 | */ 59 | static Function, List>, List> mergeLists() { 60 | return pair -> Stream.of(pair.left, pair.right).flatMap(List::stream).collect(toList()); 61 | } 62 | 63 | /** 64 | * Collects a Stream of Pairs into a single Pair of lists, where a given index can be used to access the left 65 | * and right parts of the input pairs respectively. 66 | */ 67 | static Collector, Pair, List>, Pair, List>> toParallelLists() { 68 | return using(Collections::unmodifiableList, Collections::unmodifiableList); 69 | } 70 | 71 | /** 72 | * Collects a Stream of Pairs into a single Pair of arrays, where a given index can be used to access the left 73 | * and right parts of the input pairs respectively. 74 | */ 75 | static Collector, Pair, List>, Pair> toArrays(IntFunction leftArrayConstructor, IntFunction rightArrayConstructor) { 76 | return using( 77 | left -> left.stream().toArray(leftArrayConstructor), 78 | right -> right.stream().toArray(rightArrayConstructor) 79 | ); 80 | } 81 | 82 | /** 83 | * Reduces a stream of pairs to a single pair, using the provided identities and reducer functions 84 | */ 85 | static PairReducingCollector reducing( 86 | L leftIdentity, BinaryOperator leftReducer, 87 | R rightIdentity, BinaryOperator rightReducer) { 88 | return new PairReducingCollector<>(leftIdentity, rightIdentity, leftReducer, rightReducer); 89 | } 90 | 91 | 92 | static Collector, Pair, List>, Pair> using( 93 | Function, FL> leftFinisher, 94 | Function, FR> rightFinisher) { 95 | return new PairListCollector<>(leftFinisher, rightFinisher); 96 | } 97 | 98 | /** 99 | * If there are any elements in the right side of the Pair, return a failure of 100 | * the right side, otherwise return a success of the left. 101 | */ 102 | static Result, List> anyFailures(Pair, List> sides) { 103 | return sides.right.isEmpty() ? success(sides.left) : failure(sides.right); 104 | } 105 | 106 | /** 107 | * If there are any elements in the left side of the Pair, return a success of 108 | * the left side, otherwise return a failure of the left. 109 | */ 110 | static Result, List> anySuccesses(Pair, List> sides) { 111 | return sides.left.isEmpty() ? failure(sides.right) : success(sides.left); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/pair/Quad.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.pair; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Function; 5 | 6 | public class Quad { 7 | 8 | @FunctionalInterface 9 | public interface QuadFunction { 10 | T apply(A a, B b, C c, D d); 11 | } 12 | 13 | public final A first; 14 | public final B second; 15 | public final C third; 16 | public final D fourth; 17 | 18 | 19 | public Quad(A first, B second, C third, D fourth) { 20 | this.first = first; 21 | this.second = second; 22 | this.third = third; 23 | this.fourth = fourth; 24 | } 25 | 26 | public static Quad of(A first, B second, C third, D fourth) { 27 | return new Quad(first, second, third, fourth); 28 | } 29 | 30 | public A first() { 31 | return first; 32 | } 33 | 34 | public B second() { 35 | return second; 36 | } 37 | 38 | public C third() { 39 | return third; 40 | } 41 | 42 | public D fourth() { 43 | return fourth; 44 | } 45 | 46 | public T then(Function, T> function) { 47 | return function.apply(this); 48 | } 49 | 50 | public T then(QuadFunction function) { 51 | return function.apply(first, second, third, fourth); 52 | } 53 | 54 | @Override 55 | public boolean equals(Object o) { 56 | if (this == o) return true; 57 | if (o == null || getClass() != o.getClass()) return false; 58 | Quad quad = (Quad) o; 59 | return Objects.equals(first, quad.first) && 60 | Objects.equals(second, quad.second) && 61 | Objects.equals(third, quad.third) && 62 | Objects.equals(fourth, quad.fourth); 63 | } 64 | 65 | @Override 66 | public int hashCode() { 67 | return Objects.hash(first, second, third, fourth); 68 | } 69 | 70 | @Override 71 | public String toString() { 72 | return "Quad{" + 73 | "first=" + first + 74 | ", second=" + second + 75 | ", third=" + third + 76 | ", fourth=" + fourth + 77 | '}'; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/pair/Triple.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.pair; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Function; 5 | 6 | public class Triple { 7 | 8 | @FunctionalInterface 9 | public interface TriFunction { 10 | R apply(A a, B b, C c); 11 | } 12 | 13 | public final A first; 14 | public final B second; 15 | public final C third; 16 | 17 | public Triple(A first, B second, C third) { 18 | this.first = first; 19 | this.second = second; 20 | this.third = third; 21 | } 22 | 23 | public static Triple of(A first, B second, C third) { 24 | return new Triple<>(first, second, third); 25 | } 26 | 27 | public A first() { 28 | return first; 29 | } 30 | 31 | public B second() { 32 | return second; 33 | } 34 | 35 | public C third() { 36 | return third; 37 | } 38 | 39 | public T then(Function, T> function) { 40 | return function.apply(this); 41 | } 42 | 43 | public T then(TriFunction function) { 44 | return function.apply(first, second, third); 45 | } 46 | 47 | @Override 48 | public boolean equals(Object o) { 49 | if (this == o) return true; 50 | if (o == null || getClass() != o.getClass()) return false; 51 | Triple triple = (Triple) o; 52 | return Objects.equals(first, triple.first) && 53 | Objects.equals(second, triple.second) && 54 | Objects.equals(third, triple.third); 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | return Objects.hash(first, second, third); 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return "Triple{" + 65 | "first=" + first + 66 | ", second=" + second + 67 | ", third=" + third + 68 | '}'; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/result/Combiners.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import java.util.function.BiFunction; 4 | import java.util.function.Function; 5 | 6 | import static co.unruly.control.result.Result.success; 7 | 8 | public interface Combiners { 9 | 10 | /** 11 | * Combines two Results into a single Result. If both arguments are a Success, then 12 | * it applies the given function to their values and returns a Success of it. 13 | * 14 | * If either or both arguments are Failures, then this returns the first failure 15 | * it encountered. 16 | */ 17 | static Function, MergeableResults> combineWith(Result secondArgument) { 18 | // ugh ugh ugh we need an abstract class because otherwise it can't infer generics properly can i be sick now? ta 19 | return result -> new MergeableResults() { 20 | @Override 21 | public Result using(BiFunction combiner) { 22 | return result.either( 23 | s1 -> secondArgument.either( 24 | s2 -> success(combiner.apply(s1, s2)), 25 | Result::failure 26 | ), 27 | Result::failure 28 | ); 29 | } 30 | }; 31 | } 32 | 33 | @FunctionalInterface 34 | interface MergeableResults { 35 | Result using(BiFunction combiner); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/result/Introducers.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import co.unruly.control.ThrowingLambdas; 4 | 5 | import java.util.Map; 6 | import java.util.Optional; 7 | import java.util.function.Function; 8 | import java.util.function.Predicate; 9 | import java.util.function.Supplier; 10 | import java.util.stream.Stream; 11 | 12 | import static co.unruly.control.result.Result.failure; 13 | import static co.unruly.control.result.Result.success; 14 | import static co.unruly.control.result.Transformers.unwrapSuccesses; 15 | import static java.util.function.Function.identity; 16 | 17 | /** 18 | * A collection of sample functions which take regular values and output a Result. 19 | */ 20 | public interface Introducers { 21 | 22 | /** 23 | * Returns a Function which creates a new Success wrapping the provided value 24 | */ 25 | static Function> success() { 26 | return Result::success; 27 | } 28 | 29 | /** 30 | * Returns a Function which creates a new Failure wrapping the provided value 31 | */ 32 | static Function> failure() { 33 | return Result::failure; 34 | } 35 | 36 | /** 37 | * Returns a function which takes an Optional value, and returns a success of the 38 | * wrapped value if it was present, otherwise returns a failure using the provided Supplier 39 | */ 40 | static Function, Result> fromOptional(Supplier onEmpty) { 41 | return maybe -> maybe.map(Result::success).orElseGet(() -> Result.failure(onEmpty.get())); 42 | } 43 | 44 | /** 45 | * Returns a function which takes a value and checks a predicate on it: if the predicate passes, then 46 | * return a success of that value, otherwise apply the failure mapper to it 47 | */ 48 | static Function> ifFalse(Predicate test, Function failureMapper) { 49 | return val -> test.test(val) ? Result.success(val) : Result.failure(failureMapper.apply(val)); 50 | } 51 | 52 | /** 53 | * Returns a function which takes a value and checks a predicate on it: if the predicate passes, then 54 | * return a success of that value, otherwise return a failure of the provided value 55 | */ 56 | static Function> ifFalse(Predicate test, F failureValue) { 57 | return val -> test.test(val) ? Result.success(val) : Result.failure(failureValue); 58 | } 59 | 60 | /** 61 | * Returns a function which takes a value, applies the given function to it, and returns a 62 | * success of the returned value, unless it's null, when we return the given failure value 63 | */ 64 | static Function> ifNull(Function mapper, F failure) { 65 | return input -> { 66 | final S1 output = mapper.apply(input); 67 | return output == null ? Result.failure(failure) : Result.success(output); 68 | }; 69 | } 70 | 71 | /** 72 | * Returns a function which takes a value, applies the given function to it, and returns a 73 | * success of the returned value, unless it's null, when we return a failure of the given 74 | * function to the input value. 75 | */ 76 | static Function> ifNull(Function mapper, Function failureMapper) { 77 | return input -> { 78 | final S1 output = mapper.apply(input); 79 | return output == null ? Result.failure(failureMapper.apply(input)) : Result.success(output); 80 | }; 81 | } 82 | 83 | /** 84 | * Returns a function which takes a value, applies the given function to it, and returns a success of 85 | * the input unless the returned value matches the provided value. Otherwise, it returns a failure of the 86 | * failure value provided. 87 | * 88 | * Note that the success path returns a success of the original value, not the result of applying this 89 | * function. This can be used to build more complex predicates, or to check the return value of a 90 | * consumer-with-return-code. 91 | */ 92 | static Function> ifYields(Function checker, V value, F failure) { 93 | return input -> checker.apply(input) == value ? Result.failure(failure) : Result.success(input); 94 | } 95 | 96 | /** 97 | * Returns a function which takes a value, applies the given function to it, and returns a success of 98 | * the input unless the returned value matches the provided value. Otherwise, it returns a failure of the 99 | * input value applied to the failure mapping function. 100 | * 101 | * Note that the success path returns a success of the original value, not the result of applying this 102 | * function. This can be used to build more complex predicates, or to check the return value of a 103 | * consumer-with-return-code. 104 | */ 105 | static Function> ifYields(Function checker, V value, Function failureMapper) { 106 | return input -> checker.apply(input) == value ? Result.failure(failureMapper.apply(input)) : Result.success(input); 107 | } 108 | 109 | /** 110 | * Returns a function which takes a value, applies the provided function to it, and returns 111 | * a success of the output of that function. In the case where the function throws an exception, 112 | * that exception is passed to the provided exception-mapper, and the output of that call is the 113 | * failure value. 114 | * 115 | * Whilst we take a ThrowingFunction which throws a specific checked exception type X, our 116 | * eventual Result is of the more general type Exception. That's because it's also possible for the 117 | * function to throw other types of RuntimeException, and we have two choices: don't catch (or rethrow) 118 | * RuntimeException, or have a more general failure type. Rethrowing exceptions goes against the whole 119 | * point of constraining the error path, so we opt for the latter. 120 | * 121 | * If the provided function throws an Error, we don't catch that. Errors in general are not 122 | * intended to be caught. 123 | * 124 | * Note that idiomatic handling of Exceptions as failure type does allow specialised catch blocks 125 | * on specific exception types. 126 | */ 127 | static Function> tryTo( 128 | ThrowingLambdas.ThrowingFunction throwingFunction, 129 | Function exceptionMapper 130 | ) { 131 | return input -> { 132 | try { 133 | return Result.success(throwingFunction.apply(input)); 134 | } catch (Exception ex) { 135 | return Result.failure(exceptionMapper.apply(ex)); 136 | } 137 | }; 138 | } 139 | 140 | /** 141 | * Returns a function which takes a value, applies the provided function to it, and returns 142 | * a success of the output of that function, or a failure of the exception thrown by that function 143 | * if it threw an exception. 144 | * 145 | * Whilst we take a ThrowingFunction which throws a specific checked exception type X, our 146 | * eventual Result is of the more general type Exception. That's because it's also possible for the 147 | * function to throw other types of RuntimeException, and we have two choices: don't catch (or rethrow) 148 | * RuntimeException, or have a more general failure type. Rethrowing exceptions goes against the whole 149 | * point of constraining the error path, so we opt for the latter. 150 | * 151 | * If the provided function throws an Error, we don't catch that. Errors in general are not 152 | * intended to be caught. 153 | * 154 | * Note that idiomatic handling of Exceptions as failure type does allow specialised catch blocks 155 | * on specific exception types. 156 | */ 157 | static Function> tryTo( 158 | ThrowingLambdas.ThrowingFunction throwingFunction 159 | ) { 160 | return tryTo(throwingFunction, identity()); 161 | } 162 | 163 | /** 164 | * Returns a function which takes a value, applies the provided function to it, and returns 165 | * a success of the output of that function. If an exception is thrown, return a failure of 166 | * the specified failure case value. 167 | * 168 | * Whilst we take a ThrowingFunction which throws a specific checked exception type X, our 169 | * eventual Result is of the more general type Exception. That's because it's also possible for the 170 | * function to throw other types of RuntimeException, and we have two choices: don't catch (or rethrow) 171 | * RuntimeException, or have a more general failure type. Rethrowing exceptions goes against the whole 172 | * point of constraining the error path, so we opt for the latter. 173 | * 174 | * If the provided function throws an Error, we don't catch that. Errors in general are not 175 | * intended to be caught. 176 | * 177 | * Note that idiomatic handling of Exceptions as failure type does allow specialised catch blocks 178 | * on specific exception types. 179 | */ 180 | static Function> tryTo( 181 | ThrowingLambdas.ThrowingFunction throwingFunction, 182 | F failureCase 183 | ) { 184 | return tryTo(throwingFunction, __ -> failureCase); 185 | } 186 | 187 | 188 | /** 189 | * Returns a function which takes a value, applies the provided stream-returning function to it, 190 | * and return a stream which is the stream returned by the function, with each element wrapped in 191 | * a success, or a single failure of the exception thrown by that function if it threw an exception. 192 | */ 193 | static Function>> tryAndUnwrap(ThrowingLambdas.ThrowingFunction, X> f) { 194 | return tryTo(f).andThen(unwrapSuccesses()); 195 | } 196 | 197 | /** 198 | * Takes a class and returns a function which takes a value, attempts to cast it to that class, and returns 199 | * a Success of the provided type if it's a member of it, and a Failure of the known type otherwise, in both 200 | * cases containing the input value. 201 | * 202 | * This differs from exactCastTo in that exactCastTo will only return a Success if the given value is exactly 203 | * the target type, whereas this will also return a Success if it is a subtype of that type. 204 | */ 205 | @SuppressWarnings("unchecked") 206 | static Function> castTo(Class targetClass) { 207 | return input -> targetClass.isAssignableFrom(input.getClass()) 208 | ? Result.success((OS)input) 209 | : Result.failure(input); 210 | } 211 | 212 | /** 213 | * Takes a class and returns a function which takes a value, attempts to cast it to that class, and returns 214 | * a Success of the provided type if it's the same type as it, and a Failure of the known type otherwise, in both 215 | * cases containing the input value. 216 | * 217 | * This differs from castTo in that castTo will return a Success if the given value is a subtype of the target 218 | * type, whereas this will only return a Success if it is exactly that type. 219 | */ 220 | @SuppressWarnings("unchecked") 221 | static Function> exactCastTo(Class targetClass) { 222 | return input -> targetClass.equals(input.getClass()) 223 | ? Result.success((OS)input) 224 | : Result.failure(input); 225 | } 226 | 227 | 228 | /** 229 | * Takes a java.util.Map and a failure function, and returns a function which takes a key and returns 230 | * a success of the associated value in the Map, if present, or applies the failure function to the 231 | * key otherwise. 232 | */ 233 | static Function> fromMap(Map map, Function failureProvider) { 234 | return key -> { 235 | if(map.containsKey(key)) { 236 | return Result.success(map.get(key)); 237 | } else { 238 | return Result.failure(failureProvider.apply(key)); 239 | } 240 | }; 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/result/Match.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import java.util.function.Function; 4 | import java.util.stream.Stream; 5 | 6 | import static co.unruly.control.Piper.pipe; 7 | import static co.unruly.control.HigherOrderFunctions.compose; 8 | import static co.unruly.control.result.Resolvers.ifFailed; 9 | 10 | /** 11 | * A small DSL for building compact dispatch tables: better than if-expressions, worse than 12 | * proper pattern matching. But hey, it's Java, what do you expect? 13 | * 14 | * This models a match attempt as a sequence of operations on a Result, starting with a Failure 15 | * and continuously trying to use flatMapFailure to convert that Result into a Success. 16 | */ 17 | public class Match { 18 | 19 | /** 20 | * Builds a dispatch function from the provided matchers. Note that in order to yield 21 | * a function, the otherwise() method must be called on the result of this function: 22 | * as there's no way to determine if the dispatch table is complete, a base case is 23 | * required. 24 | */ 25 | @SafeVarargs 26 | public static MatchAttempt match(Function>... potentialMatchers) { 27 | return f -> attemptMatch(potentialMatchers).andThen(ifFailed(f)); 28 | } 29 | 30 | /** 31 | * Builds a dispatch function from the provided matchers. Note that this returns a Result, 32 | * as there's no way to determine if the dispatch table is complete: if no match is found, 33 | * returns a Failure of the input value. 34 | */ 35 | public static Function> attemptMatch(Function>... potentialMatchers) { 36 | return compose(Stream.of(potentialMatchers).map(Transformers::recover)).compose(Result::failure); 37 | } 38 | 39 | /** 40 | * Dispatches a value across the provided matchers. Note that in order to yield 41 | * a value, the otherwise() method must be called on the result of this function: 42 | * as there's no way to determine if the dispatch table is complete, a base case is 43 | * required. 44 | */ 45 | @SafeVarargs 46 | public static BoundMatchAttempt matchValue(I inputValue, Function>... potentialMatchers) { 47 | return f -> pipe(inputValue) 48 | .then(attemptMatch(potentialMatchers)) 49 | .then(ifFailed(f)) 50 | .resolve(); 51 | } 52 | 53 | @FunctionalInterface 54 | public interface MatchAttempt { 55 | Function otherwise(Function baseCase); 56 | } 57 | 58 | @FunctionalInterface 59 | public interface BoundMatchAttempt { 60 | O otherwise(Function baseCase); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/result/MonadicAliases.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import java.util.function.Function; 4 | 5 | import static co.unruly.control.result.Introducers.success; 6 | import static co.unruly.control.result.Transformers.*; 7 | 8 | /** 9 | * Aliases for standard functions on Results which use names more familiar 10 | * to users of Haskell 11 | */ 12 | public interface MonadicAliases { 13 | 14 | /** 15 | * Returns a function which converts a regular value into a Result (as a Success) 16 | */ 17 | static Function> pure() { 18 | return success(); 19 | } 20 | 21 | /** 22 | * Returns a function which, when applied to a Result, applies the provided function to 23 | * the wrapped value if it's a Success, otherwise perpetuates the existing failure 24 | */ 25 | static Function, Result> map(Function f) { 26 | return onSuccess(f); 27 | } 28 | 29 | /** 30 | * Returns a function which, when applied to a Result, applies the provided function to 31 | * the wrapped value, returning that Result, if it's a Success. This can turn a Success into a 32 | * Failure. 33 | * 34 | * If the result was already a failure, it perpetuates the existing failure. 35 | */ 36 | static Function, Result> flatMap(Function> f) { 37 | return attempt(f); 38 | } 39 | 40 | /** 41 | * Returns a function which, when applied to a Result, applies the provided function to 42 | * the wrapped value, returning that Result, if it's a Success. This can turn a Success into a 43 | * Failure. 44 | * 45 | * If the result was already a failure, it perpetuates the existing failure. 46 | */ 47 | static Function, Result> bind(Function> f) { 48 | return attempt(f); 49 | } 50 | 51 | /** 52 | * Returns a function which, when applied to a Result, applies the provided function to 53 | * the wrapped value if it's a failure, otherwise perpetuates the existing success 54 | */ 55 | static Function, Result> mapFailure(Function f) { 56 | return onFailure(f); 57 | } 58 | 59 | /** 60 | * Returns a function which, when applied to a Result, applies the provided function to 61 | * the wrapped value, returning that Result, if it's a Failure. This can turn a Failure into a 62 | * Success. 63 | * 64 | * If the result was already a success, it perpetuates the existing success. 65 | */ 66 | static Function, Result> flatMapFailure(Function> f) { 67 | return recover(f); 68 | } 69 | 70 | 71 | /** 72 | * Returns a function which, when applied to a Result, applies the provided function to 73 | * the wrapped value, returning that Result, if it's a Failure. This can turn a Failure into a 74 | * Success. 75 | * 76 | * If the result was already a success, it perpetuates the existing success. 77 | */ 78 | static Function, Result> bindFailure(Function> f) { 79 | return recover(f); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/result/Recover.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import java.util.Optional; 4 | import java.util.function.Function; 5 | import java.util.function.Predicate; 6 | import java.util.function.Supplier; 7 | 8 | /** 9 | * A collection of functions to (conditionally) recover a failure into a success. 10 | */ 11 | public interface Recover { 12 | 13 | /** 14 | * Returns a function which takes an Optional value, and returns a failure of the 15 | * wrapped value if it was present, otherwise returns a success using the provided Supplier 16 | */ 17 | static Function, Result> whenAbsent(Supplier onEmpty) { 18 | return maybe -> maybe.map(Result::failure).orElseGet(() -> Result.success(onEmpty.get())); 19 | } 20 | 21 | /** 22 | * Takes a class and a mapping function and returns a function which takes a value and, if it's of the 23 | * provided class, applies the mapping function to it and returns it as a Success, otherwise returning 24 | * the input value as a Failure. 25 | */ 26 | static Function> ifType(Class targetClass, Function mapper) { 27 | return Introducers.castTo(targetClass).andThen(Transformers.onSuccess(mapper)); 28 | } 29 | 30 | /** 31 | * Takes a predicate and a mapping function and returns a function which takes a value and, if it satisfies 32 | * the predicate, applies the mapping function to it and returns it as a Success, otherwise returning 33 | * the input value as a Failure. 34 | */ 35 | static Function> ifIs(Predicate test, Function mapper) { 36 | return input -> test.test(input) 37 | ? Result.success(mapper.apply(input)) 38 | : Result.failure(input); 39 | } 40 | 41 | /** 42 | * Takes a predicate and a mapping function and returns a function which takes a value and, if it doesn't 43 | * satisfy the predicate, applies the mapping function to it and returns it as a Success, otherwise returning 44 | * the input value as a Failure. 45 | */ 46 | static Function> ifNot(Predicate test, Function mapper) { 47 | return ifIs(test.negate(), mapper); 48 | } 49 | 50 | /** 51 | * Takes a value and a mapping function and returns a function which takes a value and, if it is equal to 52 | * the provided value, applies the mapping function to it and returns it as a Success, otherwise returning 53 | * the input value as a Failure. 54 | */ 55 | static Function> ifEquals(F expectedValue, Function mapper) { 56 | return input -> expectedValue.equals(input) 57 | ? Result.success(mapper.apply(input)) 58 | : Result.failure(input); 59 | } 60 | 61 | /** 62 | * Matches the value if the provided function yields an Optional whose value is 63 | * present, returning the value in that Optional. 64 | */ 65 | static Function> ifPresent(Function> successProvider) { 66 | return value -> successProvider 67 | .apply(value) 68 | .map(Result::success) 69 | .orElseGet(() -> Result.failure(value)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/result/Resolvers.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import co.unruly.control.pair.Pair; 4 | import co.unruly.control.pair.Pairs; 5 | 6 | import java.util.List; 7 | import java.util.Optional; 8 | import java.util.function.Function; 9 | import java.util.stream.Collector; 10 | import java.util.stream.Stream; 11 | 12 | import static java.util.Collections.unmodifiableList; 13 | import static java.util.function.Function.identity; 14 | import static java.util.stream.Stream.empty; 15 | 16 | /** 17 | * A set of common functions to convert from a Result to 18 | * an unwrapped value. 19 | */ 20 | public interface Resolvers { 21 | 22 | /** 23 | * Takes a Result where both success and failure types are the same, and returns 24 | * either the success or failure value as appropriate 25 | */ 26 | static Function, T> collapse() { 27 | return r -> r.either(identity(), identity()); 28 | } 29 | 30 | /** 31 | * Takes a Result and returns the success value if it is a success, or if it's 32 | * a failure, returns the result of applying the recovery function to the 33 | * failure value. 34 | */ 35 | static Function, OS> ifFailed(Function recoveryFunction) { 36 | return r -> r.either(identity(), recoveryFunction); 37 | } 38 | 39 | /** 40 | * Takes a Result for which the failure type is an Exception, and returns the 41 | * success value if it's a success, or throws the failure exception, wrapped in a 42 | * RuntimeException. 43 | */ 44 | static Function, S> getOrThrow() { 45 | return r -> r.either(identity(), ex -> { throw new RuntimeException(ex); }); 46 | } 47 | 48 | /** 49 | * Takes a Result and returns the success value if it is a success, or if it's 50 | * a failure, throws the result of applying the exception converter to the 51 | * failure value. 52 | */ 53 | static Function, S> getOrThrow(Function exceptionConverter) { 54 | return r -> r.either(identity(), failure -> { throw exceptionConverter.apply(failure); }); 55 | } 56 | 57 | /** 58 | * Returns a Stream of successes: a stream of a single value if this is a success, 59 | * or an empty stream if this is a failure. This is intended to be used to flat-map 60 | * over a stream of Results to extract a stream of just the successes. 61 | */ 62 | static Function, Stream> successes() { 63 | return r -> r.either(Stream::of, __ -> empty()); 64 | } 65 | 66 | /** 67 | * Returns a Stream of failures: a stream of a single value if this is a failure, 68 | * or an empty stream if this is a success. This is intended to be used to flat-map 69 | * over a stream of Results to extract a stream of just the failures. 70 | */ 71 | static Function, Stream> failures() { 72 | return r -> r.either(__ -> empty(), Stream::of); 73 | } 74 | 75 | /** 76 | * Returns an Optional success value, which is present if this result was a failure 77 | * and empty if it was a failure. 78 | */ 79 | static Function, Optional> toOptional() { 80 | return r -> r.either(Optional::of, __ -> Optional.empty()); 81 | } 82 | 83 | /** 84 | * Returns an Optional failure value, which is present if this result was a failure 85 | * and empty if it was a success. 86 | */ 87 | static Function, Optional> toOptionalFailure() { 88 | return r -> r.either(__ -> Optional.empty(), Optional::of); 89 | } 90 | 91 | /** 92 | * Collects a Stream of Results into a Pair of Lists, the left containing the unwrapped 93 | * success values, the right containing the unwrapped failures. 94 | */ 95 | static Collector, Pair, List>, Pair, List>> split() { 96 | return new ResultCollector<>(pair -> Pair.of(unmodifiableList(pair.left), unmodifiableList(pair.right))); 97 | } 98 | 99 | /** 100 | * Collects a Stream of Results into a Result which contains a List of Successes, if all results in 101 | * the stream were successful, or a list of Failures if any failed. 102 | */ 103 | static Collector, Pair, List>, Result, List>> allSucceeded() { 104 | return new ResultCollector<>(Pairs::anyFailures); 105 | } 106 | 107 | /** 108 | * Collects a Stream of Results into a Result which contains a List of Successes, if any results in 109 | * the stream were successful, or a list of Failures if all failed. 110 | */ 111 | static Collector, Pair, List>, Result, List>> anySucceeded() { 112 | return new ResultCollector<>(Pairs::anySuccesses); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/result/Result.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import java.io.Serializable; 4 | import java.util.Objects; 5 | import java.util.function.Function; 6 | 7 | /** 8 | * Represents the result of an operation which could fail, represented as either 9 | * a Success (wrapping the successful output) or a Failure (wrapping a value 10 | * describing how it failed). 11 | *

12 | * The interface for Result is minimal: many common sample operations are implemented 13 | * with static methods on Introducers, Transformers, and Resolvers. 14 | * 15 | * These can be composed upon a Result by passing them as arguments to then(). 16 | * 17 | * @param The type of a success 18 | * @param The type of a failure 19 | */ 20 | public abstract class Result implements Serializable { 21 | 22 | private Result() { 23 | } 24 | 25 | /** 26 | * Creates a new Success 27 | */ 28 | public static Result success(S value) { 29 | return new Success<>(value); 30 | } 31 | 32 | /** 33 | * Creates a new Success, taking the failure type for contexts where it can't be inferred. 34 | */ 35 | public static Result success(S value, Class failureType) { 36 | return new Success<>(value); 37 | } 38 | 39 | /** 40 | * Creates a new Failure 41 | */ 42 | public static Result failure(F error) { 43 | return new Failure<>(error); 44 | } 45 | 46 | /** 47 | * Creates a new Failure, taking the success type for contexts where it can't be inferred. 48 | */ 49 | public static Result failure(F error, Class successType) { 50 | return new Failure<>(error); 51 | } 52 | 53 | /** 54 | * Takes two functions, the first of which is executed in the case that this 55 | * Result is a Success, the second of which is executed in the case that it 56 | * is a Failure, on the wrapped value in either case. 57 | * 58 | * @param onSuccess the function to process the success value, if this is a Success 59 | * @param onFailure the function to process the failure value, if this is a Failure 60 | * @param the type of the end result 61 | * @return The result of executing onSuccess if this result is a Success, or onFailure if it's a failure 62 | */ 63 | public abstract R either(Function onSuccess, Function onFailure); 64 | 65 | /** 66 | * Applies a function to this Result. This permits inverting the calling convention, so that instead of the following: 67 | *

 68 |      * {@code
 69 |      * Result shop;
 70 |      * Result hat = map(shop, Shop::purchaseHat);
 71 |      * }
 72 |      * 
73 | *

74 | * We can write: 75 | *

 76 |      * {@code
 77 |      * Result shop;
 78 |      * Result hat = shop.then(map(Shop::purchaseHat));
 79 |      * }
 80 |      * 
81 | *

82 | * The advantage of this is that it composes more nicely: instead of this: 83 | *

 84 |      * {@code
 85 |      * Result town;
 86 |      * Result hat = map(map(shop, Town::findHatShop), Shop::purchaseHat);
 87 |      * }
 88 |      * 
89 | *

90 | * We can write: 91 | * *

 92 |      * {@code
 93 |      * Result town;
 94 |      * Result hat = town.then(map(Town::findHatShop)
 95 |      *                               .then(map(Shop::purchaseHat));
 96 |      * }
 97 |      * 
98 | */ 99 | public T then(Function, T2> biMapper) { 100 | return biMapper.apply(this); 101 | } 102 | 103 | private static final class Success extends Result { 104 | private final L value; 105 | 106 | private Success(L value) { 107 | this.value = value; 108 | } 109 | 110 | @Override 111 | public S either(Function onSuccess, Function onFailure) { 112 | return onSuccess.apply(value); 113 | } 114 | 115 | @Override 116 | public String toString() { 117 | return "Success{" + value + '}'; 118 | } 119 | 120 | @Override 121 | public boolean equals(Object o) { 122 | if (this == o) return true; 123 | if (o == null || getClass() != o.getClass()) return false; 124 | Success that = (Success) o; 125 | return Objects.equals(value, that.value); 126 | } 127 | 128 | @Override 129 | public int hashCode() { 130 | return Objects.hash(value); 131 | } 132 | } 133 | 134 | private static final class Failure extends Result { 135 | private final R value; 136 | 137 | private Failure(R value) { 138 | this.value = value; 139 | } 140 | 141 | @Override 142 | public S either(Function onSuccess, Function onFailure) { 143 | return onFailure.apply(value); 144 | } 145 | 146 | @Override 147 | public String toString() { 148 | return "Failure{" + value + '}'; 149 | } 150 | 151 | @Override 152 | public boolean equals(Object o) { 153 | if (this == o) return true; 154 | if (o == null || getClass() != o.getClass()) return false; 155 | Failure that = (Failure) o; 156 | return Objects.equals(value, that.value); 157 | } 158 | 159 | @Override 160 | public int hashCode() { 161 | return Objects.hash(value); 162 | } 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/result/ResultCollector.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import co.unruly.control.pair.Pair; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Set; 9 | import java.util.function.BiConsumer; 10 | import java.util.function.BinaryOperator; 11 | import java.util.function.Function; 12 | import java.util.function.Supplier; 13 | import java.util.stream.Collector; 14 | import java.util.stream.Stream; 15 | 16 | import static java.util.Collections.unmodifiableList; 17 | import static java.util.stream.Collectors.toList; 18 | 19 | /** 20 | * Collects a Stream of Results into a Pair, with the left being a list of success values 21 | * and the right being a list of failure values. 22 | */ 23 | class ResultCollector implements Collector, Pair, List>, T> { 24 | 25 | private final Function, List>, T> finisher; 26 | 27 | ResultCollector(Function, List>, T> finisher) { 28 | this.finisher = finisher; 29 | } 30 | 31 | @Override 32 | public Supplier, List>> supplier() { 33 | return () -> new Pair<>(new ArrayList<>(), new ArrayList<>()); 34 | } 35 | 36 | @Override 37 | public BiConsumer, List>, Result> accumulator() { 38 | return (accumulator, Result) -> Result.either(accumulator.left::add, accumulator.right::add); 39 | } 40 | 41 | @Override 42 | public BinaryOperator, List>> combiner() { 43 | return (x, y) -> Pair.of( 44 | Stream.of(x, y).flatMap(l -> l.left.stream()).collect(toList()), 45 | Stream.of(x, y).flatMap(r -> r.right.stream()).collect(toList()) 46 | ); 47 | } 48 | 49 | @Override 50 | public Function, List>, T> finisher() { 51 | return finisher; 52 | } 53 | 54 | 55 | @Override 56 | public Set characteristics() { 57 | return Collections.emptySet(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/result/Transformers.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import co.unruly.control.ConsumableFunction; 4 | import co.unruly.control.HigherOrderFunctions; 5 | import co.unruly.control.ThrowingLambdas; 6 | 7 | import java.util.function.Consumer; 8 | import java.util.function.Function; 9 | import java.util.stream.Stream; 10 | 11 | import static co.unruly.control.HigherOrderFunctions.peek; 12 | import static co.unruly.control.result.Introducers.tryTo; 13 | 14 | /** 15 | * A collection of functions which take a Result and return a Result. 16 | */ 17 | public interface Transformers { 18 | 19 | /** 20 | * Returns a function which takes a Result and, if it's a success, applies the mapping value to that 21 | * success, otherwise returning the original failure. 22 | */ 23 | static Function, Result> onSuccess(Function mappingFunction) { 24 | return attempt(mappingFunction.andThen(Result::success)); 25 | } 26 | 27 | /** 28 | * Returns a Consumer which takes a Result and, if it's a Success, passes it to the provided consumer. 29 | */ 30 | static ConsumableFunction> onSuccessDo(Consumer consumer) { 31 | return r -> r.then(onSuccess(peek(consumer))); 32 | } 33 | 34 | /** 35 | * Returns a function which takes a Result with an Exception failure type and, if it's a success, applies 36 | * the mapping value to that success, returning a new success unless an exception is thrown, when it 37 | * returns a failure of that exception. If the input was a failure, it returns that failure. 38 | */ 39 | static Function, Result> onSuccessTry( 40 | ThrowingLambdas.ThrowingFunction throwingFunction 41 | ) { 42 | return attempt(tryTo(throwingFunction)); 43 | } 44 | 45 | 46 | /** 47 | * Returns a function which takes a Result and, if it's a success, applies the mapping value 48 | * to that success, returning a new success unless an exception is thrown, when it 49 | * returns a failure of the exception-mapper applied to the exception. 50 | * If the input was a failure, it returns that failure. 51 | */ 52 | static Function, Result> onSuccessTry( 53 | ThrowingLambdas.ThrowingFunction throwingFunction, 54 | Function exceptionMapper 55 | ) { 56 | return attempt(tryTo(throwingFunction).andThen(onFailure(exceptionMapper))); 57 | } 58 | 59 | /** 60 | * Returns a function which takes a Result and, if it's a success, applies the provided function 61 | * to that success - generating a new Result - and returns that Result. Otherwise, returns the 62 | * original failure. 63 | */ 64 | static Function, Result> attempt(Function> mappingFunction) { 65 | return r -> r.either(mappingFunction.andThen(onFailure((Function) (fv) -> HigherOrderFunctions.upcast(fv))), Result::failure); 66 | } 67 | 68 | /** 69 | * Returns a function which takes a Result and, if it's a failure, applies the provided function 70 | * to that failure. Otherwise, returns the original success. 71 | */ 72 | static Function, Result> onFailure(Function mappingFunction) { 73 | return recover(mappingFunction.andThen(Result::failure)); 74 | } 75 | 76 | /** 77 | * Returns a consumer which takes a Result and, if it's a failure, passes it to the provided consumer 78 | */ 79 | static ConsumableFunction> onFailureDo(Consumer consumer) { 80 | return r -> r.then(onFailure(peek(consumer))); 81 | } 82 | 83 | /** 84 | * Takes a Result of a Stream as a success or a single failure, and returns a Stream of Results 85 | * containing all the successes, or the single failure. 86 | * 87 | * The main use-case for this is to follow mapping over tryTo() on a function which was designed to 88 | * be flat-mapped over. 89 | */ 90 | static Function, F>, Stream>> unwrapSuccesses() { 91 | return r -> r.either( 92 | successes -> successes.map(Result::success), 93 | failure -> Stream.of(Result.failure(failure)) 94 | ); 95 | } 96 | 97 | /** 98 | * Returns a function which takes a Result and, if it's a failure, applies the provided function to 99 | * that failure - generating a new Result - and returns that Result. Otherwise, return the original 100 | * success. 101 | */ 102 | static Function, Result> recover(Function> recoveryFunction) { 103 | return r -> r.either(Result::success, recoveryFunction.andThen(onSuccess((Function) (fv) -> HigherOrderFunctions.upcast(fv)))); 104 | } 105 | 106 | /** 107 | * Returns a function which takes a Result, and converts failures to successes and vice versa. 108 | */ 109 | static Function, Result> invert() { 110 | return r -> r.either(Result::failure, Result::success); 111 | } 112 | 113 | /** 114 | * Returns a function which takes a Result whose success type is itself a Result, and merges the failure cases 115 | * so we have a flat Result. 116 | * 117 | * Note that *most of the time* this shouldn't be required, and indicates using onSuccess() when attempt() would 118 | * be more appropriate. 119 | * 120 | * There are some situations though where we do end up with constructs like this: one example 121 | * is when a function which returns a Result can throw exceptions (eg, when a Result-returning handler is passed 122 | * into a database context). Passing that call into a tryTo() will yield a success type of the inner Result, wrapped 123 | * in an outer Result for the tryTo(). 124 | * 125 | * If the failure types of the inner and outer failure types do not match, you'll need to either first convert the 126 | * failures of the outer Result or use the overload which maps the failures of the inner Result. 127 | */ 128 | static Function, F>, Result> mergeFailures() { 129 | return attempt(i -> i); 130 | } 131 | 132 | /** 133 | * Returns a function which takes a Result whose success type is itself a Result, and merges the failure cases 134 | * so we have a flat Result. 135 | * 136 | * Note that *most of the time* this shouldn't be required, and indicates using onSuccess() when attempt() would 137 | * be more appropriate. 138 | * 139 | * There are some situations though where we do end up with constructs like this: one example 140 | * is when a function which returns a Result can throw exceptions (eg, when a Result-returning handler is passed 141 | * into a database context). Passing that call into a tryTo() will yield a success type of the inner Result, wrapped 142 | * in an outer Result for the tryTo(). 143 | * 144 | * In these cases, it's more likely the inner failure type is domain-specific, so the default approach is to map the 145 | * outer failure to the inner failure and then merge. 146 | */ 147 | static Function, F2>, Result> mergeFailures(Function failureMapper) { 148 | return r -> r.then(onFailure(failureMapper)).then(mergeFailures()); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/result/TypeOf.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import co.unruly.control.HigherOrderFunctions; 4 | 5 | import java.util.function.Function; 6 | 7 | import static co.unruly.control.result.Transformers.onSuccess; 8 | 9 | /** 10 | * Some syntax-fu in order to get nice, readable up-casting operations on Results. 11 | * 12 | * Usage isn't totally obvious from the implementation: to upcast a success to an Animal, for example, you need: 13 | * 14 | * using(TypeOf.forSuccesses()) 15 | * 16 | * 17 | * That'll give you a Function, Function> (inferring 18 | * the types Animal and String from context), which you can then use for mapping a Stream or use in 19 | * a Result then-operation chain. 20 | */ 21 | public interface TypeOf { 22 | 23 | /** 24 | * Generalises the success type for a Result to an appropriate superclass. 25 | */ 26 | static Function, Result> using(ForSuccesses dummy) { 27 | return result -> result.then(onSuccess(HigherOrderFunctions::upcast)); 28 | } 29 | 30 | /** 31 | * Generalises the failure type for a Result to an appropriate superclass. 32 | */ 33 | static Function, Result> using(ForFailures dummy) { 34 | return result -> result.then(Transformers.onFailure(HigherOrderFunctions::upcast)); 35 | } 36 | 37 | // we don't use the return value - all this does is provide type context 38 | static ForSuccesses forSuccesses() { 39 | return null; 40 | } 41 | 42 | // we don't use the return value - all this does is provide type context 43 | static ForFailures forFailures() { 44 | return null; 45 | } 46 | 47 | // this class only exists so we can differentiate the overloads of using() 48 | // we don't even instantiate it 49 | class ForSuccesses { } 50 | 51 | // this class only exists so we can differentiate the overloads of using() 52 | // we don't even instantiate it 53 | class ForFailures { } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/validation/FailedValidation.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.validation; 2 | 3 | import java.util.List; 4 | import java.util.Objects; 5 | 6 | public final class FailedValidation implements ForwardingList { 7 | 8 | public final T value; 9 | public final List errors; 10 | 11 | public FailedValidation(T value, List errors) { 12 | this.value = value; 13 | this.errors = errors; 14 | } 15 | 16 | @Override 17 | public String toString() { 18 | return "FailedValidation{" + 19 | "value=" + value + 20 | ", errors=" + errors + 21 | '}'; 22 | } 23 | 24 | @Override 25 | public boolean equals(Object o) { 26 | if (this == o) return true; 27 | if (o == null || getClass() != o.getClass()) return false; 28 | FailedValidation that = (FailedValidation) o; 29 | return Objects.equals(value, that.value) && 30 | Objects.equals(errors, that.errors); 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Objects.hash(value, errors); 36 | } 37 | 38 | @Override 39 | public List delegate() { 40 | return errors; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/validation/ForwardingList.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.validation; 2 | 3 | import java.util.*; 4 | import java.util.function.Consumer; 5 | import java.util.function.Predicate; 6 | import java.util.function.UnaryOperator; 7 | import java.util.stream.Stream; 8 | 9 | public interface ForwardingList extends List { 10 | 11 | List delegate(); 12 | 13 | default int size() { 14 | return delegate().size(); 15 | } 16 | 17 | default boolean isEmpty() { 18 | return delegate().isEmpty(); 19 | } 20 | 21 | default boolean contains(Object o) { 22 | return delegate().contains(o); 23 | } 24 | 25 | default Iterator iterator() { 26 | return delegate().iterator(); 27 | } 28 | 29 | default Object[] toArray() { 30 | return delegate().toArray(); 31 | } 32 | 33 | default T1[] toArray(T1[] a) { 34 | return delegate().toArray(a); 35 | } 36 | 37 | default boolean add(T t) { 38 | return delegate().add(t); 39 | } 40 | 41 | default boolean remove(Object o) { 42 | return delegate().remove(o); 43 | } 44 | 45 | default boolean containsAll(Collection c) { 46 | return delegate().containsAll(c); 47 | } 48 | 49 | default boolean addAll(Collection c) { 50 | return delegate().addAll(c); 51 | } 52 | 53 | default boolean addAll(int index, Collection c) { 54 | return delegate().addAll(index, c); 55 | } 56 | 57 | default boolean removeAll(Collection c) { 58 | return delegate().removeAll(c); 59 | } 60 | 61 | default boolean retainAll(Collection c) { 62 | return delegate().retainAll(c); 63 | } 64 | 65 | default void replaceAll(UnaryOperator operator) { 66 | delegate().replaceAll(operator); 67 | } 68 | 69 | default void sort(Comparator c) { 70 | delegate().sort(c); 71 | } 72 | 73 | default void clear() { 74 | delegate().clear(); 75 | } 76 | 77 | default T get(int index) { 78 | return delegate().get(index); 79 | } 80 | 81 | default T set(int index, T element) { 82 | return delegate().set(index, element); 83 | } 84 | 85 | default void add(int index, T element) { 86 | delegate().add(index, element); 87 | } 88 | 89 | default T remove(int index) { 90 | return delegate().remove(index); 91 | } 92 | 93 | default int indexOf(Object o) { 94 | return delegate().indexOf(o); 95 | } 96 | 97 | default int lastIndexOf(Object o) { 98 | return delegate().lastIndexOf(o); 99 | } 100 | 101 | default ListIterator listIterator() { 102 | return delegate().listIterator(); 103 | } 104 | 105 | default ListIterator listIterator(int index) { 106 | return delegate().listIterator(index); 107 | } 108 | 109 | default List subList(int fromIndex, int toIndex) { 110 | return delegate().subList(fromIndex, toIndex); 111 | } 112 | 113 | default Spliterator spliterator() { 114 | return delegate().spliterator(); 115 | } 116 | 117 | default boolean removeIf(Predicate filter) { 118 | return delegate().removeIf(filter); 119 | } 120 | 121 | default Stream stream() { 122 | return delegate().stream(); 123 | } 124 | 125 | default Stream parallelStream() { 126 | return delegate().parallelStream(); 127 | } 128 | 129 | default void forEach(Consumer action) { 130 | delegate().forEach(action); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/validation/Validator.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.validation; 2 | 3 | import co.unruly.control.result.Result; 4 | 5 | import java.util.List; 6 | import java.util.function.Function; 7 | import java.util.stream.Stream; 8 | 9 | import static java.util.stream.Collectors.toList; 10 | 11 | @FunctionalInterface 12 | public interface Validator extends Function>> { 13 | 14 | default Result> apply(T item) { 15 | List errors = validate(item).collect(toList()); 16 | return errors.isEmpty() 17 | ? Result.success(item) 18 | : Result.failure(new FailedValidation(item, errors)); 19 | } 20 | 21 | Stream validate(T item); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/validation/Validators.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.validation; 2 | 3 | 4 | import co.unruly.control.Optionals; 5 | import co.unruly.control.ThrowingLambdas.ThrowingFunction; 6 | import co.unruly.control.result.Result; 7 | 8 | import java.util.Arrays; 9 | import java.util.function.BiFunction; 10 | import java.util.function.Function; 11 | import java.util.function.Predicate; 12 | import java.util.stream.Collectors; 13 | import java.util.stream.Stream; 14 | import java.util.stream.StreamSupport; 15 | 16 | import static co.unruly.control.result.Result.failure; 17 | import static co.unruly.control.result.Result.success; 18 | import static co.unruly.control.result.Transformers.onFailure; 19 | import static co.unruly.control.result.Transformers.recover; 20 | 21 | public interface Validators { 22 | 23 | @SafeVarargs 24 | public static Validator compose(Validator... validators) { 25 | return t -> Arrays.stream(validators).flatMap(v -> v.validate(t)); 26 | } 27 | 28 | public static Validator rejectIf(Predicate test, E error) { 29 | return acceptIf(test.negate(), error); 30 | } 31 | 32 | public static Validator rejectIf(Predicate test, Function errorGenerator) { 33 | return acceptIf(test.negate(), errorGenerator); 34 | } 35 | 36 | public static Validator acceptIf(Predicate test, E error) { 37 | return acceptIf(test, t -> error); 38 | } 39 | 40 | public static Validator acceptIf(Predicate test, Function errorGenerator) { 41 | return t -> test.test(t) ? Stream.empty() : Stream.of(errorGenerator.apply(t)); 42 | } 43 | 44 | public static Validator firstOf(Validator validator) { 45 | return t -> Optionals.stream(validator.validate(t).findFirst()); 46 | } 47 | 48 | public static Validator onlyIf(Predicate test, Validator validator) { 49 | return t -> test.test(t) ? validator.validate(t) : Stream.empty(); 50 | } 51 | 52 | public static Validator mappingErrors(Validator validator, BiFunction errorMapper) { 53 | return t -> validator.validate(t).map(e -> errorMapper.apply(t, e)); 54 | } 55 | 56 | public static Validator on(Function accessor, Validator innerValidator) { 57 | return t -> innerValidator.validate(accessor.apply(t)); 58 | } 59 | 60 | public static Validator tryOn(ThrowingFunction accessor, Function onException, Validator innerValidator) { 61 | return t -> { 62 | try { 63 | return innerValidator.validate(accessor.apply(t)); 64 | } catch (Exception e) { 65 | return Stream.of(onException.apply(e)); 66 | } 67 | }; 68 | } 69 | 70 | public static Validator onEach(Function> iterator, Validator innerValidator) { 71 | return t -> StreamSupport.stream(iterator.apply(t).spliterator(), false).flatMap(innerValidator::validate); 72 | } 73 | 74 | public static Validator tryTo(Validator validatorWhichThrowsRuntimeExceptions, Function errorMapper) { 75 | return t -> { 76 | try { 77 | return validatorWhichThrowsRuntimeExceptions.validate(t); 78 | } catch (RuntimeException ex) { 79 | return Stream.of(errorMapper.apply(ex)); 80 | } 81 | }; 82 | } 83 | 84 | public static Function>, Result>> ignoreWhen(Predicate filterCondition) { 85 | return result -> result 86 | .then(onFailure(fv -> new FailedValidation<>(fv.value, fv.errors.stream().filter(filterCondition.negate()).collect(Collectors.toList())))) 87 | .then(recover(fv -> fv.errors.isEmpty() ? success(fv.value) : failure(fv))); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/co/unruly/control/ThrowingLambdasTest.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.function.Predicate; 6 | 7 | import static co.unruly.control.ThrowingLambdas.ThrowingPredicate.throwingRuntime; 8 | import static org.junit.Assert.assertTrue; 9 | 10 | public class ThrowingLambdasTest { 11 | 12 | // @Test 13 | // public void cannotCompileWhenPassingLambdaThatThrows() throws Exception { 14 | // assertTrue(test(2, ThrowingLambdasTest::dodgyIsEven)); 15 | // } 16 | 17 | @Test 18 | public void canHandleThrowingMethodsWithAppropriateFunctionalInterfaceType() { 19 | assertTrue(tryToTest(2, ThrowingLambdasTest::dodgyIsEven)); 20 | } 21 | 22 | @Test 23 | public void canHandleMultiThrowingMethodsWithAppropriateFunctionalInterfaceType() { 24 | assertTrue(tryToTest(2, ThrowingLambdasTest::veryDodgyIsEven)); 25 | } 26 | 27 | @Test 28 | public void canConvertThrowingLambdasToNonThrowingLambdas() throws Exception { 29 | assertTrue(test(2, throwingRuntime(ThrowingLambdasTest::dodgyIsEven))); 30 | } 31 | 32 | @Test 33 | public void canConvertMultiThrowingLambdasToNonThrowingLambdas() throws Exception { 34 | assertTrue(test(2, throwingRuntime(ThrowingLambdasTest::veryDodgyIsEven))); 35 | } 36 | 37 | private static boolean test(T item, Predicate test) { 38 | return test.test(item); 39 | } 40 | 41 | private static boolean tryToTest(T item, ThrowingLambdas.ThrowingPredicate test) { 42 | try { 43 | return test.test(item); 44 | } catch (Exception e) { 45 | return false; 46 | } 47 | } 48 | 49 | private static boolean dodgyIsEven(int i) throws Exception { 50 | return i % 2 == 0; 51 | } 52 | 53 | private static boolean veryDodgyIsEven(int i) throws FirstException, SecondException { 54 | return i % 2 == 0; 55 | } 56 | 57 | private static class FirstException extends Exception {} 58 | private static class SecondException extends Exception {} 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/co/unruly/control/ZipTest.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control; 2 | 3 | import co.unruly.control.pair.Pair; 4 | import org.junit.Test; 5 | 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.Stream; 9 | 10 | import static co.unruly.control.HigherOrderFunctions.withIndices; 11 | import static co.unruly.control.HigherOrderFunctions.zip; 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.Matchers.contains; 14 | 15 | public class ZipTest { 16 | 17 | @Test 18 | public void zipsItemsTogether() { 19 | List> pairs = zip( 20 | Stream.of("hello", "goodbye"), 21 | Stream.of("world", "cruel world")) 22 | .collect(Collectors.toList()); 23 | 24 | assertThat(pairs, contains(Pair.of("hello", "world"), Pair.of("goodbye", "cruel world"))); 25 | } 26 | 27 | @Test 28 | public void zipsItemsTogetherUsingFunction() { 29 | List pairs = zip( 30 | Stream.of(1,2,3,4,5), 31 | Stream.of(1,10,100,1000,10000), 32 | (x, y) -> x * y) 33 | .collect(Collectors.toList()); 34 | 35 | assertThat(pairs, contains(1, 20, 300, 4000, 50000)); 36 | } 37 | 38 | @Test 39 | public void generatedStreamIsShorterOfInputStreams() { 40 | List pairs = zip( 41 | Stream.of(1,2,3), 42 | Stream.of(1,10,100,1000,10000), 43 | (x, y) -> x * y) 44 | .collect(Collectors.toList()); 45 | 46 | assertThat(pairs, contains(1, 20, 300)); 47 | } 48 | 49 | @Test 50 | public void buildsIndexedList() { 51 | List> indexed = withIndices(Stream.of("zero", "one", "two", "three")) 52 | .collect(Collectors.toList()); 53 | 54 | assertThat(indexed, contains(Pair.of(0, "zero"), Pair.of(1, "one"), Pair.of(2, "two"), Pair.of(3, "three"))); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/co/unruly/control/pair/PairTest.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.pair; 2 | 3 | import org.hamcrest.CoreMatchers; 4 | import org.junit.Test; 5 | 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Optional; 10 | import java.util.stream.Stream; 11 | 12 | import static co.unruly.control.pair.Maps.entry; 13 | import static co.unruly.control.pair.Maps.mapOf; 14 | import static co.unruly.control.pair.Maps.toMap; 15 | import static co.unruly.control.pair.Pairs.*; 16 | import static co.unruly.control.pair.Comprehensions.allOf; 17 | import static co.unruly.control.pair.Comprehensions.onAll; 18 | import static java.util.Arrays.asList; 19 | import static java.util.stream.Collectors.toList; 20 | import static org.hamcrest.CoreMatchers.is; 21 | import static org.hamcrest.MatcherAssert.assertThat; 22 | 23 | public class PairTest { 24 | 25 | @Test 26 | public void canTransformStreamOfPairs() { 27 | List> transformedPairs = Stream.of(Pair.of(2, "hello"), Pair.of(4, "goodbye")) 28 | .map(onLeft(x -> x * 3)) 29 | .map(onRight(String::toUpperCase)) 30 | .collect(toList()); 31 | 32 | assertThat(transformedPairs, CoreMatchers.hasItems(Pair.of(6, "HELLO"), Pair.of(12, "GOODBYE"))); 33 | } 34 | 35 | @Test 36 | public void canTransformAnIndividualPair() { 37 | Pair transformedPair = Pair.of(2, "hello") 38 | .then(onLeft(x -> x * 3)) 39 | .then(onRight(String::toUpperCase)); 40 | 41 | assertThat(transformedPair, is(Pair.of(6, "HELLO"))); 42 | } 43 | 44 | @Test 45 | public void canCollectToParallelLists() { 46 | Pair, List> parallelLists = Stream.of(Pair.of(2, "hello"), Pair.of(4, "goodbye")) 47 | .collect(toParallelLists()); 48 | 49 | assertThat(parallelLists, is(Pair.of(asList(2, 4), asList("hello", "goodbye")))); 50 | } 51 | 52 | @Test 53 | public void canCollectToParallelArrays() { 54 | Pair parallelArrays = Stream.of(Pair.of(2, "hello"), Pair.of(4, "goodbye")) 55 | .collect(toArrays(Integer[]::new, String[]::new)); 56 | 57 | assertThat(asList(parallelArrays.left), is(asList(2, 4))); 58 | assertThat(asList(parallelArrays.right), is(asList("hello", "goodbye"))); 59 | } 60 | 61 | @Test 62 | public void canReduceAStreamOfPairs() { 63 | Pair reduced = Stream.of(Pair.of(2, "hello"), Pair.of(4, "goodbye")) 64 | .collect(reducing( 65 | 0, (x, y) -> x + y, 66 | "", String::concat 67 | )); 68 | 69 | assertThat(reduced, is(Pair.of(6, "hellogoodbye"))); 70 | } 71 | 72 | @Test 73 | public void canCreateMaps() { 74 | 75 | Map expectedMap = new HashMap<>(); 76 | expectedMap.put("hello", "world"); 77 | expectedMap.put("six of one", "half a dozen of the other"); 78 | 79 | Map actualMap = mapOf( 80 | entry("hello", "world"), 81 | entry("six of one", "half a dozen of the other") 82 | ); 83 | 84 | assertThat(actualMap, is(expectedMap)); 85 | } 86 | 87 | @Test 88 | public void canCollectPairsIntoMap() { 89 | 90 | Map expectedMap = new HashMap<>(); 91 | expectedMap.put("hello", "world"); 92 | expectedMap.put("six of one", "half a dozen of the other"); 93 | 94 | Map actualMap = Stream.of( 95 | entry("hello", "world"), 96 | entry("six of one", "half a dozen of the other") 97 | ).collect(toMap()); 98 | 99 | assertThat(actualMap, is(expectedMap)); 100 | } 101 | 102 | @Test 103 | public void canAggregateOptionalPairs() { 104 | Optional actual = allOf( 105 | Optional.of("hello"), 106 | Optional.of("world") 107 | ).map(onAll((a, b) -> a + ", " + b)); 108 | 109 | assertThat(actual, is(Optional.of("hello, world"))); 110 | } 111 | 112 | @Test 113 | public void canAggregateOptionalTriples() { 114 | Optional actual = allOf( 115 | Optional.of("piff"), 116 | Optional.of("paff"), 117 | Optional.of("poff") 118 | ).map(onAll((a, b, c) -> a + ", " + b + ", " + c)); 119 | 120 | assertThat(actual, is(Optional.of("piff, paff, poff"))); 121 | } 122 | 123 | @Test 124 | public void whenAggregatingOptionalPairsEitherEmptyYieldsAnEmptyResult() { 125 | Optional firstEmpty = allOf(Optional.empty(), Optional.of("world")) 126 | .map(onAll((a, b) -> a + ", " + b)); 127 | 128 | Optional secondEmpty = allOf(Optional.of("hello"), Optional.empty()) 129 | .map(onAll((a, b) -> a + ", " + b)); 130 | 131 | 132 | assertThat(firstEmpty, is(Optional.empty())); 133 | assertThat(secondEmpty, is(Optional.empty())); 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /src/test/java/co/unruly/control/result/CastsTest.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import org.junit.Test; 4 | 5 | import static co.unruly.control.Piper.pipe; 6 | import static co.unruly.control.matchers.ResultMatchers.isFailureOf; 7 | import static co.unruly.control.matchers.ResultMatchers.isSuccessOf; 8 | import static co.unruly.control.result.Introducers.castTo; 9 | import static org.junit.Assert.assertThat; 10 | 11 | public class CastsTest { 12 | 13 | @Test 14 | public void castingToCorrectTypeYieldsSuccess() { 15 | final Object helloWorld = "Hello World"; 16 | 17 | Result cast = pipe(helloWorld) 18 | .resolveWith(castTo(String.class)); 19 | 20 | assertThat(cast, isSuccessOf("Hello World")); 21 | } 22 | 23 | @Test 24 | public void castingToIncorrectTypeYieldsFailure() { 25 | final Object helloWorld = "Hello World"; 26 | 27 | Result cast = pipe(helloWorld) 28 | .resolveWith(castTo(Integer.class)); 29 | 30 | assertThat(cast, isFailureOf("Hello World")); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/co/unruly/control/result/MatchTest.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.Optional; 6 | import java.util.function.Function; 7 | 8 | import static co.unruly.control.result.Match.match; 9 | import static co.unruly.control.result.Match.matchValue; 10 | import static co.unruly.control.result.Recover.*; 11 | import static org.hamcrest.CoreMatchers.is; 12 | import static org.junit.Assert.assertThat; 13 | 14 | public class MatchTest { 15 | 16 | @Test 17 | public void canMatchOnTypeWithFlowTyping() { 18 | Function matchByType = match( 19 | ifType(B.class, B::messageForB), 20 | ifType(C.class, C::messageForC) 21 | ).otherwise(A::message); 22 | 23 | assertThat(matchByType.apply(new A("Cheese")), is("Cheese")); 24 | assertThat(matchByType.apply(new B("Ketchup")), is("I'm a B and I say Ketchup")); 25 | assertThat(matchByType.apply(new C("Pickles")), is("I'm a C and I say Pickles")); 26 | } 27 | 28 | @Test 29 | public void canMatchOnValue() { 30 | Function matchByType = match( 31 | ifEquals(4, x -> x + " sure looks like a 4 to me!"), 32 | ifEquals(7, x -> x + " looks like one of them gosh-darned 7s?") 33 | ).otherwise(x -> "I have no idea what a " + x + " is though..."); 34 | 35 | assertThat(matchByType.apply(3), is("I have no idea what a 3 is though...")); 36 | assertThat(matchByType.apply(4), is("4 sure looks like a 4 to me!")); 37 | assertThat(matchByType.apply(6), is("I have no idea what a 6 is though...")); 38 | assertThat(matchByType.apply(7), is("7 looks like one of them gosh-darned 7s?")); 39 | } 40 | 41 | @Test 42 | public void canMatchOnTest() { 43 | Function matchByType = match( 44 | ifIs((Integer x) -> x % 2 == 0, x -> x + ", well, that's one of those even numbers"), 45 | ifIs(x -> x < 0, x -> x + " is one of those banker's negative number thingies") 46 | ).otherwise(x -> x + " is a regular, god-fearing number for god-fearing folks"); 47 | 48 | assertThat(matchByType.apply(2), is("2, well, that's one of those even numbers")); 49 | assertThat(matchByType.apply(-6), is("-6, well, that's one of those even numbers")); 50 | assertThat(matchByType.apply(3), is("3 is a regular, god-fearing number for god-fearing folks")); 51 | assertThat(matchByType.apply(-9), is("-9 is one of those banker's negative number thingies")); 52 | } 53 | 54 | @Test 55 | public void canMatchOnTestPassingArgument() { 56 | String matchByResult = matchValue(4, 57 | ifIs((Integer x) -> x % 2 == 0, x -> x + ", well, that's one of those even numbers"), 58 | ifIs(x -> x < 0, x -> x + " is one of those banker's negative number thingies") 59 | ).otherwise(x -> x + " is a regular, god-fearing number for god-fearing folks"); 60 | 61 | assertThat(matchByResult, is("4, well, that's one of those even numbers")); 62 | } 63 | 64 | @Test 65 | public void canOperateOverAListOfOptionalProviders() { 66 | String cheese = matchValue(new Things(null, "Cheese!", "Bacon!"), 67 | ifPresent(Things::a), 68 | ifPresent(Things::b), 69 | ifPresent(Things::c) 70 | ).otherwise(__ -> "Ketchup!"); 71 | 72 | assertThat(cheese, is("Cheese!")); 73 | } 74 | 75 | 76 | @Test 77 | public void usesDefaultIfNoOptionalProvidersProvideAValue() { 78 | String cheese = matchValue(new Things(null, null, null), 79 | ifPresent(Things::a), 80 | ifPresent(Things::b), 81 | ifPresent(Things::c) 82 | ).otherwise(__ -> "Ketchup!"); 83 | 84 | assertThat(cheese, is("Ketchup!")); 85 | } 86 | 87 | @Test 88 | public void useMatchToCalculateFactorial() { 89 | assertThat(factorial(0), is(1)); 90 | assertThat(factorial(1), is(1)); 91 | assertThat(factorial(6), is(720)); 92 | } 93 | 94 | @Test(expected = IllegalArgumentException.class) 95 | public void factorialOfNegativeNumberThrowsIllegalArgumentException() { 96 | factorial(-1); 97 | } 98 | 99 | private static int factorial(int number) { 100 | return matchValue(number, 101 | ifIs(n -> n < 0, n -> { throw new IllegalArgumentException("Cannot calculate factorial of a negative number"); }), 102 | ifEquals(0, n -> 1) 103 | ).otherwise(n -> n * factorial(n-1)); 104 | } 105 | 106 | static class A { 107 | private final String msg; 108 | 109 | 110 | A(String msg) { 111 | this.msg = msg; 112 | } 113 | 114 | String message() { 115 | return msg; 116 | } 117 | } 118 | 119 | static class B extends A { 120 | 121 | B(String msg) { 122 | super(msg); 123 | } 124 | 125 | String messageForB() { 126 | return "I'm a B and I say " + message(); 127 | } 128 | } 129 | 130 | static class C extends A { 131 | 132 | C(String msg) { 133 | super(msg); 134 | } 135 | 136 | String messageForC() { 137 | return "I'm a C and I say " + message(); 138 | } 139 | } 140 | 141 | static class Things { 142 | final String a; 143 | final String b; 144 | final String c; 145 | 146 | 147 | Things(String a, String b, String c) { 148 | this.a = a; 149 | this.b = b; 150 | this.c = c; 151 | } 152 | 153 | Optional a() { 154 | return Optional.ofNullable(a); 155 | } 156 | 157 | 158 | Optional b() { 159 | return Optional.ofNullable(b); 160 | } 161 | 162 | 163 | Optional c() { 164 | return Optional.ofNullable(c); 165 | } 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /src/test/java/co/unruly/control/result/PiperTest.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | import static co.unruly.control.Piper.pipe; 9 | import static co.unruly.control.matchers.ResultMatchers.isFailureOf; 10 | import static co.unruly.control.matchers.ResultMatchers.isSuccessOf; 11 | import static co.unruly.control.result.Recover.ifIs; 12 | import static co.unruly.control.result.Introducers.tryTo; 13 | import static co.unruly.control.result.Transformers.attempt; 14 | import static co.unruly.control.result.Transformers.onFailure; 15 | import static org.junit.Assert.assertThat; 16 | 17 | public class PiperTest { 18 | 19 | @Test 20 | public void canChainSeveralOperationsBeforeOneWhichMayFail() { 21 | Pattern pattern = Pattern.compile("a=([^;]+);"); 22 | 23 | Result result = pipe("a=1234;") 24 | .then(pattern::matcher) 25 | .then(ifIs(Matcher::find, m -> m.group(1))) 26 | .then(onFailure(__ -> "Could not find group to match")) 27 | .then(attempt(tryTo(Integer::parseInt, ex -> ex.getMessage()))) 28 | .resolve(); 29 | 30 | assertThat(result, isSuccessOf(1234)); 31 | } 32 | 33 | @Test 34 | public void canChainSeveralOperationsBeforeOneWhichMayFail2() { 35 | Pattern pattern = Pattern.compile("a=([^;]);"); 36 | 37 | Result result = pipe("cheeseburger") 38 | .then(pattern::matcher) 39 | .then(ifIs(Matcher::find, m -> m.group(1))) 40 | .then(onFailure(__ -> "Could not find group to match")) 41 | .then(attempt(tryTo(Integer::parseInt, ex -> ex.getMessage()))) 42 | .resolve(); 43 | 44 | assertThat(result, isFailureOf("Could not find group to match")); 45 | } 46 | 47 | @Test 48 | public void canChainSeveralOperationsBeforeOneWhichMayFail3() { 49 | Pattern pattern = Pattern.compile("a=([^;]);"); 50 | 51 | Result result = pipe("a=a;") 52 | .then(pattern::matcher) 53 | .then(ifIs(Matcher::find, m -> m.group(1))) 54 | .then(onFailure(__ -> "Could not find group to match")) 55 | .then(attempt(tryTo(Integer::parseInt, ex -> "Parse failure: " + ex.getMessage()))) 56 | .resolve(); 57 | 58 | assertThat(result, isFailureOf("Parse failure: For input string: \"a\"")); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/co/unruly/control/result/ResultsTest.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import co.unruly.control.Lists; 4 | import co.unruly.control.pair.Comprehensions; 5 | import co.unruly.control.pair.Pair; 6 | import org.hamcrest.core.Is; 7 | import org.junit.Test; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.function.Consumer; 12 | import java.util.function.Function; 13 | import java.util.stream.Stream; 14 | 15 | import static co.unruly.control.matchers.ResultMatchers.isFailureOf; 16 | import static co.unruly.control.matchers.ResultMatchers.isSuccessOf; 17 | import static co.unruly.control.pair.Comprehensions.ifAllSucceeded; 18 | import static co.unruly.control.pair.Maps.entry; 19 | import static co.unruly.control.pair.Maps.mapOf; 20 | import static co.unruly.control.result.Combiners.combineWith; 21 | import static co.unruly.control.result.Introducers.fromMap; 22 | import static co.unruly.control.result.Introducers.tryTo; 23 | import static co.unruly.control.result.Resolvers.*; 24 | import static co.unruly.control.result.Result.failure; 25 | import static co.unruly.control.result.Result.success; 26 | import static co.unruly.control.result.Transformers.*; 27 | import static java.util.Arrays.asList; 28 | import static java.util.stream.Collectors.toList; 29 | import static org.hamcrest.CoreMatchers.hasItems; 30 | import static org.hamcrest.core.Is.is; 31 | import static org.junit.Assert.assertThat; 32 | import static org.mockito.Mockito.*; 33 | 34 | 35 | public class ResultsTest { 36 | 37 | @Test 38 | public void canCreateSuccess() { 39 | Result shouldBeSuccess = success(5); 40 | 41 | assertThat(shouldBeSuccess.either(success -> true, failure -> false), is(true)); 42 | } 43 | 44 | @Test 45 | public void canCreateFailure() { 46 | Result shouldFail = failure("oh poop"); 47 | 48 | assertThat(shouldFail.either(success -> true, failure -> false), is(false)); 49 | } 50 | 51 | @Test 52 | public void canReduceResultToValue() { 53 | Result success = success(5); 54 | Result failure = failure("i blew up"); 55 | 56 | assertThat(success.either( 57 | succ -> String.format("I got %d out of this Result", succ), 58 | err -> err), 59 | is("I got 5 out of this Result")); 60 | 61 | assertThat(failure.either( 62 | succ -> String.format("I got %d out of this Result", succ), 63 | err -> err), 64 | is("i blew up")); 65 | } 66 | 67 | @Test 68 | public void canDoSideEffectsOnCorrectSideForSuccess() { 69 | final Consumer successCallback = mock(Consumer.class); 70 | final Consumer failureCallback = mock(Consumer.class); 71 | 72 | Result success = success(5); 73 | 74 | success 75 | .then(onSuccessDo(successCallback)) 76 | .then(onFailureDo(failureCallback)); 77 | 78 | verify(successCallback).accept(5); 79 | verifyZeroInteractions(failureCallback); 80 | } 81 | 82 | @Test 83 | public void canDoSideEffectsOnCorrectSideForFailure() { 84 | final Consumer successCallback = mock(Consumer.class); 85 | final Consumer failureCallback = mock(Consumer.class); 86 | 87 | Result failure = failure("oops"); 88 | 89 | failure 90 | .then(onSuccessDo(successCallback)) 91 | .then(onFailureDo(failureCallback)); 92 | 93 | verify(failureCallback).accept("oops"); 94 | verifyZeroInteractions(successCallback); 95 | } 96 | 97 | @Test 98 | public void flatMapsSuccessesIntoAppropriateValues() { 99 | final Function> halve = num -> 100 | num % 2 == 0 ? success(num / 2) : failure("Cannot halve an odd number into an integer"); 101 | 102 | final Result six = success(6); 103 | final Result five = success(5); 104 | final Result failure = failure("Cannot parse number"); 105 | 106 | 107 | assertThat(six.then(attempt(halve)), isSuccessOf(3)); 108 | assertThat(five.then(attempt(halve)), isFailureOf("Cannot halve an odd number into an integer")); 109 | assertThat(failure.then(attempt(halve)), isFailureOf("Cannot parse number")); 110 | } 111 | 112 | @Test 113 | public void canMapSuccesses() { 114 | final Result six = success(6); 115 | final Result failure = failure("Cannot parse number"); 116 | 117 | final Result twelve = six.then(onSuccess(x -> x * 2)); 118 | final Result stillFailure = failure.then(onSuccess(x -> x * 2)); 119 | 120 | assertThat(twelve, isSuccessOf(12)); 121 | assertThat(stillFailure, is(failure)); 122 | } 123 | 124 | @Test 125 | public void canMapFailures() { 126 | final Result six = success(6); 127 | final Result failure = failure("Cannot parse number"); 128 | 129 | final Result stillSix = six.then(onFailure(String::toLowerCase)); 130 | final Result lowerCaseFailure = failure.then(onFailure(String::toLowerCase)); 131 | 132 | assertThat(stillSix, Is.is(success(6))); 133 | assertThat(lowerCaseFailure, Is.is(failure("cannot parse number"))); 134 | } 135 | 136 | @Test 137 | public void canMapOverExceptionThrowingMethods() { 138 | Result six = success("6"); 139 | Result notANumber = success("NaN"); 140 | 141 | Result parsedSix = six.then(onSuccessTry(Long::parseLong, Exception::toString)); 142 | Result parsedNaN = notANumber.then(onSuccessTry(Long::parseLong, Exception::toString)); 143 | 144 | assertThat(parsedSix, Is.is(success(6L))); 145 | assertThat(parsedNaN, Is.is(failure("java.lang.NumberFormatException: For input string: \"NaN\""))); 146 | } 147 | 148 | @Test 149 | public void canStreamSuccesses() { 150 | Stream> results = Stream.of(success(6), success(5), failure("darnit")); 151 | 152 | Stream resultStream = results.flatMap(successes()); 153 | List successes = resultStream.collect(toList()); 154 | 155 | assertThat(successes, hasItems(6, 5)); 156 | } 157 | 158 | @Test 159 | public void canMergeOperationsOnTwoResults() { 160 | Result evenSix = success(6); 161 | Result evenTwo = success(2); 162 | 163 | Result oddFive = Result.failure("Five is odd"); 164 | Result oddSeven = Result.failure("Seven is odd"); 165 | 166 | assertThat(evenSix.then(combineWith(evenTwo)).using((x, y) -> x * y), isSuccessOf(12)); 167 | assertThat(evenSix.then(combineWith(oddSeven)).using((x, y) -> x * y), isFailureOf("Seven is odd")); 168 | assertThat(oddFive.then(combineWith(evenTwo)).using((x, y) -> x * y), isFailureOf("Five is odd")); 169 | assertThat(oddFive.then(combineWith(oddSeven)).using((x, y) -> x * y), isFailureOf("Five is odd")); 170 | } 171 | 172 | @Test 173 | public void canStreamFailures() { 174 | Stream> results = Stream.of(success(6), success(5), failure("darnit")); 175 | 176 | List failures = results.flatMap(failures()).collect(toList()); 177 | 178 | assertThat(failures, hasItems("darnit")); 179 | } 180 | 181 | @Test 182 | public void canExtractValuesFromMap() { 183 | Map frenchNumberNames = mapOf(entry("un", 1), entry("deux", 2), entry("trois", 3)); 184 | 185 | Function> extractor = fromMap(frenchNumberNames, word -> String.format("%s is not a french number", word)); 186 | 187 | assertThat(extractor.apply("deux"), isSuccessOf(2)); 188 | assertThat(extractor.apply("quattro"), isFailureOf("quattro is not a french number")); 189 | } 190 | 191 | @Test 192 | public void canConvertListOfResultsIntoResultOfList() { 193 | List> results = asList(success(1), success(42), success(69)); 194 | Result, List> unwrapped = Lists.successesOrFailures(results); 195 | 196 | assertThat(unwrapped, isSuccessOf(asList(1, 42, 69))); 197 | } 198 | 199 | @Test 200 | public void canConvertListOfResultsIntoFailureOfListOfReasons() { 201 | List> results = asList(success(1), failure("cheese"), success(69), failure("hotdog")); 202 | Result, List> unwrapped = Lists.successesOrFailures(results); 203 | 204 | assertThat(unwrapped, isFailureOf(asList("cheese", "hotdog"))); 205 | } 206 | 207 | @Test 208 | @SuppressWarnings("unchecked") 209 | public void exampleParseAndHalveNumbers() { 210 | Stream inputs = Stream.of("6", "5", "NaN"); 211 | Consumer failureCallback = mock(Consumer.class); 212 | 213 | List halvedNumbers = inputs 214 | .map(tryTo(Long::parseLong, Exception::toString)) 215 | .map(attempt(x -> x % 2 == 0 ? success(x / 2) : failure(x + " is odd"))) 216 | .peek(onFailureDo(failureCallback)) 217 | .flatMap(successes()) 218 | .collect(toList()); 219 | 220 | assertThat(halvedNumbers, hasItems(3L)); 221 | 222 | verify(failureCallback).accept("java.lang.NumberFormatException: For input string: \"NaN\""); 223 | verify(failureCallback).accept("5 is odd"); 224 | verifyNoMoreInteractions(failureCallback); 225 | } 226 | 227 | @Test 228 | @SuppressWarnings("unchecked") 229 | public void exampleSplitResults() { 230 | Stream inputs = Stream.of("6", "5", "NaN"); 231 | 232 | Pair, List> halvedNumbers = inputs 233 | .map(tryTo(Long::parseLong, Exception::toString)) 234 | .map(attempt(x -> x % 2 == 0 ? success(x / 2) : failure(x + " is odd"))) 235 | .collect(split()); 236 | 237 | assertThat(halvedNumbers.left, hasItems(3L)); 238 | assertThat(halvedNumbers.right, hasItems("java.lang.NumberFormatException: For input string: \"NaN\"", "5 is odd")); 239 | } 240 | 241 | @Test 242 | public void shouldAggregateResults_BothSuccessful() { 243 | Result actualResult = Comprehensions 244 | .allOf( 245 | success("Yay!"), 246 | success(123) 247 | ) 248 | .then(ifAllSucceeded((x, y) -> x + " = " + y)); 249 | 250 | assertThat(actualResult, isSuccessOf("Yay! = 123")); 251 | } 252 | 253 | @Test 254 | public void shouldAggregateResults_OneFailure(){ 255 | Result result = Comprehensions.allOf( 256 | success("Yes!"), 257 | failure("No!") 258 | ).then(ifAllSucceeded((x, y) -> x + " = " + y)); 259 | 260 | assertThat(result, isFailureOf("No!")); 261 | } 262 | 263 | @Test 264 | public void shouldAggregateResults_AllThreeSuccessful() { 265 | Result song = Comprehensions.allOf( 266 | success("bibbidy"), 267 | success("bobbidy"), 268 | success("boo") 269 | ).then(ifAllSucceeded((x, y, z) -> x + " " + y + " " + z)); 270 | 271 | assertThat(song, isSuccessOf("bibbidy bobbidy boo")); 272 | } 273 | 274 | @Test 275 | public void shouldAggregateResults_AllThreeFailed() { 276 | Result uhoh = Comprehensions.allOf( 277 | failure("no"), 278 | failure("noo"), 279 | failure("nooo") 280 | ).then(ifAllSucceeded((x, y, z) -> x + " " + y + " " + z)); 281 | 282 | assertThat(uhoh, isFailureOf("no")); 283 | } 284 | 285 | @Test 286 | public void shouldAggregateResults_AllFourPassed() { 287 | Result result = Comprehensions.allOf( 288 | success("Yes"), 289 | success("YesYes"), 290 | success("YesYesYes"), 291 | success("YesYesYesYes") 292 | ).then(ifAllSucceeded((a, b, c, d) -> a + b + c + d)); 293 | 294 | assertThat(result, isSuccessOf("YesYesYesYesYesYesYesYesYesYes")); 295 | } 296 | 297 | @Test 298 | public void shouldAggregateResults_AllFourFailures() { 299 | Result result = Comprehensions.allOf( 300 | failure("NoNo", String.class), 301 | failure("NoNoNoNo"), 302 | failure("NoNoNoNo"), 303 | failure("NoNo") // There's no limit 304 | ).then(ifAllSucceeded((a, b, c, d) -> a + b + c + d)); 305 | 306 | assertThat(result, isFailureOf("NoNo")); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/test/java/co/unruly/control/result/TryTest.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.result; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.List; 6 | import java.util.function.Function; 7 | import java.util.stream.IntStream; 8 | import java.util.stream.Stream; 9 | 10 | import static co.unruly.control.result.Introducers.tryAndUnwrap; 11 | import static co.unruly.control.result.Introducers.tryTo; 12 | import static co.unruly.control.result.Resolvers.collapse; 13 | import static co.unruly.control.result.Resolvers.ifFailed; 14 | import static co.unruly.control.result.Transformers.onFailure; 15 | import static co.unruly.control.result.Transformers.onSuccess; 16 | import static co.unruly.control.result.Transformers.unwrapSuccesses; 17 | import static java.util.stream.Collectors.toList; 18 | import static org.hamcrest.CoreMatchers.is; 19 | import static org.hamcrest.MatcherAssert.assertThat; 20 | import static org.hamcrest.Matchers.contains; 21 | import static org.hamcrest.Matchers.containsInAnyOrder; 22 | 23 | //import static co.unruly.control.result.Results.ifFailed; 24 | 25 | public class TryTest { 26 | 27 | @Test 28 | public void canHandleRuntimeExceptions() { 29 | Function doSomething = tryTo(TryTest::throwsRuntimeException) 30 | .andThen(ifFailed(Exception::getMessage)); 31 | 32 | assertThat(doSomething.apply("throw"), is("This is a naughty method")); 33 | assertThat(doSomething.apply("play nice"), is("Today, I was good")); 34 | } 35 | 36 | @Test 37 | public void canHandleCheckedExceptions() { 38 | Function doSomething = tryTo(TryTest::throwsCheckedException) 39 | .andThen(ifFailed(Exception::getMessage)); 40 | 41 | assertThat(doSomething.apply("throw"), is("This is a naughty method")); 42 | assertThat(doSomething.apply("play nice"), is("Today, I was good")); 43 | } 44 | 45 | @Test 46 | public void canHandleStreamFunctionsUsingFlatTry() { 47 | List doingStuffWithNumbers = Stream.of("1", "two", "3") 48 | .flatMap(tryAndUnwrap(TryTest::throwsAndMakesStream)) 49 | .map(onSuccess(x -> String.format("Success: %s", x))) 50 | .map(onFailure(Exception::getMessage)) 51 | .map(collapse()) 52 | .collect(toList()); 53 | 54 | assertThat(doingStuffWithNumbers, contains( 55 | "Success: 1", 56 | "For input string: \"two\"", 57 | "Success: 1", 58 | "Success: 2", 59 | "Success: 3" 60 | )); 61 | } 62 | 63 | @Test 64 | public void canHandleStreamFunctionsUsingTryToAndUnwrap() { 65 | List doingStuffWithNumbers = Stream.of("1", "two", "3") 66 | .map(tryTo(TryTest::throwsAndMakesStream)) 67 | .flatMap(unwrapSuccesses()) 68 | .map(onSuccess(x -> String.format("Success: %s", x))) 69 | .map(onFailure(Exception::getMessage)) 70 | .map(collapse()) 71 | .collect(toList()); 72 | 73 | assertThat(doingStuffWithNumbers, contains( 74 | "Success: 1", 75 | "For input string: \"two\"", 76 | "Success: 1", 77 | "Success: 2", 78 | "Success: 3" 79 | )); 80 | } 81 | 82 | private static String throwsRuntimeException(String instruction) { 83 | if("throw".equals(instruction)) { 84 | throw new RuntimeException("This is a naughty method"); 85 | } 86 | return "Today, I was good"; 87 | } 88 | 89 | private static String throwsCheckedException(String instruction) throws CustomCheckedException { 90 | if("throw".equals(instruction)) { 91 | throw new CustomCheckedException("This is a naughty method"); 92 | } 93 | if("sneakyThrow".equals(instruction)) { 94 | throw new RuntimeException("I can probably get away with this"); 95 | } 96 | return "Today, I was good"; 97 | } 98 | 99 | private static Stream throwsAndMakesStream(String possiblyNumber) { 100 | // adding one to make the range have an inclusive end 101 | return IntStream.range(1, Integer.parseInt(possiblyNumber) + 1).boxed(); 102 | } 103 | 104 | static class CustomCheckedException extends Exception { 105 | 106 | public CustomCheckedException(String message) { 107 | super(message); 108 | } 109 | 110 | public String specialisedMethod() { 111 | return "This is something only this exception can do"; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/test/java/co/unruly/control/validation/ValidatorTest.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.validation; 2 | 3 | import co.unruly.control.ThrowingLambdas; 4 | import co.unruly.control.matchers.ResultMatchers; 5 | import co.unruly.control.pair.Pair; 6 | import co.unruly.control.result.Result; 7 | import org.hamcrest.Matcher; 8 | import org.junit.Test; 9 | 10 | import java.util.List; 11 | import java.util.function.BiConsumer; 12 | import java.util.function.Consumer; 13 | import java.util.function.Predicate; 14 | import java.util.stream.Collectors; 15 | import java.util.stream.Stream; 16 | 17 | import static co.unruly.control.matchers.ResultMatchers.isSuccessOf; 18 | import static co.unruly.control.result.Resolvers.*; 19 | import static co.unruly.control.result.Result.failure; 20 | import static co.unruly.control.result.Result.success; 21 | import static co.unruly.control.result.Transformers.onFailureDo; 22 | import static co.unruly.control.result.Transformers.onSuccessDo; 23 | import static co.unruly.control.validation.Validators.*; 24 | import static java.util.Arrays.asList; 25 | import static java.util.stream.Collectors.toList; 26 | import static org.hamcrest.CoreMatchers.hasItems; 27 | import static org.junit.Assert.assertThat; 28 | import static org.mockito.Mockito.*; 29 | 30 | public class ValidatorTest { 31 | 32 | @Test 33 | public void canCreateValidatorsWithFixedErrorMessages() { 34 | Validator isEven = acceptIf(divisibleBy(2), "odd"); 35 | 36 | Result> validate4 = isEven.apply(4); 37 | Result> validate5 = isEven.apply(5); 38 | 39 | assertThat(validate4, isSuccessOf(4)); 40 | 41 | assertThat(validate5, isFailedValidationOf(5, "odd")); 42 | } 43 | 44 | 45 | @Test 46 | public void canCreateValidatorsWithDynamicErrorMessages() { 47 | Validator isEven = acceptIf(divisibleBy(2), x -> String.format("%d is odd", x)); 48 | 49 | Result> validate4 = isEven.apply(4); 50 | Result> validate5 = isEven.apply(5); 51 | 52 | assertThat(validate4, isSuccessOf(4)); 53 | 54 | assertThat(validate5, isFailedValidationOf(5, "5 is odd")); 55 | } 56 | 57 | @Test 58 | public void canComposeValidators() { 59 | Validator fizzbuzz = compose( 60 | rejectIf(divisibleBy(3), "fizz"), 61 | rejectIf(divisibleBy(5), x -> String.format("%d is a buzz", x))); 62 | 63 | Result> validate4 = fizzbuzz.apply(4); 64 | Result> validate5 = fizzbuzz.apply(15); 65 | 66 | assertThat(validate4, isSuccessOf(4)); 67 | 68 | assertThat(validate5, isFailedValidationOf(15, "fizz", "15 is a buzz")); 69 | } 70 | 71 | 72 | @Test 73 | public void canComposeValidatorsForFirstError() { 74 | Validator fizzbuzz = firstOf(compose( 75 | rejectIf(divisibleBy(3), "fizz"), 76 | rejectIf(divisibleBy(5), x -> String.format("%d is a buzz", x)))); 77 | 78 | Result> validate5 = fizzbuzz.apply(5); 79 | Result> validate15 = fizzbuzz.apply(15); 80 | 81 | assertThat(validate5, isFailedValidationOf(5, "5 is a buzz")); 82 | assertThat(validate15, isFailedValidationOf(15, "fizz")); 83 | } 84 | 85 | @Test 86 | public void doesNotExecuteValidatorsIfAlreadyFailedAndOnlyReportingFirst() { 87 | Validator fizzbuzz = firstOf(compose( 88 | rejectIf(divisibleBy(3), "fizz"), 89 | rejectIf(divisibleBy(5), x -> { throw new AssertionError("should not exercise this method"); }))); 90 | 91 | Validator biglittle = firstOf(compose( 92 | rejectIf(x -> x > 10, "big"), 93 | rejectIf(x -> x < 3, x -> { throw new AssertionError("should not exercise this method"); }))); 94 | 95 | Validator combined = compose(fizzbuzz, biglittle); 96 | 97 | Result> validate15 = combined.apply(15); 98 | 99 | assertThat(validate15, isFailedValidationOf(15, "fizz", "big")); 100 | } 101 | 102 | @Test 103 | public void canStreamSuccesses() { 104 | Validator isEven = acceptIf(divisibleBy(2), "odd"); 105 | 106 | List evens = Stream.of(1,2,3,4,5,6,7,8,9) 107 | .map((item) -> isEven.apply(item)) 108 | .flatMap(successes()) 109 | .collect(toList()); 110 | 111 | assertThat(evens, hasItems(2,4,6,8)); 112 | } 113 | 114 | @Test 115 | public void canStreamFailures() { 116 | Validator isEven = acceptIf(divisibleBy(2), "odd"); 117 | 118 | List> odds = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9) 119 | .map((item) -> isEven.apply(item)) 120 | .flatMap(failures()) 121 | .collect(toList()); 122 | 123 | assertThat(odds, hasItems( 124 | validationFailure(1, "odd"), 125 | validationFailure(3, "odd"), 126 | validationFailure(5, "odd"), 127 | validationFailure(7, "odd"), 128 | validationFailure(9, "odd"))); 129 | } 130 | 131 | @Test 132 | public void canConsumeSuccesses() { 133 | Consumer log = mock(Consumer.class); 134 | 135 | Validator isPrime = compose( 136 | rejectIf(multipleOf(2), x -> x + " divides by 2"), 137 | rejectIf(multipleOf(3), x -> x + " divides by 3"), 138 | rejectIf(multipleOf(5), x -> x + " divides by 5"), 139 | rejectIf(multipleOf(7), x -> x + " divides by 7") 140 | ); 141 | 142 | Stream.of(1,2,3,4,5,6,7,8,9).map((item) -> isPrime.apply(item)).forEach(onSuccessDo(log)); 143 | 144 | verify(log).accept(1); 145 | verify(log).accept(2); 146 | verify(log).accept(3); 147 | verify(log).accept(5); 148 | verify(log).accept(7); 149 | verifyNoMoreInteractions(log); 150 | } 151 | 152 | @Test 153 | public void canConsumeFailures() { 154 | Consumer> log = mock(Consumer.class); 155 | 156 | Validator isPrime = compose( 157 | rejectIf(multipleOf(2), x -> x + " divides by 2"), 158 | rejectIf(multipleOf(3), x -> x + " divides by 3"), 159 | rejectIf(multipleOf(5), x -> x + " divides by 5"), 160 | rejectIf(multipleOf(7), x -> x + " divides by 7") 161 | ); 162 | 163 | Stream.of(1,2,3,4,5,6,7,8,9).map((item) -> isPrime.apply(item)).forEach(onFailureDo(log)); 164 | 165 | verify(log).accept(validationFailure(4, "4 divides by 2")); 166 | verify(log).accept(validationFailure(6, "6 divides by 2", "6 divides by 3")); 167 | verify(log).accept(validationFailure(8, "8 divides by 2")); 168 | verify(log).accept(validationFailure(9, "9 divides by 3")); 169 | verifyNoMoreInteractions(log); 170 | } 171 | 172 | @Test 173 | public void canFireFirstErrorForEachFailure() { 174 | Consumer> log = mock(Consumer.class); 175 | 176 | Validator isPrime = firstOf(compose( 177 | rejectIf(multipleOf(2), x -> x + " divides by 2"), 178 | rejectIf(multipleOf(3), x -> x + " divides by 3"), 179 | rejectIf(multipleOf(5), x -> x + " divides by 5"), 180 | rejectIf(multipleOf(7), x -> x + " divides by 7") 181 | )); 182 | 183 | Stream.of(1,2,3,4,5,6,7,8,9).map((item) -> isPrime.apply(item)).forEach(onFailureDo(log)); 184 | 185 | verify(log).accept(validationFailure(4, "4 divides by 2")); 186 | verify(log).accept(validationFailure(6, "6 divides by 2")); 187 | verify(log).accept(validationFailure(8, "8 divides by 2")); 188 | verify(log).accept(validationFailure(9, "9 divides by 3")); 189 | verifyNoMoreInteractions(log); 190 | } 191 | 192 | @Test 193 | public void canCreateConditionalValidator() { 194 | Validator, String> containsEvens = acceptIf( 195 | list -> list.stream().filter(x -> x % 2 == 0).collect(Collectors.toList()).isEmpty(), 196 | "List contains even numbers"); 197 | 198 | Validator, String> onlyChecksEvenLengthLists = onlyIf( 199 | list -> list.size() % 2 == 0, 200 | containsEvens 201 | ); 202 | 203 | Result, FailedValidation, String>> ofFiveNumbers = onlyChecksEvenLengthLists.apply(asList(1, 2, 3, 4, 5)); 204 | Result, FailedValidation, String>> ofSixNumbers = onlyChecksEvenLengthLists.apply(asList(1, 2, 3, 4, 5, 6)); 205 | 206 | assertThat(ofFiveNumbers, isSuccessOf(asList(1,2,3,4,5))); 207 | assertThat(ofSixNumbers, isFailedValidationOf(asList(1,2,3,4,5,6), "List contains even numbers")); 208 | } 209 | 210 | @Test 211 | public void canFireAllErrorsForEachFailure() { 212 | BiConsumer log = mock(BiConsumer.class); 213 | 214 | Validator isPrime = compose( 215 | rejectIf(multipleOf(2), x -> x + " divides by 2"), 216 | rejectIf(multipleOf(3), x -> x + " divides by 3"), 217 | rejectIf(multipleOf(5), x -> x + " divides by 5"), 218 | rejectIf(multipleOf(7), x -> x + " divides by 7") 219 | ); 220 | 221 | Stream.of(1,2,3,4,5,6,7,8,9).map((item) -> isPrime.apply(item)).forEach(onFailureDo(v -> v.errors.forEach(e -> log.accept(v.value, e)))); 222 | 223 | verify(log).accept(4, "4 divides by 2"); 224 | verify(log).accept(6, "6 divides by 2"); 225 | verify(log).accept(6, "6 divides by 3"); 226 | verify(log).accept(8, "8 divides by 2"); 227 | verify(log).accept(9, "9 divides by 3"); 228 | verifyNoMoreInteractions(log); 229 | } 230 | 231 | @Test 232 | public void canMapErrors() { 233 | BiConsumer log = mock(BiConsumer.class); 234 | 235 | Validator isPrime = mappingErrors(compose( 236 | rejectIf(multipleOf(2), x -> x + " divides by 2"), 237 | rejectIf(multipleOf(3), x -> x + " divides by 3"), 238 | rejectIf(multipleOf(5), x -> x + " divides by 5"), 239 | rejectIf(multipleOf(7), x -> x + " divides by 7") 240 | ), (num, msg) -> msg + ", oh boy"); 241 | 242 | Stream.of(1,2,3,4,5,6,7,8,9).map((item) -> isPrime.apply(item)).forEach(onFailureDo(v -> v.errors.forEach(e -> log.accept(v.value, e)))); 243 | 244 | verify(log).accept(4, "4 divides by 2, oh boy"); 245 | verify(log).accept(6, "6 divides by 2, oh boy"); 246 | verify(log).accept(6, "6 divides by 3, oh boy"); 247 | verify(log).accept(8, "8 divides by 2, oh boy"); 248 | verify(log).accept(9, "9 divides by 3, oh boy"); 249 | verifyNoMoreInteractions(log); 250 | } 251 | 252 | @Test 253 | public void canSplitResults() { 254 | Validator isPrime = compose( 255 | rejectIf(multipleOf(2), x -> x + " divides by 2"), 256 | rejectIf(multipleOf(3), x -> x + " divides by 3"), 257 | rejectIf(multipleOf(5), x -> x + " divides by 5"), 258 | rejectIf(multipleOf(7), x -> x + " divides by 7") 259 | ); 260 | 261 | Pair, List>> results = Stream 262 | .of(4,5,6,7,8) 263 | .map((item) -> isPrime.apply(item)) 264 | .collect(split()); 265 | 266 | assertThat(results.left, hasItems(5, 7)); 267 | assertThat(results.right, hasItems( 268 | validationFailure(4, "4 divides by 2"), 269 | validationFailure(6, "6 divides by 2", "6 divides by 3"), 270 | validationFailure(8, "8 divides by 2") 271 | )); 272 | } 273 | 274 | @Test 275 | public void canIgnoreErrors() { 276 | Result> failedValidation = failure(new FailedValidation(42, asList("fail1", "fail2", "error1"))); 277 | 278 | Result> filteredValidation = failedValidation 279 | .then(ignoreWhen(error -> error.startsWith("fail"))); 280 | 281 | assertThat(filteredValidation, isFailedValidationOf(42, "error1")); 282 | } 283 | 284 | @Test 285 | public void convertsToSuccessWhenAllErrorsIgnored() { 286 | Result> failedValidation = failure(new FailedValidation(42, asList("fail1", "fail2", "fail3"))); 287 | 288 | Result> filteredValidation = failedValidation 289 | .then(ignoreWhen(error -> error.startsWith("fail"))); 290 | 291 | assertThat(filteredValidation, isSuccessOf(42)); 292 | } 293 | 294 | @Test 295 | public void blammo() { 296 | safelyDoSomethingDodgy(x -> { throw new Exception("hello"); }, "cheese"); 297 | } 298 | 299 | private static void safelyDoSomethingDodgy(ThrowingLambdas.ThrowingConsumer consumer, String message) { 300 | try { 301 | consumer.accept(message); 302 | } catch (Exception ex) { 303 | // do nothing cos that's how I roll 304 | 305 | } 306 | } 307 | 308 | private static void doSomethingDodgy(String message) throws Exception { 309 | throw new Exception(message); 310 | } 311 | 312 | private static Predicate divisibleBy(int factor) { 313 | return x -> x % factor == 0; 314 | } 315 | 316 | private static Predicate multipleOf(int factor) { 317 | return x -> x != factor && x % factor == 0; 318 | } 319 | 320 | @SafeVarargs 321 | private final FailedValidation validationFailure(T value, E... errors) { 322 | return new FailedValidation(value, asList(errors)); 323 | } 324 | 325 | private Matcher>> isFailedValidationOf(T value, E... errors) { 326 | FailedValidation failedValidation = validationFailure(value, errors); 327 | return ResultMatchers.isFailureOf(failedValidation); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/test/java/examples/ConciseEquals.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | import java.util.Objects; 4 | 5 | import static co.unruly.control.casts.Equality.areEqual; 6 | 7 | /** 8 | * Just a demonstration of how we can build a cleaner way to check equality 9 | * using Result-based casts under the hood. 10 | */ 11 | public class ConciseEquals { 12 | 13 | private final int number; 14 | private final String text; 15 | 16 | public ConciseEquals(int number, String text) { 17 | this.number = number; 18 | this.text = text; 19 | } 20 | 21 | // The best of the IntelliJ inbuilt equality templates 22 | // @Override 23 | // public boolean equals(Object o) { 24 | // if (this == o) return true; 25 | // if (o == null || getClass() != o.getClass()) return false; 26 | // ConciseEquals that = (ConciseEquals) o; 27 | // return number == that.number && 28 | // Objects.equals(text, that.text); 29 | // } 30 | 31 | @Override 32 | public boolean equals(Object o) { 33 | return areEqual(this, o, (a, b) -> 34 | a.number == b.number && 35 | Objects.equals(a.text, b.text) 36 | ); 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | return Objects.hash(number, text); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/examples/ExceptionsInStreamsHandling.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.IOException; 6 | import java.util.List; 7 | import java.util.stream.Stream; 8 | 9 | import static co.unruly.control.result.Recover.ifType; 10 | import static co.unruly.control.result.Introducers.tryTo; 11 | import static co.unruly.control.result.Resolvers.ifFailed; 12 | import static co.unruly.control.result.Transformers.*; 13 | import static java.util.stream.Collectors.toList; 14 | 15 | public class ExceptionsInStreamsHandling { 16 | 17 | 18 | @Test 19 | public void handling_exceptions_with_result_example() { 20 | List customerAges = Stream.of("Bob", "Bill") 21 | .map(tryTo(this::findCustomerByName)) 22 | .peek(onSuccessDo(this::sendEmailUpdateTo)) 23 | .map(onSuccess(Customer::age)) 24 | .map(recover(ifType(NoCustomerWithThatName.class, error -> { 25 | log("Customer not found :(@"); 26 | return -1; 27 | }))) 28 | .map(recover(ifType(IOException.class, error -> -2))) 29 | .map(ifFailed(__ -> -127)) 30 | .collect(toList()); 31 | } 32 | 33 | @Test 34 | public void handling_multiple_exceptions_with_result_example() { 35 | 36 | List customerValues = Stream.of("Bob", "Bill") 37 | .map(tryTo(this::findCustomerByName)) 38 | .peek(onSuccessDo(this::sendEmailUpdateTo)) 39 | .map(onSuccessTry(Customer::calculateValue)) 40 | .map(recover(ifType(NoCustomerWithThatName.class, error -> { 41 | log("Customer not found :("); 42 | return -1; 43 | }))) 44 | .map(recover(ifType(IOException.class, error -> -2))) 45 | .map(ifFailed(__ -> -127)) 46 | .collect(toList()); 47 | } 48 | 49 | 50 | static class CustomerNotFound extends Exception {} 51 | static class NoCustomerWithThatName extends CustomerNotFound {} 52 | 53 | public Customer findCustomerByName(String name) throws CustomerNotFound { 54 | return customer; 55 | } 56 | 57 | private void sendEmailUpdateTo(Customer potentialCustomer) { 58 | email(customer.emailAddress(), customer.name(), "Blah blah blah"); 59 | } 60 | 61 | private void email(String email, String name, String message) { 62 | 63 | } 64 | 65 | private void log(String s) { 66 | 67 | } 68 | 69 | public interface Customer { 70 | default String emailAddress(){ return "";} 71 | default String name() { return ""; } 72 | default int age() { return 0; } 73 | 74 | default int calculateValue() throws CostUnknown { return 0; } 75 | class CostUnknown extends Exception {} 76 | } 77 | 78 | static Customer customer = new Customer(){}; 79 | 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/examples/FlatMapVariance.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | import co.unruly.control.result.Result; 4 | import co.unruly.control.result.TypeOf; 5 | import co.unruly.control.validation.Validator; 6 | import co.unruly.control.validation.Validators; 7 | 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.IntStream; 11 | 12 | import static co.unruly.control.Piper.pipe; 13 | import static co.unruly.control.result.Resolvers.collapse; 14 | import static co.unruly.control.result.Transformers.*; 15 | import static co.unruly.control.result.TypeOf.using; 16 | import static co.unruly.control.validation.Validators.rejectIf; 17 | 18 | public class FlatMapVariance { 19 | 20 | private static Validator fizzbuzz = Validators.compose( 21 | rejectIf(n -> n % 3 == 0, "fizz"), 22 | rejectIf(n -> n % 5 == 0, "buzz")); 23 | 24 | private static Validator under100 = Validators.compose( 25 | rejectIf(s -> s.length() > 2, s -> s + " is too damn high") 26 | ); 27 | 28 | public void canFlatmapErrorTypeOfStringIntoErrorTypeOfString() { 29 | divideExactlyByTwo(3) 30 | .then(attempt(this::isPrime)); 31 | } 32 | 33 | // // this should not compile 34 | // public void cannotFlatmapErrorTypeOfListOfStringIntoErrorTypeOfString() { 35 | // Result foo = divideExactlyByTwo(4) 36 | // .then(flatMap(this::listFactors)); 37 | // } 38 | 39 | public String isThisAnInterview(final int m) { 40 | final Validator fizzbuzz = Validators.compose( 41 | rejectIf(n -> n % 3 == 0, "fizz"), 42 | rejectIf(n -> n % 5 == 0, "buzz")); 43 | 44 | Validator under100 = rejectIf(s -> s.length() > 2, s -> s + " is too damn high"); 45 | 46 | return pipe(m) 47 | .then(fizzbuzz) 48 | .then(onSuccess(x -> Integer.toString(x))) 49 | .then(using(TypeOf.>forFailures())) 50 | .then(attempt(under100)) 51 | .then(onSuccess(s -> "Great success! " + s)) 52 | .then(onFailure(f -> "Big fails :( " + String.join(", ", f))) 53 | .then(collapse()) 54 | .resolve(); 55 | } 56 | 57 | public void canFlatmapErrorTypeOfFailedValidationIntoErrorTypeOfListOfString() { 58 | Result> foo = pipe(4) 59 | .then(fizzbuzz) 60 | .then(using(TypeOf.>forFailures())) 61 | .then(attempt(this::listFactors)) 62 | .resolve(); 63 | } 64 | 65 | public void canFlatmapErrorTypeOfListOfStringIntoErrorTypeOfFailedValidation() { 66 | Result> foo = listFactors(5) 67 | .then(attempt((item) -> fizzbuzz.apply(item))); 68 | } 69 | 70 | private Result divideExactlyByTwo(int number) { 71 | return number % 2 == 0 72 | ? Result.success(number / 2) 73 | : Result.failure(number + " is odd: cannot divide exactly by two"); 74 | } 75 | 76 | private Result isPrime(int number) { 77 | return IntStream.range(2, (int) Math.sqrt(number)) 78 | .anyMatch(possibleDivisor -> number % possibleDivisor == 0) 79 | ? Result.success(number) 80 | : Result.failure(number + " is not prime"); 81 | } 82 | 83 | private boolean checkPrime(int number) { 84 | return IntStream 85 | .range(2, (int) Math.sqrt(number)) 86 | .anyMatch(possibleDivisor -> number % possibleDivisor == 0); 87 | } 88 | 89 | private Result> listFactors(int number) { 90 | List primeFactors = IntStream 91 | .range(2, (int) Math.sqrt(number)) 92 | .filter(possibleDivisor -> number % possibleDivisor == 0) 93 | .mapToObj(divisor -> number + " is divisible by " + divisor) 94 | .collect(Collectors.toList()); 95 | 96 | return primeFactors.isEmpty() 97 | ? Result.success(number) 98 | : Result.failure(primeFactors); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/examples/FunctionalErrorHandling.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | import co.unruly.control.result.Result; 4 | import org.junit.Test; 5 | 6 | import static co.unruly.control.result.Combiners.combineWith; 7 | import static co.unruly.control.result.Resolvers.ifFailed; 8 | import static co.unruly.control.result.Result.failure; 9 | import static co.unruly.control.result.Result.success; 10 | import static co.unruly.control.result.Transformers.attempt; 11 | import static co.unruly.control.result.Transformers.onSuccess; 12 | 13 | public class FunctionalErrorHandling { 14 | 15 | @Test 16 | public void howToMakeBreakfast() { 17 | final Fridge fridge = new Fridge(); 18 | final Bread bread = new Bread(); 19 | 20 | // I don't know whether the eggs are good or not... 21 | Result eggs 22 | = fridge.areEggsOff() 23 | ? success(fridge.getEggs()) 24 | : failure(new Garbage(fridge.getEggs())); 25 | 26 | // I'm also terrible at cooking, and can ruin eggs by burning 27 | // or undercooking them, but salting them isn't a problem 28 | Result scrambledEggs 29 | = eggs.then(attempt(Eggs::scramble)) 30 | .then(onSuccess(Condiments::salt)); 31 | 32 | 33 | // I can reliably turn bread into toast, too 34 | Result toast 35 | = success(bread, Garbage.class).then(onSuccess(Bread::toast)); 36 | 37 | // I am however good enough to put the eggs on toast 38 | Result eggsOnToast = scrambledEggs.then(combineWith(toast)).using(ScrambledEggsOnToast::new); 39 | 40 | Breakfast breakfast = eggsOnToast.then(ifFailed(__ -> new BowlOfCornflakes())); 41 | } 42 | 43 | private static class Fridge { 44 | 45 | public boolean areEggsOff() { 46 | return false; 47 | } 48 | 49 | public Eggs getEggs() { 50 | return new Eggs(); 51 | } 52 | } 53 | 54 | private static class Bread { 55 | 56 | public Toast toast() { 57 | return new Toast(); 58 | } 59 | } 60 | 61 | private static class Garbage { 62 | 63 | public Garbage(Eggs eggs) { 64 | 65 | } 66 | } 67 | 68 | private static class Toast { 69 | 70 | } 71 | 72 | private static class ScrambledEggs { 73 | 74 | } 75 | 76 | private static class Eggs { 77 | 78 | public Result scramble() { 79 | return success(new ScrambledEggs()); 80 | } 81 | } 82 | 83 | private static class Condiments { 84 | public static ScrambledEggs salt(ScrambledEggs unsalted) { 85 | return unsalted; 86 | } 87 | } 88 | 89 | private static class ScrambledEggsOnToast implements Breakfast { 90 | private final ScrambledEggs eggs; 91 | private final Toast toast; 92 | 93 | private ScrambledEggsOnToast(ScrambledEggs eggs, Toast toast) { 94 | this.eggs = eggs; 95 | this.toast = toast; 96 | } 97 | } 98 | 99 | private static class BowlOfCornflakes implements Breakfast { 100 | 101 | } 102 | 103 | private interface Breakfast { 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/examples/NovelErrorHandling.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | import co.unruly.control.result.Result; 4 | 5 | import static co.unruly.control.result.Resolvers.collapse; 6 | import static co.unruly.control.result.Result.failure; 7 | import static co.unruly.control.result.Result.success; 8 | import static co.unruly.control.result.Transformers.attempt; 9 | import static co.unruly.control.result.Transformers.onSuccess; 10 | import static java.lang.String.format; 11 | 12 | public class NovelErrorHandling { 13 | 14 | public static String novelSales(Author author, Publisher publisher, Editor editor, Retailer retailer) { 15 | return author.getIdea() 16 | .then(attempt(publisher::getAdvance)) 17 | .then(attempt(author::writeNovel)) 18 | .then(onSuccess(editor::editNovel)) 19 | .then(onSuccess(publisher::publishNovel)) 20 | .then(onSuccess(retailer::sellNovel)) 21 | .then(onSuccess(sales -> format("%s sold %d copies", sales.novel, sales.copiesSold))) 22 | .then(collapse()); 23 | } 24 | 25 | public static class Author { 26 | private Result idea; 27 | private final int skill; 28 | private final int lifestyleCosts; 29 | 30 | public Author(Result idea, int skill, int lifestyleCosts) { 31 | this.idea = idea; 32 | this.skill = skill; 33 | this.lifestyleCosts = lifestyleCosts; 34 | } 35 | 36 | public Result getIdea() { 37 | return idea; 38 | } 39 | 40 | public Result writeNovel(Advance advance) { 41 | if(advance.amount > lifestyleCosts) { 42 | int happiness = advance.amount - lifestyleCosts; 43 | return success(new Manuscript(advance.idea.title, happiness * skill)); 44 | } else { 45 | return failure("Ran out of money, went back to work at Tescos"); 46 | } 47 | } 48 | } 49 | 50 | public static class Publisher { 51 | 52 | public final int qualityThreshold; 53 | public final int generosity; 54 | 55 | public Publisher(int qualityThreshold, int generosity) { 56 | this.qualityThreshold = qualityThreshold; 57 | this.generosity = generosity; 58 | } 59 | 60 | public Result getAdvance(Idea idea) { 61 | if(idea.appeal >= qualityThreshold) { 62 | return success(new Advance(idea.appeal * generosity, idea)); 63 | } else { 64 | return failure("This novel wouldn't sell"); 65 | } 66 | } 67 | 68 | public Novel publishNovel(Manuscript manuscript) { 69 | return new Novel(manuscript.title, manuscript.quality); 70 | } 71 | } 72 | 73 | public static class Editor { 74 | public Manuscript editNovel(Manuscript manuscript) { 75 | return new Manuscript(manuscript.title, manuscript.quality + 3); 76 | } 77 | } 78 | 79 | public static class Retailer { 80 | private final int customerCount; 81 | 82 | public Retailer(int customerCount) { 83 | this.customerCount = customerCount; 84 | } 85 | 86 | public Sales sellNovel(Novel novel) { 87 | return new Sales(novel, novel.quality * customerCount); 88 | } 89 | } 90 | 91 | public static class Idea { 92 | public final String title; 93 | public final int appeal; 94 | 95 | public Idea(String title, int appeal) { 96 | this.title = title; 97 | this.appeal = appeal; 98 | } 99 | } 100 | 101 | public static class Advance { 102 | public final int amount; 103 | public final Idea idea; 104 | 105 | public Advance(int amount, Idea idea) { 106 | this.amount = amount; 107 | this.idea = idea; 108 | } 109 | } 110 | 111 | public static class Manuscript { 112 | public final String title; 113 | public final int quality; 114 | 115 | public Manuscript(String title, int quality) { 116 | this.title = title; 117 | this.quality = quality; 118 | } 119 | } 120 | 121 | public static class Novel { 122 | public final String title; 123 | public final int quality; 124 | 125 | public Novel(String title, int quality) { 126 | this.title = title; 127 | this.quality = quality; 128 | } 129 | } 130 | 131 | public static class Sales { 132 | private final Novel novel; 133 | private final int copiesSold; 134 | 135 | public Sales(Novel novel, int copiesSold) { 136 | 137 | this.novel = novel; 138 | this.copiesSold = copiesSold; 139 | } 140 | } 141 | } 142 | --------------------------------------------------------------------------------

9 | * This exists to offer a bridge between void and regular functions, providing 10 | * convenience methods to convert between them. 11 | */ 12 | public enum Unit { 13 | 14 | UNIT; 15 | 16 | /** 17 | * Converts a Consumer to a Function, which returns Unit.UNIT 18 | */ 19 | public static Function functify(Consumer toVoid) { 20 | return x -> { 21 | toVoid.accept(x); 22 | return Unit.UNIT; 23 | }; 24 | } 25 | 26 | /** 27 | * Converts a Function to a Consumer, throwing away the return value 28 | */ 29 | public static Consumer voidify(Function function) { 30 | return function::apply; 31 | } 32 | 33 | /** 34 | * A no-op function which takes any argument, does nothing, and returns Unit.UNIT 35 | */ 36 | public static Unit noOp(T __) { 37 | return UNIT; 38 | } 39 | 40 | /** 41 | * A no-op consumer which takes any argument and does nothing 42 | */ 43 | public static void noOpConsumer(T __) { 44 | // do nothing 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/casts/Equality.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.casts; 2 | 3 | import java.util.function.BiPredicate; 4 | 5 | import static co.unruly.control.Piper.pipe; 6 | import static co.unruly.control.result.Introducers.exactCastTo; 7 | import static co.unruly.control.result.Resolvers.ifFailed; 8 | import static co.unruly.control.result.Transformers.onSuccess; 9 | 10 | public interface Equality { 11 | 12 | static boolean areEqual(T self, Object other, BiPredicate equalityChecker) { 13 | if(self==other) { 14 | return true; 15 | } 16 | 17 | if(other==null) { 18 | return false; 19 | } 20 | 21 | return pipe(other) 22 | .then(exactCastTo((Class)self.getClass())) 23 | .then(onSuccess(o -> equalityChecker.test(self, o))) 24 | .then(ifFailed(__ -> false)) 25 | .resolve(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/matchers/FailureMatcher.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.matchers; 2 | 3 | import co.unruly.control.result.Result; 4 | import org.hamcrest.Description; 5 | import org.hamcrest.Matcher; 6 | import org.hamcrest.TypeSafeDiagnosingMatcher; 7 | 8 | public class FailureMatcher extends TypeSafeDiagnosingMatcher> { 9 | 10 | private final Matcher innerMatcher; 11 | 12 | public FailureMatcher(Matcher innerMatcher) { 13 | this.innerMatcher = innerMatcher; 14 | } 15 | 16 | @Override 17 | protected boolean matchesSafely(Result result, Description description) { 18 | Boolean matches = result.either( 19 | success -> false, 20 | innerMatcher::matches 21 | ); 22 | 23 | if(!matches) { 24 | ResultMatchers.describeTo(result, description); 25 | } 26 | 27 | return matches; 28 | } 29 | 30 | @Override 31 | public void describeTo(Description description) { 32 | description.appendText("A Failure containing "); 33 | innerMatcher.describeTo(description); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/matchers/ResultMatchers.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.matchers; 2 | 3 | import co.unruly.control.result.Result; 4 | import org.hamcrest.Description; 5 | import org.hamcrest.Matcher; 6 | 7 | import static org.hamcrest.CoreMatchers.equalTo; 8 | 9 | /** 10 | * Hamcrest matchers for Result types 11 | */ 12 | public class ResultMatchers { 13 | 14 | /** 15 | * Matches if the received value is a Success containing the specified value 16 | */ 17 | public static Matcher> isSuccessOf(S expectedValue) { 18 | return isSuccessThat(equalTo(expectedValue)); 19 | } 20 | 21 | /** 22 | * Matches if the received value is a Success matching the specified value 23 | */ 24 | public static Matcher> isSuccessThat(Matcher expectedSuccess) { 25 | return new SuccessMatcher<>(expectedSuccess); 26 | } 27 | 28 | /** 29 | * Matches if the received value is a Failure containing the specified value 30 | */ 31 | public static Matcher> isFailureOf(F expectedValue) { 32 | return isFailureThat(equalTo(expectedValue)); 33 | } 34 | 35 | /** 36 | * Matches if the received value is a Failure matching the specified value 37 | */ 38 | public static Matcher> isFailureThat(Matcher expectedFailure) { 39 | return new FailureMatcher<>(expectedFailure); 40 | } 41 | 42 | static void describeTo(Result result, Description description) { 43 | result.either( 44 | success -> description.appendText("A Success containing " + success), 45 | failure -> description.appendText("A Failure containing " + failure) 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/matchers/SuccessMatcher.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.matchers; 2 | 3 | import co.unruly.control.result.Result; 4 | import org.hamcrest.Description; 5 | import org.hamcrest.Matcher; 6 | import org.hamcrest.TypeSafeDiagnosingMatcher; 7 | 8 | public class SuccessMatcher extends TypeSafeDiagnosingMatcher> { 9 | 10 | private final Matcher innerMatcher; 11 | 12 | public SuccessMatcher(Matcher innerMatcher) { 13 | this.innerMatcher = innerMatcher; 14 | } 15 | 16 | @Override 17 | protected boolean matchesSafely(Result result, Description description) { 18 | Boolean matches = result.either( 19 | innerMatcher::matches, 20 | failure -> false 21 | ); 22 | 23 | if(!matches) { 24 | ResultMatchers.describeTo(result, description); 25 | } 26 | 27 | return matches; 28 | } 29 | 30 | @Override 31 | public void describeTo(Description description) { 32 | description.appendText("A Success containing "); 33 | innerMatcher.describeTo(description); 34 | } 35 | 36 | private void describe(Result result, Description description) { 37 | result.either( 38 | success -> description.appendText("A Success containing " + success), 39 | failure -> description.appendText("A Failure containing " + failure) 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/pair/Comprehensions.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.pair; 2 | 3 | import co.unruly.control.pair.Quad.QuadFunction; 4 | import co.unruly.control.pair.Triple.TriFunction; 5 | import co.unruly.control.result.Result; 6 | 7 | import java.util.Optional; 8 | import java.util.function.BiFunction; 9 | import java.util.function.Function; 10 | 11 | import static co.unruly.control.result.Transformers.attempt; 12 | import static co.unruly.control.result.Transformers.onSuccess; 13 | 14 | public interface Comprehensions { 15 | 16 | static Function, T> onAll(BiFunction f) { 17 | return pair -> pair.then(f); 18 | } 19 | 20 | static Function, T> onAll(TriFunction f) { 21 | return triple -> triple.then(f); 22 | } 23 | 24 | static Function, T> onAll(QuadFunction f) { 25 | return quad -> quad.then(f); 26 | } 27 | 28 | static Optional> allOf(Optional maybeLeft, Optional maybeRight) { 29 | return maybeLeft.flatMap(left -> 30 | maybeRight.map(right -> 31 | Pair.of(left, right))); 32 | } 33 | 34 | static Optional> allOf(Optional maybeFirst, Optional maybeSecond, Optional maybeThird) { 35 | return maybeFirst.flatMap(first -> maybeSecond.flatMap(second -> maybeThird.map(third -> Triple.of(first, second, third)))); 36 | } 37 | 38 | static Optional> allOf(Optional maybeFirst, Optional maybeSecond, Optional maybeThird, Optional maybeFourth) { 39 | return maybeFirst.flatMap(first -> maybeSecond.flatMap(second -> maybeThird.flatMap(third -> maybeFourth.map(fourth -> Quad.of(first, second, third, fourth))))); 40 | } 41 | 42 | static Result, F> allOf(Result left, Result right) { 43 | return left.then(attempt(l -> 44 | right.then(onSuccess(r -> 45 | Pair.of(l, r))))); 46 | } 47 | 48 | static Result, F> allOf(Result first, Result second, Result third) { 49 | return first.then(attempt(firstValue -> 50 | second.then(attempt(secondValue -> 51 | third.then(onSuccess(thirdValue -> 52 | Triple.of(firstValue, secondValue, thirdValue) 53 | )) 54 | )) 55 | )); 56 | } 57 | 58 | static Result, F> allOf(Result first, Result second, Result third, Result fourth) { 59 | return first.then(attempt(firstValue -> 60 | second.then(attempt(secondValue -> 61 | third.then(attempt(thirdValue -> 62 | fourth.then(onSuccess(fourthValue -> 63 | Quad.of(firstValue, secondValue, thirdValue, fourthValue) 64 | )) 65 | )) 66 | )) 67 | )); 68 | } 69 | 70 | 71 | static Function, F>, Result> ifAllSucceeded( 72 | BiFunction f 73 | ) { 74 | return onSuccess(onAll(f)); 75 | } 76 | 77 | static Function, F>, Result> ifAllSucceeded( 78 | TriFunction f 79 | ) { 80 | return onSuccess(onAll(f)); 81 | } 82 | 83 | static Function, F>, Result> ifAllSucceeded( 84 | QuadFunction f 85 | ) { 86 | return onSuccess(onAll(f)); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/co/unruly/control/pair/Maps.java: -------------------------------------------------------------------------------- 1 | package co.unruly.control.pair; 2 | 3 | import java.util.Map; 4 | import java.util.stream.Collector; 5 | import java.util.stream.Collectors; 6 | import java.util.stream.Stream; 7 | 8 | /** 9 | * Convenience methods for defining maps inline. 10 | */ 11 | public interface Maps { 12 | 13 | /** 14 | * Build a map from the provided key-value pairs. For example: 15 | *