├── .gitattributes ├── .github └── workflows │ ├── gradle.yml │ └── javadoc.yml ├── .gitignore ├── README.md ├── build.gradle ├── func-exc-handling-2.md ├── func-exc-handling.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── idioms-and-patterns.md ├── pics └── exponential-backoff.png ├── retry.md ├── settings.gradle └── src ├── main └── java │ └── com │ └── github │ └── skopylov58 │ └── functional │ ├── Decorator.java │ ├── Either.java │ ├── FPUtils.java │ ├── Functor.java │ ├── IO.java │ ├── IO2.java │ ├── Memo.java │ ├── Monad.java │ ├── MonadAccessor.java │ ├── Option.java │ ├── Reader.java │ ├── Result.java │ ├── State.java │ ├── Try.java │ ├── Tuple.java │ ├── Tuple3.java │ ├── Validation.java │ ├── Validator.java │ ├── ZIO.java │ ├── monad │ └── pictures │ │ ├── Maybe.java │ │ └── MonadInPictures.java │ └── package-info.java └── test └── java └── com └── github └── skopylov58 └── functional ├── ChainOfResponsibilityTest.java ├── EitherTest.java ├── FPDesign.java ├── FPUtilsTest.java ├── FilterTest.java ├── IO2Test.java ├── IOTest.java ├── MemoTest.java ├── OptionTest.java ├── OptionalTest.java ├── ReaderTest.java ├── ResourceTest.java ├── RetryTest.java ├── RunnableTest.java ├── StateTest.java ├── SupplierTest.java ├── TryTest.java ├── TupleTest.java ├── ZIOTest.java └── samples ├── ArithmeticExceptionTest.java ├── BookRecord.java ├── BookRecordTest.java ├── BuilderTest.java ├── LogExceptionTest.java ├── NumbersTest.java ├── Person.java ├── PropertiesTest.java ├── ReadURLTest.java ├── RecoverTest.java ├── SimpleValidator.java ├── SocketTest.java ├── URLStreamTest.java └── ValidationTest.java /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 6 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 7 | 8 | name: Java CI with Gradle 9 | 10 | on: 11 | push: 12 | branches: [ master ] 13 | pull_request: 14 | branches: [ master ] 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up JDK 17 24 | uses: actions/setup-java@v3 25 | with: 26 | java-version: '17' 27 | distribution: 'temurin' 28 | - name: Build with Gradle 29 | uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee 30 | with: 31 | arguments: build 32 | -------------------------------------------------------------------------------- /.github/workflows/javadoc.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Javadoc 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Deploy JavaDoc 🚀 17 | uses: MathieuSoysal/Javadoc-publisher.yml@v2.4.0 18 | with: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | javadoc-branch: javadoc 21 | java-version: 17 22 | target-folder: docs 23 | project: gradle 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | # Ignore Gradle build output directory 4 | build/ 5 | bin/ 6 | function/bin/ 7 | .classpath 8 | .project 9 | .settings 10 | .vscode/settings.json 11 | .idea 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # java-functional-addons 2 | 3 | ![example workflow](https://github.com/skopylov58/java-functional-addons/actions/workflows/gradle.yml/badge.svg) 4 | [![Javadoc](https://img.shields.io/badge/JavaDoc-Online-green)](https://skopylov58.github.io/java-functional-addons/) 5 | [![Habr.com publication](https://badgen.net/badge/habr.com/publication/green)](https://habr.com/ru/post/676852/) 6 | [![Habr.com publication](https://badgen.net/badge/habr.com/publication/green)](https://habr.com/ru/post/687954/) 7 | 8 | ## `Try` - functional exception handling 9 | 10 | ### Rational behind `Try` 11 | 12 | I really like Java's functional features (streams, optionals, etc.) very much but it becomes painful 13 | when using them in the real context. That is because Java's functional interfaces are Exception unaware 14 | and you must handle checked exceptions inside lambdas. 15 | Lets look at simple procedure of converting list of strings to list of URLs. 16 | 17 | ```java 18 | private List urlListTraditional(String[] urls) { 19 | return Stream.of(urls) 20 | .map(s -> { 21 | try { 22 | return new URL(s); 23 | } catch (MalformedURLException me) { // Ops..., not too pretty 24 | return null; 25 | } 26 | }).filter(Objects::nonNull) 27 | .collect(Collectors.toList()); 28 | } 29 | ``` 30 | 31 | `Try` presents computation that may produce success result of type T or failure with exception. 32 | It is quite similar to Java `Optional` which may have result value of type T or nothing (null value). 33 | With `Try`, sample above will look like this: 34 | 35 | ```java 36 | private List urlListWithTry(String[] urls) { 37 | return Stream.of(urls) 38 | .map(Try.of(URL::new)) 39 | .flatMap(Try::stream) //Failure gives empty stream 40 | .collect(Collectors.toList()); 41 | } 42 | ``` 43 | ### `Try` with `Stream` and `Optional` 44 | 45 | `Try` can be easily converted to the `Optional` by using `Try#optional()` such way that failed Try will be converted to the `Optional.empty`. 46 | 47 | I have intentionally made `Try` API very similar to the Java `Optional` API. 48 | `Try#filter(Predicate)` and`Try#map(Function)` methods have the same semantics as corresponding Optional methods. 49 | So if you are familiar to `Optional` then you will get used to `Try` with easy. 50 | 51 | `Try` can be easily converted to the `Stream` by using `Try#stream()` the same way as it is done for `Optional#stream()`. 52 | Successful `Try` will be converted to one element stream of type T, failed Try will be converted to the empty stream. 53 | 54 | You can filter failed tries in the stream two possible ways - first is traditional by using `Try#filter()` 55 | 56 | ``` 57 | ... 58 | .filter(Try::isSuccess) 59 | .map(Try::get) 60 | ... 61 | 62 | ``` 63 | 64 | Second approach is a bit shorter by using `Try#stream()` 65 | 66 | ``` 67 | ... 68 | .flatMap(Try::stream) 69 | ... 70 | ``` 71 | 72 | This code will filter failed tries and return stream of successful values of type T. 73 | 74 | ### Recovering failed `Try` 75 | 76 | Do you have plans to recover from failures? If yes then `Try` will help you to recover with easy. 77 | You can chain as many recover strategies as you want. 78 | 79 | ```java 80 | Try.of(...) 81 | .map(...) 82 | .map(...) 83 | .filter(...) 84 | .recover(recoverPlanA) 85 | .recover(recoverPlanB) 86 | .recover(recoverPlanC) 87 | .onSuccess(...) 88 | .orElse(...) 89 | ``` 90 | 91 | If planA succeeds then PlanB and PlanC will not have effect (will not be invoked). Explanation is 92 | simple - recover procedure has no effect for successful Try, so the only the first successful 93 | plan will be in action. 94 | 95 | ### `Try` with resources 96 | 97 | `Try` implements AutoCloseable interface, and `Try` can be used inside `try-with-resource` block. 98 | Let's imagine we need open socket, write a few bytes to the socket's output stream and then close socket. 99 | This code with `Try` will look as following: 100 | 101 | ``` 102 | try (var s = Try.of(() -> new Socket("host", 8888))) { 103 | s.map(Socket::getOutputStream) 104 | .onSuccess(out -> out.write(new byte[] {1,2,3})) 105 | .onFailure(e -> System.out.println(e)); 106 | } 107 | ``` 108 | 109 | Socket will be closed after last curly bracket. 110 | 111 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "org.sonarqube" version "3.5.0.2730" 3 | } 4 | 5 | 6 | apply plugin: 'java-library' 7 | apply plugin: 'eclipse' 8 | apply plugin: 'jacoco' 9 | 10 | repositories { 11 | mavenCentral() 12 | mavenLocal() 13 | } 14 | 15 | 16 | java { 17 | sourceCompatibility = "17" 18 | targetCompatibility = "17" 19 | } 20 | 21 | 22 | dependencies { 23 | testImplementation 'junit:junit:4.12' 24 | 25 | // https://mvnrepository.com/artifact/org.projectlombok/lombok 26 | //compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.36' 27 | 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /func-exc-handling-2.md: -------------------------------------------------------------------------------- 1 | # Обработка исключений в Java в функциональном стиле. Часть 2. 2 | 3 | В [предыдущей статье](https://habr.com/ru/post/676852/) была рассмотрена функциональная обработка исключений с помощью интерфейса `Try`. Статья вызвала определенный интерес читателей и была отмечена в ["Сезоне Java"](https://habr.com/ru/article/673202/). 4 | 5 | В данной статье автор продолжит тему и рассмотрит простую и "грамотную" (literate) обработку исключений при помощи функций высшего порядка без использования каких либо внешних зависимостей и сторонних библиотек. 6 | 7 | ## Решение 8 | 9 | Для начала перепишем пример из предыдущей статьи преобразования URL из строкового представления к объектам URL c использованием Optional. 10 | 11 | ```java 12 | public List urlList(String[] urls) { 13 | return Stream.of(urls) //Stream 14 | .map(s -> { 15 | try { 16 | return Optional.of(new URL(s)); 17 | } catch (MalformedURLException me) { 18 | return Optional.empty(); 19 | } 20 | }) //StreamURL> 21 | .flatMap(Optional::stream) //Stream, filters empty optionals 22 | .toList(); 23 | } 24 | ``` 25 | 26 | Общая схема понятна, однако не хотелось бы писать подобный boilerplate код каждый раз когда мы встречается с функцией выбрасывающей проверяемые исключения. 27 | 28 | И тут нам помогут следующие функции: 29 | 30 | ````java 31 | @FunctionalInterface 32 | interface CheckedFunction { 33 | R apply(T t) throws Exception; 34 | } 35 | 36 | /** 37 | * Higher-order function to convert partial function T=>R to total function T=>Optional 38 | * @param function input parameter type 39 | * @param function result type 40 | * @param func partial function T=>R that may throw checked exception 41 | * @return total function T => Optional 42 | */ 43 | static Function> toOptional(CheckedFunction func) { 44 | return param -> { 45 | try { 46 | return Optional.ofNullable(func.apply(param)); 47 | } catch (RuntimeException err) { 48 | throw err; //All runtime exceptions are treated as errors/bugs 49 | } catch (Exception e) { 50 | return Optional.empty(); 51 | } 52 | }; 53 | } 54 | ```` 55 | 56 | Дадим некоторые пояснения. `CheckedFunction` это функция которая может выбросить исключение при преобразовании T => R. 57 | Подобные функции в терминах функционального программирования называются частичными (partial) функциями, потому что значение функции не определено при некоторых входных значениях параметра. 58 | 59 | Функция `toOptional(...)` преобразует частичную (partial) функцию T => R в полную (total) функцию T => Optional. Подобного рода функции, которые принимают параметром и/или возвращают другую функцию, в терминах функционального программирования называются функциями высшего порядка (higher-order function). 60 | 61 | С использованием новой функции код примет следующий опрятный вид: 62 | 63 | ```java 64 | public List urlList(String[] urls) { 65 | return Stream.of(urls) //Stream 66 | .map(toOptional(URL::new)) //Stream> 67 | .flatMap(Optional::stream) //Stream, filters empty optionals 68 | .toList(); //List 69 | } 70 | ``` 71 | 72 | И теперь её можно применять везде где возможны проверяемые (checked) исключения. 73 | 74 | ````java 75 | List intList(String [] numbers) { 76 | NumberFormat format = NumberFormat.getInstance(); 77 | return Stream.of(numbers) //Stream 78 | .map(toOptional(format::parse)) //Checked ParseException may happen here 79 | .flatMap(Optional::stream) //Stream 80 | .toList(); //List 81 | } 82 | ```` 83 | 84 | ## Улучшаем обработку исключений 85 | 86 | При использовании `Optional` пропадает информация о самом исключении. В крайнем случае исключение можно залогировать в теле функции `toOptional`, но мы найдем лучшее решение. 87 | 88 | Нам нужен любой контейнер, который может содержать значение типа T либо само исключение. В терминах функционального программирования таким контейнером является `Either`, но к сожаления класса `Either` (как и класса `Try`) нет в составе стандартной библиотеки Java. 89 | 90 | Вы можете использовать любой подходящий контейнер, которым Вы обладаете. Я же в целях краткости буду использовать следующий 91 | 92 | ````java 93 | //Require Java 14+ 94 | record Result(T result, Exception exception) { 95 | public boolean failed() {return exception != null;} 96 | public Stream stream() {return failed() ? Stream.empty() : Stream.of(result);} 97 | } 98 | ```` 99 | 100 | Теперь наша функция высшего порядка получит имя `toResult` и будет выглядеть так: 101 | 102 | ````java 103 | static Function> toResult(CheckedFunction func) { 104 | return param -> { 105 | try { 106 | return new Result<>(func.apply(param), null); 107 | } catch (RuntimeException err) { 108 | throw err; 109 | } catch (Exception e) { 110 | return new Result<>(null, e); 111 | } 112 | }; 113 | } 114 | ```` 115 | 116 | А вот и применение новой функции toResult() 117 | 118 | ````java 119 | List intListWithResult(String [] numbers) { 120 | NumberFormat format = NumberFormat.getInstance(); 121 | return Stream.of(numbers) //Stream 122 | .map(toResult(format::parse)) //Stream>, ParseException may happen 123 | .peek(this::handleErr) //Stream> 124 | .flatMap(Result::stream) //Stream 125 | .toList(); //List 126 | } 127 | 128 | void handleErr(Result r) { 129 | if (r.failed()) { 130 | System.out.println(r.exception()); 131 | } 132 | } 133 | ```` 134 | 135 | Теперь возможное проверяемое исключение сохраняется в контейнере Result и его можно обработать в потоке. 136 | 137 | ## Выводы 138 | 139 | Для простой и "грамотной" (literate) обработки проверяемых (checked) исключений в функциональном стиле без использования внешних зависимостей необходимо 140 | 141 | 1. Выбрать подходящий контейнер для хранения результата. В простейшем случае это может быть `Optional`. Лучше использовать контейнер который может хранить значение результата или перехваченное исключение. 142 | 143 | 2. Написать функцию высшего порядка которая преобразует частичную функцию `T => R`, которая может выбросить исключение, в полную функцию `T => YourContainer` и применять ее в случае необходимости. 144 | 145 | ## Ссылки 146 | На github-e 147 | 148 | [JavaDoc](https://skopylov58.github.io/java-functional-addons/com/github/skopylov58/functional/TryUtils.html) 149 | 150 | [Source Code](https://github.com/skopylov58/java-functional-addons/blob/master/function/src/main/java/com/github/skopylov58/functional/TryUtils.java) 151 | 152 | [Junit tests](https://github.com/skopylov58/java-functional-addons/blob/master/function/src/test/java/com/github/skopylov58/functional/TryUtilsTest.java) 153 | 154 | Автор - Сергей А. Копылов 155 | e-mail skopylov@gmail.com 156 | 157 | -------------------------------------------------------------------------------- /func-exc-handling.md: -------------------------------------------------------------------------------- 1 | # Обработка исключений в Java в функциональном стиле. 2 | 3 | В данной статье автор предоставит информацию о собственной библиотеке для обработки исключений (Exception) в функциональном стиле. 4 | 5 | ## Предпосылки 6 | 7 | В Java начиная с версии 8 появились новые возможности в виде функциональных интерфейсов и потоков (Stream API). Эти возможности позволяют писать код в новом функциональном стиле без явных циклов,временных переменных, условий ветвления и проч. Я уверен что этот стиль программирования станет со временем основным для большинства Java программистов. 8 | 9 | Однако применение функционального стиля на практике осложняется тем, что все стандартные функциональные интерфейсы из пакета `java.util.function` не объявляют проверяемых исключений (являются checked exception unaware). 10 | 11 | Рассмотрим простой пример преобразования URL из строкового представления к объектам URL. 12 | 13 | ```java 14 | public List urlListTraditional(String[] urls) { 15 | return Stream.of(urls) 16 | .map(URL::new) //MalformedURLException here 17 | .collect(Collectors.toList()); 18 | } 19 | ``` 20 | 21 | К сожалению данный код не будет компилироваться из-за того, что конструктор URL может выбросить `MalformedURLException`. Правильный код будет выглядеть следующим образом 22 | 23 | ```java 24 | public List urlListTraditional(String[] urls) { 25 | return Stream.of(urls) 26 | .map(s -> { 27 | try { 28 | return new URL(s); 29 | } catch (MalformedURLException me) { 30 | return null; 31 | } 32 | }).filter(Objects::nonNull) 33 | .collect(Collectors.toList()); 34 | } 35 | ``` 36 | Мы должны явно обработать `MalformedURLException`, вернуть `null` из лямбды, а затем отфильтровать нулевые значения в потоке. Увы, такой код на практике нивелирует все преимущества функционального подхода. 37 | 38 | При использовании функционального подхода к обработке исключений, код может выглядеть гораздо приятней. 39 | 40 | ```java 41 | public List urlListWithTry(String[] urls) { 42 | return Stream.of(urls) 43 | .map(s -> Try.of(() -> new URL(s))) 44 | .flatMap(Try::stream) 45 | .collect(Collectors.toList()); 46 | } 47 | ``` 48 | 49 | Итак, по порядку про `Try` 50 | 51 | ## Интерфейс `Try` 52 | 53 | Интерфейс `Try` представляет собой некоторое вычисление, которое может завершиться успешно с результатом типа T или неуспешно с исключением. `Try` очень похож на Java `Optional`, который может иметь результат типа T или не иметь результата вообще (иметь null значение). 54 | 55 | Объекты `Try` создаются с помощью статического фабричного метода `Try.of(...)` который принимает параметром поставщика (supplier) значения типа T, который может выбросить любое исключение Exception. 56 | 57 | ```java 58 | Try url = Try.of(() -> new URL("foo")); 59 | ``` 60 | 61 | Каждый объект `Try` находится в одном из двух состояний - успеха или неудачи, что можно узнать вызывая методы `Try#isSuccess()` или `Try#isFailure()`. 62 | 63 | Для логирования исключений подойдет метод `Try#onFailure(Consumer)`, для обработки успешных значений - `Try#.onSuccess(Consumer)`. 64 | 65 | Многие методы `Try` возвращают также объект Try, что позволяет соединять вызовы методов через точку (method chaining). Вот пример как можно открыть InputStream от строкового представления URL в несколько строк без явного использования `try/catch`. 66 | 67 | ``` 68 | Optional input = 69 | Try.success(urlString) //Factory method to create success Try from value 70 | .filter(Objects::nonNull) //Filter null strings 71 | .map(URL::new) //Creating URL from string, may throw an Exception 72 | .map(URL::openStream) //Open URL input stream, , may throw an Exception 73 | .onFailure(e -> logError(e)) //Log possible error 74 | .optional(); //Convert to Java Optional 75 | ``` 76 | 77 | ## Интеграция `Try` с Java `Optional` и `Stream` 78 | 79 | `Try` легко превращается в `Optional` при помощи метода `Try#optional()`, так что в случае неуспешного Try вернется `Optional.empty`. 80 | 81 | Я намеренно сделал `Try` API восьма похожим на Java `Optional` API. Методы `Try#filter(Predicate)` и `Try#map(Function)` имеют аналогичную семантику соответствующих методов из Optional. Так что если Вы знакомы с `Optional`, то Вы легко будете работать с `Try`. 82 | 83 | `Try` легко превращается в `Stream` при помощи метода `Try#stream()` точно так же, как это сделано для `Optional#stream()`. Успешный Try превращается в поток (stream) из одного элемента типа T, неуспешный Try - в пустой поток. 84 | 85 | Фильтровать успешные попытки в потоке можно двумя способами - первый традиционный с использованием `Try#filter()` 86 | 87 | ``` 88 | ... 89 | .filter(Try::isSuccess) 90 | .map(Try::get) 91 | ... 92 | 93 | ``` 94 | 95 | Второй короче - при помощи `Try#stream()` 96 | 97 | ``` 98 | ... 99 | .flatMap(Try::stream) 100 | ... 101 | ``` 102 | 103 | будет фильтровать в потоке неуспешные попытки и возвращать поток успешных значений. 104 | 105 | ## Восстановление после сбоев (Recovering from failures) 106 | 107 | `Try` имеет встроенные средства `recover(...)` для восстановления после сбоев если вы имеете несколько стратегий для получения результата T. Предположим у Вас есть несколько стратегий: 108 | 109 | ``` 110 | public T planA(); 111 | public T planB(); 112 | public T planC(); 113 | 114 | ``` 115 | 116 | Задействовать все три стратегии/плана одновременно в коде можно следующим образом 117 | 118 | ``` 119 | Try.of(this::planA) 120 | .recover(this::planB) 121 | .recover(this::planC) 122 | .onFailure(...) 123 | .map(...) 124 | ... 125 | 126 | ``` 127 | 128 | В данном случае сработает только первый успешный план (или ни один из них). Например, если план А не сработал, но сработал план Б, то план С не будет выполняться. 129 | 130 | 131 | ## Работа с ресурсами (`Try` with resources) 132 | 133 | `Try` имплементирует AutoCloseable интерфейс, а следовательно `Try` можно использовать внутри `try-with-resource` блока. Допустим нам надо открыть сокет, записать несколько байт в выходной поток сокета и затем закрыть сокет. Соответствующий код с использованием `Try` будет выглядеть следующим образом. 134 | 135 | ``` 136 | try (var s = Try.of(() -> new Socket("host", 8888))) { 137 | s.map(Socket::getOutputStream) 138 | .onSuccess(out -> out.write(new byte[] {1,2,3})) 139 | .onFailure(e -> System.out.println(e)); 140 | } 141 | ``` 142 | 143 | Сокет будет закрыт при выходе за последнюю фигурную скобку. 144 | 145 | ## Выводы 146 | 147 | `Try` позволяет обрабатывать исключения в функциональном стиле без явного использования конструкций `try/catch/finally` и поможет сделать Ваш код более коротким, выразительным, легким для понимания и сопровождения. 148 | 149 | Надеюсь Вы получите удовольствие от использования `Try` 150 | 151 | ## Ссылки 152 | 153 | Автор - Сергей А. Копылов 154 | e-mail skopylov@gmail.com 155 | 156 | Последнее место работы 157 | Communications Solutions CMS IUM R&D Lab 158 | Hewlett Packard Enterprise 159 | Ведущий специалист 160 | 161 | [Код библиотеки на github - https://github.com/skopylov58/java-functional-addons](https://github.com/skopylov58/java-functional-addons) 162 | 163 | [Try JavaDoc - https://skopylov58.github.io/java-functional-addons/com/skopylov/functional/Try.html](https://skopylov58.github.io/java-functional-addons/com/skopylov/functional/Try.html) 164 | 165 | Еще одна функциональная библиотека для Java - https://www.vavr.io/ 166 | 167 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skopylov58/java-functional-addons/f4ada603581c600f058fc726c36d956215289ede/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /idioms-and-patterns.md: -------------------------------------------------------------------------------- 1 | # Идиомы и паттерны (Design patterns) в функциональной Java 2 | 3 | ## Декораторы (Decorators) 4 | 5 | ## Цепочка ответственности (Chain of Responsibility) 6 | 7 | ## Кэширование результатов (Memoization) 8 | 9 | ```java 10 | static Function memoize(Function func, Map cache) { 11 | return t -> cache.computeIfAbsent(t, func::apply); 12 | } 13 | 14 | static Function memoize(Function func) { 15 | return memoize(func, new ConcurrentHashMap<>()); 16 | } 17 | 18 | ``` 19 | 20 | 21 | ## Однократное исполнение (Run once) 22 | 23 | ## Обработка ошибок (Error handling with Monads) 24 | 25 | ## Создание объектов (Builder) 26 | 27 | [Exploring Joshua Bloch’s Builder design pattern in Java](https://blogs.oracle.com/javamagazine/post/exploring-joshua-blochs-builder-design-pattern-in-java) 28 | 29 | ```java 30 | public record BookRecord(String isbn, String title, String genre, String author, int published, String description) { 31 | 32 | private BookRecord(Builder builder) { 33 | this(builder.isbn, builder.title, builder.genre, builder.author, builder.published, builder.description); 34 | } 35 | 36 | public static class Builder { 37 | private final String isbn; 38 | private final String title; 39 | String genre; 40 | String author; 41 | int published; 42 | String description; 43 | 44 | public Builder(String isbn, String title) { 45 | this.isbn = isbn; 46 | this.title = title; 47 | } 48 | 49 | public Builder configure(Consumer b) { 50 | b.accept(this); 51 | return this; 52 | } 53 | 54 | public BookRecord build() { 55 | return new BookRecord(this); 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | Использование 62 | 63 | ```java 64 | var bookRec = new BookRecord.Builder("1234", "foo bar") 65 | .configure(book -> { 66 | book.author = "author"; 67 | book.description = "desc"; 68 | }) 69 | .build(); 70 | ``` 71 | 72 | ## Стратегия (Strategy) 73 | 74 | ## Наблюдатель (Observer) ??? 75 | 76 | ## Шаблонный метод (Template Method) 77 | 78 | 79 | -------------------------------------------------------------------------------- /pics/exponential-backoff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skopylov58/java-functional-addons/f4ada603581c600f058fc726c36d956215289ede/pics/exponential-backoff.png -------------------------------------------------------------------------------- /retry.md: -------------------------------------------------------------------------------- 1 | # Неблокирующий повтор (retry) в Java и проект Loom 2 | 3 | ## Введение 4 | 5 | Повтор (retry) операции является старейшим механизмом обеспечения надежности программного обеспечения. Мы используем повторы при выполнении HTTP запросов, запросов к базам данных, отсылке электронной почты и проч. и проч. 6 | 7 | ## Наивный повтор 8 | 9 | Если Вы программировали, то наверняка писали процедуру какого либо повтора. Простейший повтор использует цикл с некоторым ожиданием после неудачной попытки. Примерно вот так. 10 | 11 | ```java 12 | static T retry(long maxTries, Duration delay, CheckedSupplier supp) { 13 | for (int i = 0; i < maxTries; i++) { 14 | try { 15 | return supp.get(); 16 | } catch (Exception e) { 17 | if (i < maxTries - 1) { //not last attempt 18 | try { 19 | Thread.sleep(delay.toMillis()); 20 | } catch (InterruptedException ie) { 21 | Thread.currentThread().interrupt(); //Propagate interruption 22 | break; 23 | } 24 | } 25 | } 26 | } 27 | throw new RuntimeException("Retry failed after %d retries".formatted(maxTries)); 28 | } 29 | ``` 30 | 31 | Вот пример использования повтора для получения соединения к базе данных. Мы делаем 10 попыток с задержкой 100 msec. 32 | 33 | ```java 34 | Connection retryConnection() { 35 | return retry(10, Duration.ofMillis(100), () -> DriverManager.getConnection("jdbc:foo")); 36 | } 37 | ``` 38 | 39 | Этот код блокирует Ваш поток на одну секунду (точнее 900 milliseconds, мы не ждем после последней попытки) потому что ```Thread.sleep()``` является блокирующей операцией. Попробуем оценить производительность метода в терминах количества потоков (threads) и времени. Предположим нам нужно сделать 12 операций повтора. Нам потребуется 12 потоков чтобы выполнить задуманное за минимальное время 1 сек, 6 потоков выполнят повторы на 2 сек, 4 - за три секунды, один поток - за 12 секунд. А что если нам потребуется выполнить 1000 операций повтора? Для быстрого выполнения потребуется 1000 потоков (нет, только не это) или 16 минут в одном потоке. Как мы видим этот метод плохо масштабируется. 40 | 41 | Давайте проверим это на тесте 42 | 43 | ```java 44 | public void testNaiveRetry() throws Exception { 45 | ExecutorService ex = Executors.newFixedThreadPool(4); //4 threads 46 | Duration dur = TryUtils.measure(() -> { 47 | IntStream.range(0, 12) //12 retries 48 | .mapToObj(i -> ex.submit(() -> this.retryConnection())) //submit retry 49 | .toList() //collect all 50 | .forEach(f -> Try.of(()->f.get())); //wait all finished 51 | }); 52 | System.out.println(dur); 53 | } 54 | 55 | Output: PT2.723379388S 56 | ``` 57 | Теоретически: 900 ms * 3 = 2.7 sec, хорошее совпадение 58 | 59 | ## Неблокирующий повтор 60 | 61 | А можно ли делать повторы не блокируя потоки? Можно, если вместо ```Thread.sleep()``` использовать перепланирование потока на некоторый момент в будущем при помощи ```CompletableFuture.delayedExecutor()```. Как это сделать можно посмотреть в моем классе ```Retry.java```. Кстати подобный подход используется в неблокирующем методе ```delay()``` в Kotlin. 62 | 63 | Retry.java - компактный класс без внешних зависимостей который может делать неблокирующий асинхронный повтор операции ( [исходный код](https://github.com/skopylov58/java-functional-addons/blob/master/function/src/main/java/com/github/skopylov58/retry/Retry.java), [Javadoc](https://skopylov58.github.io/java-functional-addons/com/github/skopylov58/retry/package-summary.html) ). 64 | Полное описание возможностей ```Retry``` [с примерами можно посмотреть тут](https://github.com/skopylov58/java-functional-addons#retryt---non-blocking-asynchronous-functional-retry-procedure) 65 | 66 | 67 | Так можно сделать повтор, который мы уже делали выше. 68 | 69 | ```java 70 | CompletableFuture retryConnection(Executor ex) { 71 | return Retry.of(() -> DriverManager.getConnection("jdbc:foo")) 72 | .withFixedDelay(Duration.ofMillis(100)) 73 | .withExecutor(ex) 74 | .retry(10); 75 | } 76 | ``` 77 | 78 | Мы видим что здесь возвращается не ```Connection```, а ```CompletableFuture```, что говорит об асинхронной природе этого вызова. Давайте попробуем выполнить 1000 повторов в одном потоке при помощи ```Retry```. 79 | 80 | ```java 81 | public void testRetry() throws Exception { 82 | Executor ex = Executors.newSingleThreadExecutor(); //Один поток 83 | Duration dur = TryUtils.measure(() -> { //Это удобная утилита для измерения времен 84 | IntStream.range(0, 1_000) //1000 раз 85 | .mapToObj(i -> this.retryConnection(ex)) //запускаем операцию повтора 86 | .toList() //собираем future в список 87 | .forEach(f -> Try.of(()->f.get())); //дожидаемся всех результатов 88 | }); 89 | System.out.println(dur); //печатаем прошедшее время 90 | } 91 | 92 | Output: PT1.065544748S 93 | ``` 94 | 95 | Как мы видим, ```Retry``` не блокируется и выполняет 1000 повторов в одном потоке чуть-чуть более одной секунды. Ура, мы можем эффективно делать повторы. 96 | 97 | ## Причем здесь проект Loom? 98 | 99 | Проект Loom добавляет в JDK 19 виртуальные потоки (пока в стадии preview). Цель введения виртуальных потоков лучше всего описана в [JEP 425](https://openjdk.org/jeps/425) и я рекомендую это к прочтению. 100 | 101 | Возвращаясь к нашей теме повтора операций, коротко скажу что ```Thread.sleep()``` более не является блокирующей операцией будучи выполняемой в виртуальном потоке. Точнее говоря, ```sleep()``` приостановит (suspend) виртуальный поток, давая возможность системному потоку (carrier thread) переключиться на выполнение других виртуальных потоков. После истечения срока сна, виртуальный поток возобновит (resume) свою работу. Давайте запустим наивный алгоритм повтора в виртуальных потоках. 102 | 103 | ```java 104 | var dur =TryUtils.measure(() -> { 105 | ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); 106 | IntStream.range(0, 1_000) 107 | .mapToObj(i -> executor.submit(() -> retryConnection())) 108 | .toList() 109 | .forEach(f -> Try.of(() -> f.get())); 110 | }); 111 | System.out.println(dur); 112 | 113 | Output: PT1.010342284S 114 | ``` 115 | 116 | Поразительно, имеем чуть более одной секунды на 1000 повторов, как и при использовании ```Retry```. 117 | 118 | Проект Loom принесет кардинальные изменения в экосистему Java. 119 | 120 | - Стиль виртуальный поток на запрос (thread per request) масштабируется с почти оптимальным использованием процессорных ядер. Этот стиль становится рекомендуемым. 121 | 122 | - Виртуальные потоки не усложняют модель программирования и не требуют изучения новых концепций, автор JEP 425 говорит что скорее нужно будет забыть старые привычки ("it may require unlearning habits developed to cope with today's high cost of threads.") 123 | 124 | - Многие стандартные библиотеки модифицированы для совместной работы с виртуальными потоками.Так например, блокирующие операции чтения из сокета станут неблокирующими в виртуальном потоке. 125 | 126 | - Реактивный/асинхронный стиль программирования становится практически не нужным. 127 | 128 | Нас ждут интересные времена в ближайшем будущем. Я очень надеюсь что виртуальные потоки станут стандартной частью JDK в очередной LTS версии Java. 129 | 130 | Сергей Копылов 131 | skopylov@gmail.com 132 | [Резюме](https://github.com/skopylov58/cv) 133 | [Смотреть код на Github](https://github.com/skopylov58/) 134 | 135 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'java-functional-addons' 2 | 3 | //include 'function' 4 | 5 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/Decorator.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.Function; 5 | 6 | public class Decorator implements Function { 7 | 8 | private final Consumer before; 9 | private final Function func; 10 | 11 | private Decorator(Function func, Consumer consumer) { 12 | this.func = func; 13 | before = consumer; 14 | } 15 | 16 | public static Function decorate(Function func, Consumer before ) { 17 | return new Decorator<>(func, before); 18 | } 19 | 20 | @Override 21 | public R apply(T t) { 22 | before.accept(t); 23 | return func.apply(t); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/Either.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.io.Closeable; 4 | import java.util.Optional; 5 | import java.util.function.Consumer; 6 | import java.util.function.Function; 7 | import java.util.function.Predicate; 8 | import java.util.stream.Stream; 9 | 10 | import com.github.skopylov58.functional.Try.CheckedConsumer; 11 | import com.github.skopylov58.functional.Try.CheckedFunction; 12 | import com.github.skopylov58.functional.Try.CheckedSupplier; 13 | 14 | /** 15 | * Minimal functional Either implementation. 16 | * 17 | *

18 | * 19 | * It does not have left or right projections, map/flatMap/filter operations work only on 20 | * right side of Either and do not have effect for left side. Anyway you can use {@link #swap()} for this purpose. 21 | * 22 | *

23 | * 24 | * Either intentionally does not have any getX() methods, use {@link #fold(Function, Function)}, 25 | * {@link #optional()} or {@link #stream()} instead. 26 | * 27 | *

28 | * 29 | * Either has a set of {@link #catching(CheckedConsumer)} higher order functions which will be useful for exception handling. 30 | * 31 | *

 32 |  *       var socket = catching(() -> new Socket("foo", 1234));
 33 |  *       try (var c = socket.asCloseable()) {
 34 |  *           socket.flatMap(catching(Socket::getOutputStream))
 35 |  *           .flatMap(catching(o -> {
 36 |  *            o.write(new byte[] {1,2,3});
 37 |  *        }));
 38 |  *    }
 39 |  * }));
 40 |  * 
41 | * 42 | * @author skopylov@gmail.com 43 | * 44 | * @param left side type 45 | * @param right side type 46 | */ 47 | @SuppressWarnings("rawtypes") 48 | public sealed interface Either permits Either.Left, Either.Right { 49 | 50 | /** 51 | * Checks if this is left side. 52 | * @return true is this is left side of Either. 53 | */ 54 | default boolean isLeft() { 55 | return fold(__ -> true, __ -> false); 56 | } 57 | 58 | /** 59 | * Checks if this is right side. 60 | * @return true is this is right side of Either. 61 | */ 62 | default boolean isRight() {return !isLeft();} 63 | 64 | /** 65 | * Converts Either to Stream 66 | * @return one element stream for right, empty stream for left. 67 | */ 68 | default Stream stream() { 69 | return fold(__ -> Stream.empty(), Stream::of); 70 | } 71 | 72 | /** 73 | * Converts Either to Optional 74 | * @return Optional for right, Optional empty for left. 75 | */ 76 | default Optional optional() { 77 | return fold(__ -> Optional.empty(), Optional::ofNullable); 78 | } 79 | 80 | /** 81 | * Maps right side of the Either. 82 | * @param new right type 83 | * @param mapper mapper function that maps R type to T type 84 | * @return new Either 85 | */ 86 | @SuppressWarnings("unchecked") 87 | default Either map(Function mapper) { 88 | return (Either) fold(__ -> this, r -> right(mapper.apply(r))); 89 | } 90 | 91 | /** 92 | * Flat maps right side of the Either. 93 | * @param new right type 94 | * @param mapper R to {@code Either} 95 | * @return new {@code Either} 96 | */ 97 | @SuppressWarnings("unchecked") 98 | default Either flatMap(Function> mapper) { 99 | return (Either) fold(__ -> this, mapper::apply); 100 | } 101 | 102 | /** 103 | * Filters right side of Either, does not have effect if it is left side. 104 | * @param predicate condition to test 105 | * @param leftMapper will map R to L if predicate return false 106 | * @return Either 107 | */ 108 | default Either filter(Predicate predicate, Function leftMapper) { 109 | return fold( 110 | left -> this, 111 | right -> predicate.test(right) ? this : left(leftMapper.apply(right)) 112 | ); 113 | } 114 | 115 | @SuppressWarnings("unchecked") 116 | default Either> filter(Predicate predicate) { 117 | return (Either>) fold( 118 | left -> this, 119 | right -> predicate.test(right) ? right(Optional.of(right)) : right(Optional.empty()) 120 | ); 121 | } 122 | 123 | /** 124 | * Swaps Either 125 | * @return swapped Either 126 | */ 127 | default Either swap() { 128 | return fold(Either::right, Either::left); 129 | } 130 | 131 | /** 132 | * Folds Either to type T 133 | * @param folded type 134 | * @param leftMapper maps left to T 135 | * @param rightMapper maps right to T 136 | * @return value of T type 137 | */ 138 | T fold(Function leftMapper, Function rightMapper); 139 | 140 | /** 141 | * Produces side effects for Either 142 | * @param leftConsumer left side effect 143 | * @param rightConsumer right side effect 144 | * @return this Either 145 | */ 146 | default Either accept(Consumer leftConsumer, Consumer rightConsumer) { 147 | fold(left -> { 148 | leftConsumer.accept(left); 149 | return null; 150 | }, 151 | right -> { 152 | rightConsumer.accept(right); 153 | return null; 154 | }); 155 | return this; 156 | } 157 | 158 | /** 159 | * Factory method to produce right side of Either from R value 160 | * @param type of right side 161 | * @param type of left side 162 | * @param right right value 163 | * @return {@link Either.Right} 164 | */ 165 | static Either right(R right) {return new Right<>(right);} 166 | 167 | /** 168 | * Factory method to produce left side of Either from L value 169 | * @param type of right side 170 | * @param type of left side 171 | * @param left left value 172 | * @return {@link Either.Left} 173 | */ 174 | static Either left(L left) {return new Left<>(left);} 175 | 176 | /** 177 | * Right side of Either. 178 | * @author skopylov@gmail.com 179 | * 180 | * @param type of right side 181 | * @param type of left side 182 | */ 183 | record Right(R right) implements Either { 184 | @Override 185 | public T fold(Function leftMapper, Function rightMapper) { 186 | return rightMapper.apply(right); 187 | } 188 | } 189 | 190 | /** 191 | * Left side of Either. 192 | * @author skopylov@gmail.com 193 | * 194 | * @param type of right side 195 | * @param type of left side 196 | */ 197 | 198 | record Left(L left) implements Either { 199 | @Override 200 | public T fold(Function leftMapper, Function rightMapper) { 201 | return leftMapper.apply(left); 202 | } 203 | } 204 | 205 | /** 206 | * Factory method to produce {@code Either} from throwing supplier 207 | * @param right side type 208 | * @param supplier throwing supplier of R 209 | * @return Either 210 | */ 211 | static Either catching(CheckedSupplier supplier) { 212 | try { 213 | return right(supplier.get()); 214 | } catch (Exception e) { 215 | return left(e); 216 | } 217 | } 218 | 219 | /** 220 | * Lifts throwing {@code R->T} function to total {@code R->Either} function. 221 | * May be useful with flatMap. 222 | * @param right side type 223 | * @param left side type 224 | * @param mapper throwing function 225 | * @return lifted function 226 | */ 227 | static Function> catching(CheckedFunction mapper) { 228 | return param -> { 229 | try { 230 | return right(mapper.apply(param)); 231 | } catch (Exception e) { 232 | return left(e); 233 | } 234 | }; 235 | } 236 | 237 | /** 238 | * Lifts throwing consumer to total {@code R->Either} function. 239 | * @param right side type 240 | * @param consumer 241 | * @return lifted function 242 | */ 243 | static Function> catching(CheckedConsumer consumer) { 244 | return param -> { 245 | try { 246 | consumer.accept(param); 247 | return right(param); 248 | } catch (Exception e) { 249 | return left(e); 250 | } 251 | }; 252 | } 253 | 254 | /** 255 | * Converts this Either to Closeable which is handy to use in try-with-resources block. 256 | * @return Closeable 257 | */ 258 | default Closeable asCloseable() { 259 | return fold( 260 | left -> () -> { 261 | if (left instanceof Closeable clo) { 262 | clo.close(); 263 | } 264 | }, 265 | right -> () -> { 266 | if (right instanceof Closeable clo) { 267 | clo.close(); 268 | } 269 | }); 270 | } 271 | 272 | } 273 | 274 | 275 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/FPUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.lang.System.Logger.Level; 4 | import java.time.Duration; 5 | import java.time.Instant; 6 | import java.util.Map; 7 | import java.util.NoSuchElementException; 8 | import java.util.Optional; 9 | import java.util.concurrent.Callable; 10 | import java.util.concurrent.CompletableFuture; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.concurrent.atomic.AtomicBoolean; 13 | import java.util.concurrent.atomic.AtomicReference; 14 | import java.util.function.BiConsumer; 15 | import java.util.function.BiFunction; 16 | import java.util.function.Consumer; 17 | import java.util.function.Function; 18 | import java.util.function.LongPredicate; 19 | import java.util.function.Predicate; 20 | import java.util.function.Supplier; 21 | import java.util.function.UnaryOperator; 22 | import java.util.stream.LongStream; 23 | import java.util.stream.Stream; 24 | import com.github.skopylov58.functional.Try.CheckedConsumer; 25 | 26 | /** 27 | * Collection of useful higher-order functions to handle exceptions in functional style. 28 | * 29 | * @author skopylov@gmail.com 30 | * 31 | */ 32 | public interface FPUtils { 33 | 34 | /** 35 | * Function that may throw an exception. 36 | */ 37 | @FunctionalInterface 38 | interface CheckedFunction { 39 | R apply(T t) throws Exception; 40 | } 41 | 42 | /** Supplier that may throw an exception. */ 43 | @FunctionalInterface 44 | interface CheckedSupplier { 45 | T get() throws Exception; 46 | } 47 | 48 | /** Try Result for Java 8+ */ 49 | public static class ResultJava8 { 50 | private final Object value; 51 | 52 | public ResultJava8(T t) { 53 | value = t; 54 | } 55 | 56 | public ResultJava8(Exception e) { 57 | value = e; 58 | } 59 | 60 | public boolean failed() { 61 | return value instanceof Exception; 62 | } 63 | 64 | public T result() { 65 | if (failed()) { 66 | throw new NoSuchElementException("Cause: " + value); 67 | } 68 | return (T) value; 69 | } 70 | 71 | public Exception cause() { 72 | if (!failed()) { 73 | throw new IllegalStateException("Value: " + value); 74 | } 75 | return (Exception) value; 76 | } 77 | } 78 | 79 | /** Try Result for Java 14+ */ 80 | @SuppressWarnings("unchecked") 81 | public record Result(T result, Exception exception) { 82 | public static Result success(T t) { 83 | return new Result<>(t, null); 84 | } 85 | 86 | public static Result failure(Exception e) { 87 | return new Result<>(null, e); 88 | } 89 | 90 | public boolean isFailure() { 91 | return exception != null; 92 | } 93 | 94 | public boolean isSuccess() { 95 | return !isFailure(); 96 | } 97 | 98 | public Result map(Function mapper) { 99 | return isSuccess() ? success(mapper.apply(result)) : (Result) this; 100 | } 101 | 102 | public Result flatMap(Function> mapper) { 103 | return isSuccess() ? mapper.apply(result) : (Result) this; 104 | } 105 | 106 | public Result filter(Predicate pred) { 107 | return isSuccess() ? pred.test(result) ? this : failure(new NoSuchElementException()) : this; 108 | } 109 | 110 | public Stream stream() { 111 | return isFailure() ? Stream.empty() : Stream.of(result); 112 | } 113 | 114 | public Optional optional() { 115 | return isFailure() ? Optional.empty() : Optional.ofNullable(result); 116 | } 117 | 118 | public R fold(Function onSuccess, Function onFailure) { 119 | return isSuccess() ? onSuccess.apply(result) : onFailure.apply(exception); 120 | } 121 | 122 | public Result fold(Consumer onSuccess, Consumer onFailure) { 123 | if (isSuccess()) 124 | onSuccess.accept(result); 125 | else 126 | onFailure.accept(exception); 127 | return this; 128 | } 129 | 130 | public Result handle(BiFunction> handler) { 131 | return handler.apply(result, exception); 132 | } 133 | 134 | public Result recover(Supplier> supplier) { 135 | return isFailure() ? supplier.get() : this; 136 | } 137 | 138 | public Result recover(Function> errorMapper) { 139 | return isFailure() ? errorMapper.apply(exception) : this; 140 | } 141 | 142 | static Function> catching(CheckedFunction func) { 143 | return param -> { 144 | try { 145 | return success(func.apply(param)); 146 | } catch (Exception e) { 147 | return failure(e); 148 | } 149 | }; 150 | } 151 | 152 | static Result catching(CheckedSupplier supplier) { 153 | try { 154 | return success(supplier.get()); 155 | } catch (Exception e) { 156 | return failure(e); 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * Higher-order function to convert partial function {@code T=>R} to total function 163 | * {@code T=>Result} 164 | * 165 | * @param function input parameter type 166 | * @param function result type 167 | * @param func partial function {@code T=>R} that may throw checked exception 168 | * @return total function {@code T=>Result} 169 | */ 170 | static Function> toResult(CheckedFunction func) { 171 | return param -> { 172 | try { 173 | return Result.success(func.apply(param)); 174 | } catch (Exception e) { 175 | return Result.failure(e); 176 | } 177 | }; 178 | } 179 | 180 | default Result onSuccessCatching(Result result, CheckedConsumer cons) { 181 | CheckedFunction f = param -> { 182 | cons.accept(param); 183 | return param; 184 | }; 185 | return result.flatMap(Result.catching(f)); 186 | } 187 | 188 | /** 189 | * Higher-order function to convert partial function {@code T=>R} to total function 190 | * {@code T=>Optional} 191 | * 192 | * @param function input parameter type 193 | * @param function result type 194 | * @param func partial function {@code T=>R} that may throw checked exception 195 | * @return total function {@code T=>Optional} 196 | */ 197 | static Function> toOptional(CheckedFunction func) { 198 | return toOptional(func, (t, e) -> { 199 | }); 200 | } 201 | 202 | /** 203 | * Higher-order function to convert partial function {@code T=>R} to total function 204 | * {@code T=>Optional} Gives access to Exception and corresponding input value. 205 | * 206 | * @param function input parameter type 207 | * @param function result type 208 | * @param func partial function {@code T=>R} that may throw checked exception 209 | * @param consumer has access to exception and corresponding input value 210 | * @return total function {@code T=>Optional} 211 | */ 212 | static Function> toOptional(CheckedFunction func, 213 | BiConsumer consumer) { 214 | return param -> { 215 | try { 216 | return Optional.ofNullable(func.apply(param)); 217 | } catch (Exception e) { 218 | consumer.accept(param, e); 219 | return Optional.empty(); 220 | } 221 | }; 222 | } 223 | 224 | static Function catchingMapper(CheckedFunction func, 225 | BiFunction errorMapper) { 226 | return param -> { 227 | try { 228 | return func.apply(param); 229 | } catch (Exception e) { 230 | return errorMapper.apply(param, e); 231 | } 232 | }; 233 | } 234 | 235 | static Function throwingMapper(CheckedFunction func) { 236 | return param -> { 237 | try { 238 | return func.apply(param); 239 | } catch (Exception e) { 240 | sneakyThrow(e); 241 | return null; 242 | } 243 | }; 244 | } 245 | 246 | static Function catchingMapper(CheckedFunction func, Supplier supplier) { 247 | return catchingMapper(func, (t, e) -> supplier.get()); 248 | } 249 | 250 | 251 | /** 252 | * Higher-order function to convert partial supplier {@code ()=>T} to total supplier 253 | * {@code ()=>Optional} 254 | * 255 | * @param supplier result type 256 | * @param supplier {@code ()=>T} that may throw an exception 257 | * @return total supplier {@code ()=>Optional} 258 | */ 259 | static Supplier> toOptional(CheckedSupplier supplier) { 260 | return toOptional(supplier, e -> { 261 | }); 262 | } 263 | 264 | /** 265 | * Higher-order function to convert partial supplier {@code ()=>T} to total supplier 266 | * {@code ()=>Optional} 267 | * 268 | * @param supplier result type 269 | * @param supplier {@code ()=>T} that may throw an exception 270 | * @param consumer error consumer 271 | * @return total supplier {@code ()=>Optional} 272 | */ 273 | static Supplier> toOptional(CheckedSupplier supplier, 274 | Consumer consumer) { 275 | return () -> { 276 | try { 277 | return Optional.ofNullable(supplier.get()); 278 | } catch (Exception e) { 279 | consumer.accept(e); 280 | return Optional.empty(); 281 | } 282 | }; 283 | } 284 | 285 | /** 286 | * Higher-order function to convert partial function {@code T=>R} to total function 287 | * {@code T=>Either} 288 | * 289 | * @param function input parameter type 290 | * @param function result type 291 | * @param func partial function {@code T=>R} that may throw an exception 292 | * @return total function {@code T=>Either} 293 | */ 294 | static Function> toEither(CheckedFunction func) { 295 | return param -> { 296 | try { 297 | return Either.right(func.apply(param)); 298 | } catch (Exception e) { 299 | return Either.left(e); 300 | } 301 | }; 302 | } 303 | 304 | static Function> toResultJava8(CheckedFunction func) { 305 | return param -> { 306 | try { 307 | return new ResultJava8<>(func.apply(param)); 308 | } catch (Exception e) { 309 | return new ResultJava8<>(e); 310 | } 311 | }; 312 | } 313 | 314 | static Function> toFuture(CheckedFunction func) { 315 | return param -> { 316 | try { 317 | return CompletableFuture.completedFuture(func.apply(param)); 318 | } catch (Exception e) { 319 | return CompletableFuture.failedFuture(e); 320 | } 321 | }; 322 | } 323 | 324 | @SuppressWarnings("unchecked") 325 | public static void sneakyThrow(Throwable e) throws E { 326 | throw (E) e; 327 | } 328 | 329 | static Supplier toSupplier(CheckedSupplier supplier) { 330 | return () -> { 331 | try { 332 | return supplier.get(); 333 | } catch (Exception e) { 334 | sneakyThrow(e); 335 | return null; // we never will get here 336 | } 337 | }; 338 | } 339 | 340 | static Duration measure(Runnable runnable) { 341 | Instant start = Instant.now(); 342 | runnable.run(); 343 | Instant end = Instant.now(); 344 | return Duration.between(start, end); 345 | } 346 | 347 | static Function after(Function func, Consumer after) { 348 | return t -> { 349 | R r = func.apply(t); 350 | after.accept(r); 351 | return r; 352 | }; 353 | } 354 | 355 | static Function before(Function func, Consumer before) { 356 | return t -> { 357 | before.accept(t); 358 | return func.apply(t); 359 | }; 360 | } 361 | 362 | public static Function memoize(Function func, Map cache) { 363 | return t -> cache.computeIfAbsent(t, func::apply); 364 | } 365 | 366 | public static Function memoize(Function func) { 367 | return memoize(func, new ConcurrentHashMap<>()); 368 | } 369 | 370 | public static Runnable once(Runnable runnable, AtomicBoolean runFlag) { 371 | return () -> { 372 | if (runFlag.compareAndSet(false, true)) { 373 | runnable.run(); 374 | } 375 | }; 376 | } 377 | 378 | /** 379 | * Runs runnable exactly once. 380 | * @param run runnable to decorate 381 | * @return runnable that will run only once. 382 | */ 383 | public static Runnable once(Runnable run) { 384 | return once(run, new AtomicBoolean(false)); 385 | } 386 | 387 | public static Supplier memoize(Supplier supplier, AtomicReference ref) { 388 | return () -> ref.getAndUpdate(t -> t == null ? supplier.get() : t); 389 | } 390 | 391 | public static Supplier memoize(Supplier supplier) { 392 | return memoize(supplier, new AtomicReference()); 393 | } 394 | 395 | /** 396 | * Consumer to UnaryOperator conversion. 397 | * 398 | * @param type T 399 | * @param cons consumer 400 | * @return UnaryOperator 401 | */ 402 | static UnaryOperator ctou(Consumer cons) { 403 | return t -> { 404 | cons.accept(t); 405 | return t; 406 | }; 407 | } 408 | 409 | @FunctionalInterface 410 | interface Backoff extends Function{ 411 | 412 | default Backoff withJitter(Supplier jitter) { 413 | return i -> apply(i).plus(jitter.get()); 414 | } 415 | } 416 | 417 | /** 418 | * Simple jitter using system timer. 419 | * @param duration maximum jitter value 420 | * @return random jitter between 0 and duration 421 | */ 422 | static Supplier simpleJitter(Duration duration) { 423 | long m = System.currentTimeMillis() % duration.toMillis(); 424 | return () -> Duration.ofMillis(m); 425 | } 426 | 427 | /** 428 | * Backoff strategy that starts with min value, increasing delay twice on each step, with max upper threshold. 429 | * @param min min delay in milliseconds 430 | * @param max max delay in milliseconds 431 | * @return exponential backoff 432 | */ 433 | static Backoff exponentialBackoff(long min, long max) { 434 | if (min <= 0 || min >= max) { 435 | throw new IllegalArgumentException(); 436 | } 437 | var maxDur = Duration.ofMillis(max); 438 | return i -> { 439 | if (i > 31) { 440 | return maxDur; 441 | } 442 | Duration dur = Duration.ofMillis(min << i); 443 | return dur.toMillis() > max ? maxDur : dur; 444 | }; 445 | } 446 | 447 | /** 448 | * Fixed delay backoff strategy 449 | * @param delay delay between tries 450 | * @return fixed backoff 451 | */ 452 | static Backoff fixedDelay(Duration delay) { 453 | return i -> delay; 454 | } 455 | 456 | /** 457 | * Retry with fixed delay. 458 | * @param resulting type 459 | * @param callable callable to retry 460 | * @param numOfRetries number of retries 461 | * @param delay delay between tries 462 | * @return Optional result 463 | */ 464 | static Optional retry(Callable callable, long numOfRetries, Duration delay) { 465 | return retry(callable, numOfRetries, fixedDelay(delay), x -> true); 466 | } 467 | 468 | /** 469 | * Retry with backoff strategy. 470 | * @param resulting type 471 | * @param callable callable to retry 472 | * @param numOfRetries number of retries 473 | * @param backoff backoff strategy 474 | * @return Optional result 475 | */ 476 | static Optional retry(Callable callable, long numOfRetries, Backoff backoff) { 477 | return retry(callable, numOfRetries, backoff, x -> true); 478 | } 479 | 480 | static Optional retry(Callable callable, long numOfRetries, Backoff backoff, 481 | Predicate isSuccess) { 482 | for (long i = 0; i < numOfRetries && !Thread.currentThread().isInterrupted(); i++) { 483 | if (i != 0) { 484 | try { 485 | Thread.sleep(backoff.apply(i).toMillis()); 486 | } catch (InterruptedException ie) { 487 | Thread.currentThread().interrupt(); 488 | break; 489 | } 490 | } 491 | try { 492 | V value = callable.call(); 493 | if (isSuccess.test(value)) { 494 | return Optional.ofNullable(value); 495 | } 496 | } catch (Exception e) { 497 | System.getLogger(FPUtils.class.getName()).log(Level.INFO, e); 498 | } 499 | } 500 | return Optional.empty(); 501 | } 502 | 503 | static Optional retry2(Callable callable, 504 | LongPredicate lp, 505 | Backoff backoff, 506 | Predicate isSuccess) 507 | { 508 | return LongStream.iterate(0, i -> i + 1) 509 | .takeWhile(lp::test) 510 | .takeWhile(i -> i == 0 || sleep(backoff.apply(i))) 511 | .mapToObj(i -> call(callable)) 512 | .map(opt -> opt.filter(isSuccess)) 513 | .flatMap(Optional::stream) 514 | .findFirst(); 515 | } 516 | 517 | static boolean sleep(Duration delay) { 518 | if (Thread.currentThread().isInterrupted()) { 519 | return false; 520 | } 521 | try { 522 | Thread.sleep(delay.toMillis()); 523 | return true; 524 | } catch (InterruptedException e) { 525 | Thread.currentThread().interrupt(); 526 | return false; 527 | } 528 | } 529 | 530 | static Optional call(Callable callable) { 531 | try { 532 | return Optional.ofNullable(callable.call()); 533 | } catch (Exception e) { 534 | return Optional.empty(); 535 | } 536 | } 537 | 538 | } 539 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/Functor.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.function.Function; 4 | 5 | @FunctionalInterface 6 | public interface Functor { 7 | Functor map(Function mapper); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/IO.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.Function; 5 | import java.util.function.Supplier; 6 | 7 | interface IO { 8 | 9 | T run(); 10 | 11 | static IO of(Supplier s) { 12 | return s::get; 13 | } 14 | 15 | static IO of(T t) { 16 | return () -> t; 17 | } 18 | 19 | static IO consume(T t, Consumer c) { 20 | c.accept(t); 21 | return () -> null; 22 | } 23 | 24 | default IO flatMap(Function> mapper) { 25 | return () -> mapper.apply(run()).run(); 26 | } 27 | 28 | default IO map(Function mapper) { 29 | return () -> mapper.apply(run()); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/IO2.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.function.Function; 4 | 5 | public interface IO2 { 6 | 7 | Monad run(C context); 8 | 9 | default Monadrun() { 10 | return run(null); 11 | } 12 | 13 | default IO2 map(Function mapper) { 14 | return ctx -> { 15 | Monad monad = run(ctx); 16 | return monad.map(mapper); 17 | }; 18 | } 19 | 20 | default IO2 flatMap(Function> mapper) { 21 | return ctx -> { 22 | Monad monad = run(ctx); 23 | return monad.bind(t -> mapper.apply(t).run(ctx)); 24 | }; 25 | } 26 | 27 | static IO2 of(T t) { 28 | return c -> Option.some(t); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/Memo.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | import java.util.function.Function; 5 | 6 | /** 7 | * Simple memoizer. 8 | * @param 9 | * @param 10 | */ 11 | public class Memo implements Function { 12 | 13 | private ConcurrentHashMap cache = new ConcurrentHashMap<>(); 14 | private Function func; 15 | 16 | private Memo(Function func) { 17 | this.func = func; 18 | } 19 | 20 | static Function memoize(Function func) { 21 | return new Memo<>(func); 22 | } 23 | 24 | @Override 25 | public R apply(T t) { 26 | return cache.computeIfAbsent(t, tt -> func.apply(tt)); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/Monad.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.function.BiFunction; 4 | import java.util.function.Function; 5 | 6 | public interface Monad extends MonadAccessor { 7 | 8 | Monad map(Function mapper); 9 | 10 | Monad bind(Function> mapper); 11 | 12 | static Monad liftM2(Monad am, Monad bm, BiFunction mapper) { 13 | return am.bind(a -> bm.map(b -> mapper.apply(a, b))); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/MonadAccessor.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.Optional; 4 | import java.util.stream.Stream; 5 | 6 | public interface MonadAccessor { 7 | 8 | T getOrDefault(T defaultValue); 9 | 10 | Stream stream(); 11 | 12 | Optional optional(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/Option.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.Optional; 4 | import java.util.function.Function; 5 | import java.util.stream.Stream; 6 | 7 | public interface Option extends Monad { 8 | 9 | static Option of(T t) { 10 | return t == null ? none() : some(t); 11 | } 12 | 13 | static Option some(T t) { 14 | return new Some<>(t); 15 | } 16 | 17 | static Option none() { 18 | return NONE; 19 | } 20 | 21 | static None NONE = new None<>(); 22 | 23 | record Some(T t) implements Option { 24 | 25 | @Override 26 | public Option map(Function mapper) { 27 | return some(mapper.apply(t)); 28 | } 29 | 30 | public Option flatMap(Function> mapper) { 31 | return mapper.apply(t); 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "Some: " + t.toString(); 37 | } 38 | 39 | @Override 40 | public Monad bind(Function> mapper) { 41 | return mapper.apply(t); 42 | } 43 | 44 | @Override 45 | public T getOrDefault(T defaultValue) { 46 | return t; 47 | } 48 | 49 | @Override 50 | public Stream stream() { 51 | return Stream.of(t); 52 | } 53 | 54 | @Override 55 | public Optional optional() { 56 | return Optional.ofNullable(t); 57 | } 58 | } 59 | 60 | record None() implements Option { 61 | 62 | @Override 63 | public Option map(Function mapper) { 64 | return (Option) this; 65 | } 66 | 67 | public Option flatMap(Function> mapper) { 68 | return (Option) this; 69 | } 70 | 71 | @Override 72 | public String toString() { 73 | return "None"; 74 | } 75 | 76 | @Override 77 | public Monad bind(Function> mapper) { 78 | return (Monad) this; 79 | } 80 | 81 | @Override 82 | public T getOrDefault(T defaultValue) { 83 | return defaultValue; 84 | } 85 | 86 | @Override 87 | public Stream stream() { 88 | return Stream.empty(); 89 | } 90 | 91 | @Override 92 | public Optional optional() { 93 | return Optional.empty(); 94 | } 95 | 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/Reader.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.function.Function; 4 | 5 | /** 6 | * Reader monad. 7 | * 8 | * @param context type 9 | * @param value type 10 | */ 11 | @FunctionalInterface 12 | public interface Reader { 13 | 14 | /** 15 | * Reader monad is just function C -> T where C is context, T is value 16 | * 17 | * @param context 18 | * @return value of type T 19 | */ 20 | T run(C context); 21 | 22 | /** 23 | * Creates Reader monad from value of type T 24 | * @param context type 25 | * @param value type 26 | * @param t value 27 | * @return Reader monad 28 | */ 29 | static Reader pure(T t) { 30 | return ctx -> t; 31 | } 32 | 33 | default Reader map(Function mapper) { 34 | return ctx -> { 35 | return mapper.apply(run(ctx)); 36 | }; 37 | } 38 | 39 | default Reader flatMap(Function> mapper) { 40 | return ctx -> { 41 | T t = run(ctx); 42 | Reader r = mapper.apply(t); 43 | return r.run(ctx); 44 | }; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/Result.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.Optional; 4 | import java.util.function.Function; 5 | import java.util.stream.Stream; 6 | 7 | /** 8 | * Result monad 9 | * @param value type 10 | */ 11 | public interface Result extends Monad { 12 | 13 | static Result success(T t) { 14 | return new Success<>(t); 15 | } 16 | 17 | static Result failure(Exception e) { 18 | return new Failure<>(e); 19 | } 20 | 21 | class Success implements Result { 22 | 23 | private final T t; 24 | 25 | private Success(T t) { 26 | this.t = t; 27 | } 28 | 29 | @Override 30 | public Result map(Function mapper) { 31 | return success(mapper.apply(t)); 32 | } 33 | 34 | public Result flatMap(Function> mapper) { 35 | return mapper.apply(t); 36 | } 37 | 38 | @Override 39 | public Monad bind(Function> mapper) { 40 | return mapper.apply(t); 41 | } 42 | 43 | @Override 44 | public T getOrDefault(T defaultValue) { 45 | return t; 46 | } 47 | 48 | @Override 49 | public Stream stream() { 50 | return Stream.of(t); 51 | } 52 | 53 | @Override 54 | public Optional optional() { 55 | return Optional.ofNullable(t); 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return "Success: " + t; 61 | } 62 | 63 | } 64 | 65 | class Failure implements Result { 66 | 67 | private final Exception e; 68 | private Failure(Exception e) { 69 | this.e = e; 70 | } 71 | 72 | @Override 73 | public Result map(Function mapper) { 74 | return (Result) this; 75 | } 76 | 77 | public Result flatMap(Function> mapper) { 78 | return (Result) this; 79 | } 80 | 81 | @Override 82 | public Monad bind(Function> mapper) { 83 | return (Monad) this; 84 | } 85 | 86 | @Override 87 | public T getOrDefault(T defaultValue) { 88 | return defaultValue; 89 | } 90 | 91 | @Override 92 | public Stream stream() { 93 | return Stream.empty(); 94 | } 95 | 96 | @Override 97 | public Optional optional() { 98 | return Optional.empty(); 99 | } 100 | 101 | @Override 102 | public String toString() { 103 | return "Failure: " + e.getMessage(); 104 | } 105 | 106 | 107 | } 108 | 109 | static Function> lift(FPUtils.CheckedFunction mapper) { 110 | return t -> { 111 | try { 112 | R apply = mapper.apply(t); 113 | if (apply == null) { 114 | return failure(new NullPointerException()); 115 | } 116 | return success(apply); 117 | } catch (Exception e) { 118 | return failure(e); 119 | } 120 | }; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/State.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | 4 | import java.util.function.Function; 5 | 6 | /** 7 | * State monad. 8 | * 9 | * @param value type 10 | * @param state type 11 | */ 12 | @FunctionalInterface 13 | public interface State { 14 | 15 | /** 16 | * State monad is just function S -> (T, S) 17 | * @param state 18 | * @return 19 | */ 20 | Tuple apply(S state); 21 | 22 | /** 23 | * Creates state monad from pure value 24 | * @param value type 25 | * @param state type 26 | * @param t value 27 | * @return state monad 28 | */ 29 | static State pure(T t) { 30 | return s -> new Tuple<>(t, s); 31 | } 32 | 33 | default State map(Function mapper) { 34 | return state -> { 35 | Tuple tuple = apply(state); 36 | return new Tuple<>(mapper.apply(tuple.first), tuple.second); 37 | }; 38 | } 39 | 40 | default State flatMap(Function> mapper) { 41 | return state -> { 42 | Tuple tuple = apply(state); 43 | return mapper.apply(tuple.first).apply(tuple.second); 44 | }; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/Try.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.NoSuchElementException; 4 | import java.util.Optional; 5 | import java.util.function.Consumer; 6 | import java.util.function.Function; 7 | import java.util.function.Predicate; 8 | import java.util.function.Supplier; 9 | import java.util.stream.Stream; 10 | 11 | /** 12 | * Interface to handle exceptions in a functional way. 13 | * 14 | *

15 | * Try can be either success with value of type T or failure with an exception. 16 | * In some way Try is similar to {@link Optional} which may have value or not. 17 | * Use {@link #isSuccess()} and {@link #isFailure()} methods to determine flavor of current try. 18 | * 19 | *

20 | * Try has static factory methods Try.of(...) for producing tries from 21 | * {@link CheckedSupplier}, {@link CheckedRunnable} functional interfaces. 22 | * 23 | *

Methods that return Try could be used for method chaining. 24 | *

 25 |  *   Try.of(...)
 26 |  *   .map(...)
 27 |  *   .map(...)
 28 |  *   .filter(...)
 29 |  *   .recover(...)
 30 |  *   .recover(...)
 31 |  *   .onFailure(...)
 32 |  *   .optional()
 33 |  *   .get(...)
 34 |  * 
35 | * 36 | *

37 | * Be aware that these Try methods behave differently depending on success or failure. 38 | * For example {@link #onSuccess(CheckedConsumer)} method does not have any effect 39 | * in case of failure and {@link #onFailure(Consumer)} does not have any effect for success. 40 | * 41 | *

42 | * When Try becomes failure, the only way to get back to success is recover method. 43 | * Try has a set of recover(...) methods to recover failed try to the success. 44 | * You can invoke recover many times implementing different recover strategies. 45 | * If some recover method succeeds, then remaining recover methods 46 | * will not have an action/effect. 47 | * 48 | * @author skopylov@gmail.com 49 | * 50 | * @param type of Try's success result. 51 | */ 52 | 53 | public interface Try extends AutoCloseable { 54 | 55 | 56 | @FunctionalInterface 57 | interface CheckedSupplier {T get() throws Exception;} 58 | 59 | @FunctionalInterface 60 | interface CheckedConsumer {void accept(T t) throws Exception;} 61 | 62 | @FunctionalInterface 63 | interface CheckedFunction {R apply(T t) throws Exception;} 64 | 65 | @FunctionalInterface 66 | interface CheckedRunnable {void run() throws Exception;} 67 | 68 | 69 | //------------------ 70 | // Interface methods 71 | //------------------ 72 | /** 73 | * Executes action on success, has no action for failure. 74 | * @param consumer consumer 75 | * @return this Try or new failure is consumer throws an exception 76 | */ 77 | @SuppressWarnings("unchecked") 78 | default Try onSuccess(CheckedConsumer consumer) { 79 | return map(toFunction(consumer)); 80 | }; 81 | 82 | /** 83 | * Executes action on failure, has no action for success. 84 | * @param consumer consumer 85 | * @return this Try or new failure is consumer throws an exception 86 | */ 87 | default Try onFailure(Consumer consumer) { 88 | return fold(__ -> this, e -> {consumer.accept(e); return this;}); 89 | } 90 | 91 | /** 92 | * Checks if it is success try. 93 | * @return true if it is success. 94 | */ 95 | default boolean isSuccess() { 96 | return fold(__ -> true, __ -> false); 97 | }; 98 | 99 | /** 100 | * Tries recover failed try with given supplier, has no action for success. 101 | * @param supplier supplier to recover 102 | * @return this Try for success, new success or new failure depending on if supplier had thrown exception. 103 | */ 104 | @SuppressWarnings("unchecked") 105 | default Try recover(CheckedSupplier supplier) { 106 | return (Try) fold(__ -> this, e -> catching(toFunction(supplier)).apply(e)); 107 | } 108 | 109 | // default Try recover(Supplier supl) { 110 | // return (Try) fold(__ -> this, e -> success(supl.get())); 111 | // } 112 | // 113 | // default Try recover(Function mapper) { 114 | // return (Try) fold(__ -> this, e -> success(mapper.apply(e))); 115 | // } 116 | // 117 | // default Try flatRecover(Function> mapper) { 118 | // return fold(__ -> this, e -> mapper.apply(e)); 119 | // } 120 | // 121 | // default Try flatRecover(Supplier> supl) { 122 | // return fold(__ -> this, e -> supl.get()); 123 | // } 124 | 125 | /** 126 | * Tries recover failed try with given supplier, has no action for success. 127 | * @param supplier supplier to recover 128 | * @param predicate recover attempt happens if predicate returns true. 129 | * @return this Try for success, new success or new failure depending on if supplier had thrown exception. 130 | */ 131 | default Try recover(CheckedSupplier supplier, Predicate predicate) { 132 | return fold(__ -> this, e -> predicate.test(e) ? recover(supplier) : this); 133 | } 134 | 135 | /** 136 | * Maps Try of type T to type R 137 | * @param new result type 138 | * @param mapper mapper 139 | * @return new Try of R type or failure if mapper throws exception/ 140 | */ 141 | @SuppressWarnings("unchecked") 142 | default Try map(CheckedFunction mapper) { 143 | return (Try) flatMap(catching(mapper)); 144 | } 145 | 146 | /** 147 | * Maps Try of type T to try of type R 148 | * @param new result type 149 | * @param mapper mapper 150 | * @return new Try of R type or failure if mapper throws exception/ 151 | */ 152 | @SuppressWarnings("unchecked") 153 | default Try flatMap(Function> mapper) { 154 | return fold(mapper::apply, __ -> (Try) this); 155 | } 156 | 157 | 158 | /** 159 | * Filters current Try, has no action for failure. 160 | * @param predicate predicate to test 161 | * @return this Try if predicate returns true or new fail with {@link NoSuchElementException} 162 | */ 163 | default Try filter(Predicate predicate) { 164 | return fold(v -> predicate.test(v) ? this : failure(new NoSuchElementException()), __ -> this); 165 | } 166 | 167 | default Stream stream() { 168 | return fold(Stream::of, __ -> Stream.empty()); 169 | } 170 | 171 | default Optional optional() { 172 | return fold(Optional::ofNullable, __ -> Optional.empty()); 173 | } 174 | 175 | @Override 176 | void close(); 177 | 178 | /** 179 | * Gets Try's value or throws exception 180 | * @return value of T in case of success 181 | * @throws RuntimeException in case of failure 182 | */ 183 | T orElseThrow(); 184 | 185 | R fold (Function onSuccess, Function onFailure); 186 | 187 | //----------------------- 188 | //Interface default methods 189 | //----------------------- 190 | 191 | /** 192 | * Checks if it is failure try. 193 | * @return true if failure. 194 | */ 195 | default boolean isFailure() {return !isSuccess();} 196 | 197 | /** 198 | * Synonym for {@link #orElseThrow()} 199 | * @return see {@link #orElseThrow()} 200 | */ 201 | default T get() { 202 | return orElseThrow(); 203 | } 204 | 205 | /** 206 | * Behaves like finally block in Java's try/catch/finally. 207 | * Will executed for both cases - either success or failure. 208 | * @param runnable runnable to execute 209 | * @return this Try or new failure if runnable throws exception. 210 | */ 211 | default Try andFinally(CheckedRunnable runnable) { 212 | try { 213 | runnable.run(); 214 | return this; 215 | } catch (Exception e) { 216 | return failure(e); 217 | } 218 | } 219 | 220 | /** 221 | * Gives access to current Try. 222 | * @param consumer Try's consumer 223 | * @return this Try or failure if consumer throws an exception. 224 | */ 225 | default Try peek(CheckedConsumer> consumer) { 226 | try { 227 | consumer.accept(this); 228 | return this; 229 | } catch (Exception e) { 230 | return failure(e); 231 | } 232 | } 233 | 234 | //---------------------------------- 235 | // Factory methods for producing Try 236 | //---------------------------------- 237 | 238 | /** 239 | * Factory method to produce Try from value of T type. 240 | * @param result type 241 | * @param value success value 242 | * @return Try <T> 243 | */ 244 | static Try success(T value) {return new Success<>(value);} 245 | 246 | /** 247 | * Factory method to produce Try from value of Exception. 248 | * @param result type 249 | * @param exception exception 250 | * @return Try <T> 251 | */ 252 | static Try failure(Exception exception) {return new Failure<>(exception);} 253 | 254 | /** 255 | * Factory method to produce Try from supplier that may throw an exception. 256 | *

257 | * @param Try's result type 258 | * @param supplier that gives the result of T type and may throw an exception. 259 | * @return Try of T type 260 | * @throws NullPointerException if supplier returns null. 261 | */ 262 | @SuppressWarnings("unchecked") 263 | static Try of(CheckedSupplier supplier) { 264 | return (Try) catching((T t) -> supplier.get()).apply(null); 265 | } 266 | 267 | /** 268 | * Factory method to produce Try from runnable that may throw an exception. 269 | * @param runnable exceptional runnable 270 | * @return Try of {@link Void} type 271 | */ 272 | static Try of(CheckedRunnable runnable) { 273 | return of(() -> { 274 | runnable.run(); 275 | return null; 276 | }); 277 | } 278 | 279 | /** 280 | * Higher order function to transform partial {@code T->R} function 281 | * to the total {@code T->Try} function. 282 | *
283 | * Simplifies using Try with Java's streams and optionals. 284 | * Sample usage: 285 | *

{@code 
286 |      *  Stream.of("1", "2")           //Stream
287 |      *  .map(Try.lift(Integer::valueOf) //Stream>
288 |      *  .flatMap(Try::stream)         //Stream
289 |      *  .toList()                     //List
290 |      * }
291 | * 292 | * @param function parameter type 293 | * @param function result type 294 | * @param func partial function {@code T->R} 295 | * @return total function {@code T->Try} 296 | */ 297 | static Function> catching(CheckedFunction func) { 298 | return (T t) -> { 299 | try { 300 | return success(func.apply(t)); 301 | } catch (Exception e) { 302 | return failure(e); 303 | } 304 | }; 305 | } 306 | 307 | static Function> consumeCatching(CheckedConsumer cons) { 308 | return (T t) -> { 309 | try { 310 | cons.accept(t); 311 | return success(t); 312 | } catch (Exception e) { 313 | return failure(e); 314 | } 315 | }; 316 | } 317 | 318 | static Function> getCatching(CheckedSupplier supl) { 319 | return (T t) -> { 320 | try { 321 | return success(supl.get()); 322 | } catch (Exception e) { 323 | return failure(e); 324 | } 325 | }; 326 | } 327 | 328 | static Function> runCatching(CheckedRunnable run) { 329 | return (T t) -> { 330 | try { 331 | run.run(); 332 | return success(null); 333 | } catch (Exception e) { 334 | return failure(e); 335 | } 336 | }; 337 | } 338 | 339 | 340 | 341 | /** 342 | * Try's success projection. 343 | * @author skopylov@goole.com 344 | * 345 | * @param result type 346 | */ 347 | class Success implements Try { 348 | 349 | private final T value; 350 | 351 | Success(T val) { 352 | value = val; 353 | } 354 | 355 | @Override 356 | public T orElseThrow() {return value;} 357 | 358 | @Override 359 | public void close() { 360 | if (value instanceof AutoCloseable) { 361 | try { 362 | ((AutoCloseable) value).close(); 363 | } catch (Exception e) { 364 | } 365 | } else { 366 | throw new IllegalStateException(); 367 | } 368 | } 369 | 370 | @Override 371 | public R fold(Function onSuccess, Function onFailure) { 372 | return onSuccess.apply(value); 373 | } 374 | } 375 | 376 | /** 377 | * Try's failure projection. 378 | * @author skopylov@gmail.com 379 | * 380 | * @param result type 381 | */ 382 | class Failure implements Try { 383 | 384 | private final Exception exception; 385 | 386 | Failure(Exception e) { 387 | exception = e; 388 | } 389 | 390 | @Override 391 | public T orElseThrow() { 392 | if (exception instanceof RuntimeException) { 393 | throw (RuntimeException) exception; 394 | } else { 395 | throw new RuntimeException(exception); 396 | } 397 | } 398 | 399 | @Override 400 | public void close() {/*nothing to close*/} 401 | 402 | @Override 403 | public R fold(Function onSuccess, Function onFailure) { 404 | return onFailure.apply(exception); 405 | } 406 | } 407 | 408 | static CheckedFunction toFunction(CheckedConsumer cons) { 409 | return (T t) -> { 410 | cons.accept(t); 411 | return t; 412 | }; 413 | } 414 | 415 | /** 416 | * Converts supplier to function. 417 | */ 418 | static CheckedFunction toFunction(CheckedSupplier supplier) { 419 | return (Object o) -> supplier.get(); 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/Tuple.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | 4 | /** 5 | * Container for pair (first, second) objects. 6 | * 7 | * @author skopylov@gmail.com 8 | * 9 | * @param type of first object 10 | * @param type of second object 11 | */ 12 | public class Tuple { 13 | 14 | public final F first; 15 | public final S second; 16 | 17 | /** 18 | * Constructor. 19 | * @param first first object 20 | * @param second second object 21 | */ 22 | public Tuple(F first, S second) { 23 | this.first = first; 24 | this.second = second; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | String f = first == null ? "null" : first.toString(); 30 | String s = second == null ? "null" : second.toString(); 31 | return "First=" + f + ", Second=" + s; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/Tuple3.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | public class Tuple3 extends Tuple { 4 | 5 | public final T third; 6 | 7 | public Tuple3(F f, S s, T t) { 8 | super(f ,s); 9 | third = t; 10 | } 11 | 12 | @Override 13 | public String toString() { 14 | String s = third == null ? "null" : third.toString(); 15 | return super.toString() + ", Third=" + s; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/Validation.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.function.Function; 7 | import java.util.function.Predicate; 8 | 9 | /** 10 | * Simple functional validator collecting all validation errors. 11 | * 12 | * @author skopylov@gmail.com 13 | * 14 | * @param type to validate 15 | * @param type of validation error 16 | * 17 | * @deprecated Use {@link com.github.skopylov58.functional.Validator} instead. 18 | */ 19 | @Deprecated 20 | public class Validation { 21 | 22 | sealed interface Validator permits PredicateValidator, NestedValidator {} 23 | 24 | private record PredicateValidator( 25 | Predicate predicate, 26 | Function errorMapper) implements Validator {} 27 | 28 | record NestedValidator( 29 | Function mapper, 30 | Validation validation) implements Validator {} 31 | 32 | 33 | private final List validators; 34 | 35 | /** 36 | * Private constructor 37 | * @param errorPredicates validation predicates 38 | */ 39 | private Validation(List validators) { 40 | this.validators = validators; 41 | } 42 | 43 | /** 44 | * Validates data. 45 | * @param t data to validate, null data will not be validated. 46 | * @return list of validation errors, empty list if there are not any errors. 47 | */ 48 | @SuppressWarnings({ "preview", "unchecked" }) 49 | public List validate(T t) { 50 | if (t == null) { 51 | return Collections.emptyList(); 52 | } 53 | List errors = new ArrayList<>(); 54 | for (Validator validator : validators) { 55 | 56 | if (validator instanceof PredicateValidator pv) { 57 | boolean err = pv.predicate.test(t); 58 | if (err) { 59 | errors.add((E) pv.errorMapper.apply(t)); 60 | } 61 | } else if (validator instanceof NestedValidator nv) { 62 | errors.addAll(nv.validation.validate(nv.mapper.apply(t))); 63 | } else { 64 | throw new IllegalStateException(); 65 | } 66 | } 67 | return errors; 68 | } 69 | 70 | 71 | 72 | /** 73 | * Validates data 74 | * @param t data to validate 75 | * @return Either 76 | */ 77 | public Either, T> validateToEither(T t) { 78 | List list = validate(t); 79 | return list.isEmpty() ? Either.right(t) : Either.left(list); 80 | } 81 | 82 | /** 83 | * Creates validation builder. 84 | * @param type to validate 85 | * @param validation error type 86 | * @return validation builder 87 | */ 88 | public static Builder builder() { 89 | return new Builder<>(); 90 | } 91 | 92 | /** 93 | * Validator builder. 94 | * 95 | * @param type to validate 96 | * @param type of validation error 97 | */ 98 | public static class Builder { 99 | 100 | private final List validators = new ArrayList<>(); 101 | 102 | private Builder() {} 103 | 104 | /** 105 | * Adds new validation to the validator. 106 | * @param predicate validity predicate, should return true if data is invalid 107 | * @param error error to collect if predicate returns true 108 | * @return this builder 109 | */ 110 | public Builder addValidation(Predicate predicate, E error) { 111 | validators.add(new PredicateValidator<>(predicate, t -> error)); 112 | return this; 113 | } 114 | 115 | /** 116 | * Adds new validation to the validator. 117 | * @param predicate validity predicate, should return true if data is invalid 118 | * @param errorMapper maps invalid data to validation error. 119 | * @return this builder 120 | */ 121 | public Builder addValidation(Predicate predicate, Function errorMapper) { 122 | validators.add(new PredicateValidator<>(predicate, errorMapper)); 123 | return this; 124 | } 125 | 126 | /** 127 | * Add new validation of the R type field. 128 | * @param mapper result type. 129 | * @param mapper maps T to R 130 | * @param validation validation for R 131 | * @return 132 | */ 133 | public Builder addValidation(Function mapper, Validation validation) { 134 | validators.add(new NestedValidator(mapper, validation)); 135 | return this; 136 | } 137 | 138 | /** 139 | * Builds validator. 140 | * @return Validation 141 | */ 142 | public Validation build() { 143 | return new Validation<>(validators); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/Validator.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import java.util.function.Function; 7 | import java.util.function.Predicate; 8 | import java.util.function.Supplier; 9 | 10 | /** 11 | * Simple validator that can collect validation errors. 12 | * 13 | * @param type to validate 14 | * @param validation error type 15 | */ 16 | public interface Validator { 17 | 18 | /** 19 | * Validates validator's object of type T. 20 | * @param predicate predicate to validate validator's object 21 | * @param errorSupplier error supplier if predicate returns false. 22 | * @return this validator 23 | */ 24 | Validator validate(Predicate predicate, Supplier errorSupplier); 25 | 26 | /** 27 | * Validates some internal state (class members, etc.) of validator's object. 28 | * @param 29 | * @param mapper 30 | * @param predicate 31 | * @param errorSupplier 32 | * @return 33 | */ 34 | Validator validate(Function mapper, Predicate predicate, Supplier errorSupplier); 35 | Validator validate(Function mapper, Predicate predicate, Function errorMapper); 36 | Validator validate(Function> errorChecker); 37 | 38 | Validator notNull(Function mapper, Supplier errorSupplier); 39 | 40 | /** 41 | * Checks if object is valid 42 | * @return true if no validation errors were found 43 | */ 44 | boolean hasErrors(); 45 | 46 | /** 47 | * Returns validation errors. 48 | * @return empty list if no errors were found. 49 | */ 50 | List getErrors(); 51 | 52 | default Validator validate(Predicate predicate, E error) { 53 | return validate(predicate, () -> error); 54 | } 55 | 56 | default Validator validate(Function mapper, Predicate predicate, E error) { 57 | return validate(mapper, predicate, () -> error); 58 | } 59 | 60 | default Validator notNull(Function member, E error) { 61 | return notNull(member, () -> error); 62 | } 63 | 64 | /** 65 | * Factory method to create validator of type T and errors type E 66 | * @param type to validate 67 | * @param validation error type 68 | * @param t object to validate 69 | * @return validator 70 | */ 71 | static Validator of(T t) { 72 | return new ValidatorImpl<>(t); 73 | } 74 | 75 | class ValidatorImpl implements Validator { 76 | private final T value; 77 | private final List errors = new LinkedList<>(); 78 | 79 | private ValidatorImpl(T t) { 80 | value = t; 81 | } 82 | 83 | public Validator validate(Predicate predicate, Supplier errorSupplier) { 84 | return validate(Function.identity(), predicate, errorSupplier); 85 | } 86 | 87 | @Override 88 | public Validator notNull(Function mapper, Supplier errorSupplier) { 89 | if (mapper.apply(value) == null) { 90 | errors.add(errorSupplier.get()); 91 | } 92 | return this; 93 | } 94 | 95 | @Override 96 | public Validator validate(Function mapper, Predicate predicate, Supplier errorSupplier) { 97 | return validate(mapper, predicate, x -> errorSupplier.get()); 98 | } 99 | 100 | @Override 101 | public Validator validate(Function mapper, Predicate predicate, Function errorMapper) { 102 | R apply = mapper.apply(value); 103 | if (apply == null || !predicate.test(apply)) { 104 | errors.add(errorMapper.apply(apply)); 105 | } 106 | return this; 107 | } 108 | 109 | @Override 110 | public boolean hasErrors() { 111 | return !errors.isEmpty(); 112 | } 113 | 114 | @Override 115 | public List getErrors() { 116 | return errors; 117 | } 118 | 119 | @Override 120 | public Validator validate(Function> errorChecker) { 121 | errorChecker.apply(value).ifPresent(errors::add); 122 | return this; 123 | } 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/ZIO.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.function.Function; 4 | 5 | 6 | /** 7 | * Minimalistic ZIO effects 8 | * 9 | * @param input parameter class 10 | * @param error class 11 | * @param
result class 12 | */ 13 | public record ZIO(Function> run) { 14 | 15 | public static ZIO succeed(A a) { 16 | return new ZIO<>(__ -> Either.right(a)); 17 | } 18 | 19 | public static ZIO fail(E e) { 20 | return new ZIO<>(__ -> Either.left(e)); 21 | } 22 | 23 | public ZIO map(Function mapper) { 24 | return new ZIO<>( r -> { 25 | return run.apply(r).map(mapper); 26 | }); 27 | } 28 | 29 | public ZIO flatMap(Function> mapper) { 30 | return new ZIO<>(r -> run.apply(r).fold( 31 | Either::left, 32 | right -> mapper.apply(right).run().apply(r))); 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/monad/pictures/Maybe.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.monad.pictures; 2 | 3 | import java.util.function.Function; 4 | 5 | sealed interface Maybe { 6 | 7 | default Maybe map(Function mapper) { 8 | if (this instanceof Just just) { 9 | return Maybe.just(mapper.apply(just.t)); 10 | } else { 11 | return Maybe.nothing(); 12 | } 13 | } 14 | 15 | default Maybe flatMap(Function> mapper) { 16 | if (this instanceof Just just) { 17 | return mapper.apply(just.t); 18 | } else { 19 | return Maybe.nothing(); 20 | } 21 | } 22 | 23 | static Maybe just(T t) { 24 | return new Just<>(t); 25 | } 26 | 27 | @SuppressWarnings("unchecked") 28 | static Maybe nothing() { 29 | return (Maybe) Nothing.INSTANCE; 30 | } 31 | 32 | record Just(T t) implements Maybe { 33 | } 34 | 35 | final class Nothing implements Maybe { 36 | static final Nothing INSTANCE = new Nothing<>(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/monad/pictures/MonadInPictures.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.monad.pictures; 2 | 3 | import java.util.Optional; 4 | import java.util.function.BiFunction; 5 | import java.util.function.Function; 6 | import java.util.stream.Stream; 7 | 8 | class MonadInPictures { 9 | 10 | interface Functor { 11 | Functor map(Function mapper); 12 | } 13 | 14 | interface Maybe { 15 | Maybe map(Function mapper); 16 | } 17 | 18 | record Some(T t) implements Maybe { 19 | @Override 20 | public Maybe map(Function mapper) { 21 | return new Some<>(mapper.apply(t)); 22 | } 23 | } 24 | 25 | record None() implements Maybe { 26 | @Override 27 | public Maybe map(Function mapper) { 28 | return new None<>(); 29 | } 30 | 31 | } 32 | 33 | public static void main(String[] args) { 34 | 35 | Maybe maybe = new Some<>(6); 36 | maybe.map(i -> i + 3); 37 | System.out.println(maybe); 38 | 39 | } 40 | 41 | Optional applicative(Optional> func, Optional value) { 42 | return func.flatMap(f -> value.flatMap(v -> Optional.ofNullable(f.apply(v)))); 43 | } 44 | 45 | Optional lift_a2(Optional optA, Optional optB, BiFunction bifunc) { 46 | return optA.flatMap(a -> optB.flatMap(b -> Optional.of(bifunc.apply(a, b)))); 47 | } 48 | 49 | 50 | void foo() { 51 | Function mult2 = x -> x * 2; 52 | Function add3 = x -> x + 3; 53 | 54 | var intStream = Stream.of(1,2,3); 55 | 56 | Stream.of(mult2, add3).flatMap(f -> Stream.of(1,2,3).map(f)).toList(); 57 | } 58 | 59 | 60 | Optional half(Integer value) { 61 | return value % 2 == 0 ? Optional.of(value/2) : Optional.empty(); 62 | } 63 | 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/main/java/com/github/skopylov58/functional/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package provides interfaces for functional exception handling {@code Try}, 3 | * {@code Either}, {@code Validation} and some useful high order functions. 4 | */ 5 | package com.github.skopylov58.functional; -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/ChainOfResponsibilityTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.Arrays; 4 | import java.util.function.Consumer; 5 | import java.util.function.Function; 6 | import org.junit.Test; 7 | 8 | public class ChainOfResponsibilityTest { 9 | 10 | 11 | 12 | @SafeVarargs 13 | public static Consumer chain(Consumer head, Consumer... tail) { 14 | return Arrays.stream(tail).reduce(head, (a, b) -> a.andThen(b)); 15 | }; 16 | 17 | @SafeVarargs 18 | public static Function chain(Function head, Function... tail) { 19 | return Arrays.stream(tail).reduce(head, (a, b) -> a.andThen(b)); 20 | }; 21 | 22 | @Test 23 | public void testName() throws Exception { 24 | 25 | Consumer chain = chain( 26 | (Integer i) -> System.out.println(i), 27 | (Integer i) -> System.out.println(i + 1), 28 | (Integer i) -> System.out.println(i + 2) 29 | ); 30 | 31 | chain.accept(1); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/EitherTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import static com.github.skopylov58.functional.Either.catching; 4 | 5 | import java.net.Socket; 6 | import java.net.URL; 7 | 8 | import org.junit.Test; 9 | 10 | public class EitherTest { 11 | 12 | @Test 13 | public void testEitherWithException() throws Exception { 14 | var either = Either.catching(() -> new URL("foo")); 15 | either.map(u -> u.getFile()) 16 | .filter(n -> !n.isEmpty(), f -> new Exception()); 17 | } 18 | 19 | @Test 20 | public void testSocket() throws Exception { 21 | 22 | var socket = catching(() -> new Socket("foo", 1234)); 23 | try (var c = socket.asCloseable()) { 24 | socket.flatMap(catching(Socket::getOutputStream)) 25 | .flatMap(catching(o -> { 26 | o.write(new byte[] {1,2,3}); 27 | })); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/FPDesign.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.sql.SQLException; 4 | import java.util.Optional; 5 | import java.util.function.Function; 6 | 7 | public class FPDesign { 8 | 9 | class User {} 10 | class Connection {} 11 | 12 | 13 | class App { 14 | Function> getUser; 15 | 16 | App(Function> getUserFunc) { 17 | getUser = getUserFunc; 18 | } 19 | 20 | void doSomeBusinessStuff() { 21 | //... 22 | long userid = 1000; 23 | Optional user = getUser.apply(userid); 24 | //... 25 | } 26 | } 27 | 28 | 29 | class Main { 30 | 31 | void main_( ) { 32 | Connection con = new Connection(); 33 | 34 | Function> getUser = id -> { 35 | try { 36 | User user = getUserFromDB(id, con); 37 | return Optional.of(user); 38 | } catch (SQLException e) { 39 | return Optional.empty(); 40 | } 41 | }; 42 | 43 | App app = new App(getUser); 44 | app.doSomeBusinessStuff(); 45 | } 46 | } 47 | 48 | User getUserFromDB(long id, Connection con) throws SQLException { 49 | return new User(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/FPUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import static com.github.skopylov58.functional.FPUtils.throwingMapper; 4 | import static com.github.skopylov58.functional.FPUtils.toEither; 5 | import static com.github.skopylov58.functional.FPUtils.toFuture; 6 | import static com.github.skopylov58.functional.FPUtils.toOptional; 7 | import static com.github.skopylov58.functional.FPUtils.toResult; 8 | import static com.github.skopylov58.functional.FPUtils.toResultJava8; 9 | import static com.github.skopylov58.functional.FPUtils.memoize; 10 | import static com.github.skopylov58.functional.FPUtils.ctou; 11 | import static java.util.function.Predicate.not; 12 | import static org.hamcrest.CoreMatchers.is; 13 | import static org.junit.Assert.*; 14 | 15 | import java.net.MalformedURLException; 16 | import java.net.URL; 17 | import java.text.NumberFormat; 18 | import java.util.List; 19 | import java.util.Optional; 20 | import java.util.concurrent.CompletableFuture; 21 | import java.util.concurrent.ConcurrentHashMap; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | import java.util.function.Consumer; 24 | import java.util.function.Function; 25 | import java.util.function.Predicate; 26 | import java.util.function.UnaryOperator; 27 | import java.util.stream.Stream; 28 | 29 | import org.junit.Test; 30 | 31 | import com.github.skopylov58.functional.FPUtils.Result; 32 | 33 | public class FPUtilsTest { 34 | 35 | final List URLS = List.of("www.google.com", "foo.bar", "http://a.b.c"); 36 | final String[] NUMS = new String[] {"foo", "bar", "3", "4.9"}; 37 | 38 | @Test 39 | public void testWithOptional() throws Exception { 40 | List list = URLS.stream().map(toOptional(URL::new)).flatMap(Optional::stream).toList(); 41 | check(list); 42 | } 43 | 44 | void check(List list) throws MalformedURLException { 45 | System.out.println(list); 46 | assertTrue(list.size() == 1); 47 | assertTrue(list.get(0).equals(new URL("http://a.b.c"))); 48 | } 49 | 50 | @Test 51 | public void testWithEither() throws Exception { 52 | List res = URLS.stream().map(toEither(URL::new)) 53 | .peek(either -> either.accept(System.out::println, r -> { 54 | })).flatMap(Either::stream).toList(); 55 | check(res); 56 | } 57 | 58 | @Test 59 | public void testWithResultJava8() throws Exception { 60 | List res = URLS.stream().map(toResultJava8(URL::new)).peek(r -> { 61 | if (r.failed()) 62 | System.out.println(r.cause()); 63 | }).filter(Predicate.not(FPUtils.ResultJava8::failed)).map(r -> r.result()).toList(); 64 | check(res); 65 | } 66 | 67 | @Test 68 | public void testWithResultRecord() throws Exception { 69 | var res = URLS.stream().map(toResult(URL::new)).filter(rec -> !rec.isFailure()) 70 | .map(Result::result).toList(); 71 | 72 | check(res); 73 | } 74 | 75 | @Test 76 | public void intList() { 77 | var list = Stream.of(NUMS).map(toOptional(Integer::parseInt)) // Runtime NumberFormatException 78 | // may happen here 79 | .flatMap(Optional::stream).toList(); 80 | } 81 | 82 | @Test 83 | public void intListWithResult() throws NumberFormatException { 84 | var list = Stream.of(NUMS).map(toResult(Integer::parseInt)) // Runtime NumberFormatException may 85 | // happen here 86 | .peek(this::handleErr).flatMap(Result::stream).toList(); 87 | } 88 | 89 | @Test 90 | public void intListWithNumberFormat() { 91 | NumberFormat format = NumberFormat.getInstance(); 92 | var list = Stream.of(NUMS).map(toOptional(format::parse)) // Checked ParseException may happen 93 | // here 94 | .flatMap(Optional::stream).map(Number::intValue).toList(); 95 | System.out.println(list); 96 | } 97 | 98 | void handleErr(Result r) { 99 | if (r.isFailure()) { 100 | System.out.println(r.exception()); 101 | } 102 | } 103 | 104 | List intListWithResult(String[] numbers) { 105 | NumberFormat format = NumberFormat.getInstance(); 106 | return Stream.of(numbers) // Stream 107 | .map(toResult(format::parse)) // Stream>, ParseException may happen 108 | .peek(this::handleErr) // Stream> 109 | .flatMap(Result::stream) // Stream 110 | .toList(); // List 111 | } 112 | 113 | List intListWithFuture(String[] numbers) { 114 | NumberFormat format = NumberFormat.getInstance(); 115 | return Stream.of(numbers) // Stream 116 | .map(toFuture(format::parse)) // Stream>, ParseException may 117 | // happen 118 | .filter(not(CompletableFuture::isCompletedExceptionally)).map(CompletableFuture::join) 119 | .toList(); // List 120 | } 121 | 122 | @Test 123 | public void testCurrying() throws Exception { 124 | Function handled = FPUtils.catchingMapper(Integer::parseInt, (t, e) -> -1); 125 | } 126 | 127 | @Test 128 | public void testIgnoreErrors() { 129 | var list = Stream.of("1", "foo", "2", "bar").map(toOptional(Integer::valueOf)) 130 | .flatMap(Optional::stream).toList(); 131 | 132 | assertThat(List.of(1, 2), is(list)); 133 | } 134 | 135 | @Test 136 | public void testIgnoreErrorsAndLog() { 137 | var list = Stream.of("1", "foo", "2", "bar") 138 | .map(toOptional(Integer::valueOf, 139 | (i, e) -> System.err.println(i + " causes error " + e.getMessage()))) 140 | .flatMap(Optional::stream).toList(); 141 | 142 | assertThat(List.of(1, 2), is(list)); 143 | } 144 | 145 | @Test 146 | public void testProvideDefaultsForErrors() { 147 | var list = Stream.of("1", "foo", "2", "bar").map(toOptional(Integer::valueOf)) 148 | .map(o -> o.orElse(-1)).toList(); 149 | 150 | assertThat(List.of(1, -1, 2, -1), is(list)); 151 | } 152 | 153 | @Test 154 | public void testHandleErrors() { 155 | var list = Stream.of("1", "foo", "2", "foo-bar") 156 | .map(FPUtils.catchingMapper(Integer::valueOf, (s, e) -> s.length())).toList(); 157 | 158 | assertThat(List.of(1, 3, 2, 7), is(list)); 159 | } 160 | 161 | @Test(expected = NumberFormatException.class) 162 | public void testStopOnErrors() { 163 | Stream.of("1", "foo", "2", "bar").map(throwingMapper(Integer::valueOf)).toList(); 164 | 165 | fail(); 166 | } 167 | 168 | @Test 169 | public void testMemoize() throws Exception { 170 | 171 | Function hello = s -> { 172 | try { 173 | Thread.sleep(1000); 174 | } catch (InterruptedException e) { 175 | e.printStackTrace(); 176 | } 177 | return "Hello " + s; 178 | }; 179 | 180 | var cache = new ConcurrentHashMap(); 181 | Function memoizedHello = memoize(hello, cache); 182 | 183 | System.out.println("Start"); 184 | String apply = memoizedHello.apply("Sergey"); 185 | System.out.println(apply); 186 | 187 | 188 | apply = memoizedHello.apply("Sergey"); 189 | System.out.println(apply); 190 | } 191 | 192 | 193 | @Test 194 | public void testDecorator() throws Exception { 195 | 196 | Function hello = s -> "Hello " + s; 197 | 198 | UnaryOperator before = ctou(s -> { 199 | System.out.println("before " + s); 200 | }); 201 | UnaryOperator after = ctou(s -> { 202 | System.out.println("after " + s); 203 | }); 204 | 205 | Function decoratedHello = hello.compose(before).andThen(after); 206 | 207 | String h = decoratedHello.apply("Sergey"); 208 | System.out.println(h); 209 | } 210 | 211 | 212 | @Test 213 | public void testRunOnce() throws Exception { 214 | 215 | AtomicInteger i = new AtomicInteger(0); 216 | 217 | Runnable r = () -> i.incrementAndGet(); 218 | 219 | Runnable once = FPUtils.once(r); 220 | 221 | once.run(); 222 | once.run(); 223 | once.run(); 224 | 225 | assertEquals(1, i.get()); 226 | } 227 | 228 | 229 | } 230 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/FilterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import java.util.NoSuchElementException; 7 | 8 | import org.junit.Test; 9 | 10 | public class FilterTest { 11 | @Test 12 | public void testFilter() throws Exception { 13 | Try t = Try.success(3); 14 | Try filtered = t.filter(i -> i%2 == 0); 15 | assertTrue(filtered.isFailure()); 16 | filtered.onFailure(e -> assertEquals(NoSuchElementException.class, e.getClass())); 17 | 18 | t = Try.success(4); 19 | filtered = t.filter(i -> i%2 == 0); 20 | assertTrue(filtered.isSuccess()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/IO2Test.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import java.util.Optional; 7 | import java.util.Properties; 8 | import org.junit.Test; 9 | 10 | public class IO2Test { 11 | 12 | @Test 13 | public void testCombineMonads() { 14 | 15 | IO2 ioi = ctc -> Option.some(3); 16 | IO2 ios = ctc -> Result.success("three"); 17 | 18 | IO2 combined = ioi.flatMap(i -> ios.map(s -> s + " " + i)); 19 | 20 | Monad result = combined.run(new Properties()); 21 | System.out.println(result); 22 | 23 | boolean isSuccess = result instanceof Result.Success; 24 | assertTrue(isSuccess); 25 | 26 | String str = result.getOrDefault(null); 27 | assertEquals("three 3", str); 28 | 29 | } 30 | 31 | @Test 32 | public void testAccessContext() { 33 | 34 | IO2 ioi = ctx -> Option.some(3); 35 | 36 | IO2 strio = ioi.flatMap( i -> { 37 | return ctx -> { 38 | String prop = ctx.getProperty(i.toString()); 39 | return Option.some(prop); 40 | }; 41 | }); 42 | 43 | Properties p = new Properties(); 44 | p.put("3", "three"); 45 | 46 | Monad resMonad = strio.run(p); 47 | 48 | System.out.println(resMonad); 49 | 50 | 51 | } 52 | 53 | @Test 54 | public void testVoid() { 55 | var x = IO2.of(5); 56 | x = x.map(i -> i + 2); 57 | var res = x.run(); 58 | 59 | assertEquals(Integer.valueOf(7) , res.getOrDefault(0)); 60 | System.out.println("res = " + res); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/IOTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import static org.junit.Assert.*; 4 | import java.io.BufferedReader; 5 | import java.io.IOException; 6 | import java.io.InputStreamReader; 7 | import org.junit.Test; 8 | 9 | public class IOTest { 10 | 11 | // @Test 12 | // public void testIO() throws Exception { 13 | // IO readLine = IO.of(() -> readLine()); 14 | // IO greet = readLine.flatMap(s -> IO.consume(s, this::sayHello)); 15 | // greet.run(); 16 | // } 17 | 18 | 19 | @Test 20 | public void testIO2() throws Exception { 21 | 22 | var io = getLine() 23 | .flatMap(s -> readFile(s)) 24 | .flatMap(s -> printLine(s)); 25 | 26 | io.run(); 27 | 28 | } 29 | 30 | IO getLine() { 31 | return () -> "foo"; 32 | } 33 | 34 | IO readFile(String fileName) { 35 | return () -> "bar"; 36 | } 37 | 38 | IO printLine(String text) { 39 | return () -> { 40 | System.out.println(text); 41 | return null; 42 | }; 43 | } 44 | 45 | void sayHello(String s) { 46 | System.out.println("Hello " + s); 47 | } 48 | 49 | String readLine0() { 50 | System.out.println("Enter line:"); 51 | BufferedReader r = new BufferedReader(new InputStreamReader(System.in)); 52 | try { 53 | return r.readLine(); 54 | } catch (IOException e) { 55 | throw new RuntimeException(e); 56 | } finally { 57 | try { 58 | r.close(); 59 | } catch (IOException e) {// ignore} 60 | } 61 | } 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/MemoTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.time.Duration; 4 | import java.util.function.Function; 5 | 6 | import org.junit.Test; 7 | 8 | public class MemoTest { 9 | 10 | 11 | @Test 12 | public void test() throws Exception { 13 | 14 | Function add3 = x -> { 15 | try { 16 | Thread.sleep(1000); 17 | } catch (InterruptedException e) { 18 | } 19 | return x + 3; 20 | }; 21 | 22 | 23 | Function memoized = Memo.memoize(add3); 24 | 25 | Duration duration = FPUtils.measure(() -> { 26 | memoized.apply(2); 27 | }); 28 | System.out.println("First call: " + duration); 29 | 30 | duration = FPUtils.measure(() -> { 31 | memoized.apply(2); 32 | }); 33 | System.out.println("Second call: " + duration); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/OptionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import org.junit.Test; 7 | 8 | public class OptionTest { 9 | 10 | @Test 11 | public void testSomeAndNone() throws Exception { 12 | 13 | Option someInt = Option.some(1); 14 | System.out.println(someInt); 15 | 16 | Option noneInt = Option.none(); 17 | System.out.println(noneInt); 18 | 19 | Monad lifted = Monad.liftM2(someInt, noneInt, (x, y) -> x + y); 20 | assertTrue(lifted instanceof Option.None); 21 | System.out.println(lifted); 22 | } 23 | 24 | @Test 25 | public void testSomeAndSome() throws Exception { 26 | 27 | Option someInt1 = Option.some(1); 28 | System.out.println(someInt1); 29 | 30 | Option someInt2 = Option.some(2); 31 | System.out.println(someInt2); 32 | 33 | Monad lifted = Monad.liftM2(someInt1, someInt2, (x, y) -> x + y); 34 | assertTrue(lifted instanceof Option.Some); 35 | System.out.println(lifted); 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/OptionalTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.Optional; 7 | import java.util.function.Function; 8 | 9 | import org.junit.Test; 10 | 11 | public class OptionalTest { 12 | 13 | @Test 14 | public void test() throws Exception { 15 | 16 | Optional opt = Try.of(() -> 1).optional(); 17 | assertTrue(opt.isPresent()); 18 | assertEquals(1, opt.get().intValue()); 19 | 20 | Try t = Try.of(() -> {throw new NullPointerException();}); 21 | assertTrue(t.isFailure()); 22 | opt = t.optional(); 23 | assertFalse(opt.isPresent()); 24 | assertEquals(Optional.empty(), opt); 25 | } 26 | 27 | @Test 28 | public void testTryOfOptional() throws Exception { 29 | 30 | Try> tried = Try.of(() -> Optional.of(1)); 31 | 32 | Optional opt = tried 33 | .optional() //Optional> 34 | .flatMap(Function.identity()); 35 | 36 | assertTrue(opt.isPresent()); 37 | assertEquals(1, opt.get().intValue()); 38 | 39 | } 40 | 41 | @Test 42 | public void testMoney() throws Exception { 43 | double three = 3.0; 44 | double one = 1.0; 45 | 46 | double third = one /three; 47 | 48 | System.out.println(Double.compare(one, three * third)); 49 | 50 | 51 | System.out.println(third); 52 | } 53 | 54 | @Test 55 | public void sumDoubles() throws Exception { 56 | double third = 1.0d/3.0d; 57 | double sum = 0; 58 | 59 | int loops = 3_000_000; 60 | double expected = loops/3; 61 | 62 | for (int i = 0; i < loops; i++) { 63 | sum += third; 64 | } 65 | System.out.println("expected: " + expected); 66 | System.out.println("res: " + sum); 67 | System.out.println("compare: " + Double.compare(expected, sum)); 68 | System.out.println("diff: " + (expected - sum)); 69 | } 70 | 71 | @Test 72 | public void multiplyDoubles() throws Exception { 73 | double third = 1.0d/3.0d; 74 | double sum = 0; 75 | 76 | int loops = 3_000_000; 77 | double expected = loops/3; 78 | 79 | sum = third * loops; 80 | // for (int i = 0; i < loops; i++) { 81 | // sum += third; 82 | // } 83 | System.out.println("expected: " + expected); 84 | System.out.println("res: " + sum); 85 | System.out.println("compare: " + Double.compare(expected, sum)); 86 | System.out.println("diff: " + (expected - sum)); 87 | } 88 | 89 | 90 | 91 | @Test 92 | public void bar() throws Exception { 93 | 94 | double third = 1.0d/3.0d; 95 | System.out.println("Double.toString: " + Double.toString(third)); 96 | BigDecimal tenth = BigDecimal.valueOf(third); 97 | BigDecimal res = BigDecimal.ZERO; 98 | 99 | for (int i = 0; i< 3; i++) { 100 | res = res.add(tenth); 101 | } 102 | 103 | System.out.println(res); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/ReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import static org.junit.Assert.*; 4 | import java.util.Properties; 5 | import org.junit.Test; 6 | 7 | public class ReaderTest { 8 | 9 | 10 | @Test 11 | public void testPropertiesReader() throws Exception { 12 | 13 | Properties props = new Properties(); 14 | props.put("foo", "bar"); 15 | 16 | Reader r = Reader.pure("foo"); 17 | 18 | Integer length = r.map(s -> "Hello " + s) 19 | .flatMap(s -> (p -> strLength(s, p))) 20 | .run(props); 21 | 22 | System.out.println("Length: " + length); 23 | assertTrue(9==length); 24 | 25 | } 26 | 27 | int strLength(String s, Properties p) { 28 | System.out.println("Accessing properties " + p); 29 | return s.length(); 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/ResourceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import java.net.Socket; 7 | import java.net.UnknownHostException; 8 | 9 | import org.junit.Test; 10 | 11 | public class ResourceTest { 12 | 13 | @Test 14 | public void testTryWithResource() throws Exception { 15 | CloseableMock closeable = new CloseableMock("res4"); 16 | try (var tr = Try.success(closeable)) { 17 | tr.map(c -> 1); 18 | } //should close this try 19 | assertTrue(closeable.isClosed()); 20 | } 21 | 22 | @Test 23 | public void testNotCloseable() throws Exception { 24 | try (var tr = Try.success(1)) { 25 | tr.map(c -> 2); 26 | } catch (IllegalStateException e) { 27 | //expected 28 | } 29 | } 30 | 31 | @Test 32 | public void testFailedSocket () { 33 | try (var tr = Try.of(() -> new Socket("foo", 2234))) { 34 | 35 | tr.map(c -> 2); 36 | 37 | assertEquals(false, tr.isSuccess()); 38 | tr.onFailure(e -> assertEquals(UnknownHostException.class, e.getClass())); 39 | } 40 | } 41 | 42 | class CloseableMock implements AutoCloseable { 43 | private final String name; 44 | boolean closed = false; 45 | 46 | CloseableMock(String name) { 47 | this.name = name; 48 | } 49 | @Override 50 | public void close() throws Exception { 51 | closed = true; 52 | } 53 | @Override 54 | public String toString() { 55 | return name + " closed: " + closed; 56 | } 57 | boolean isClosed() {return closed;} 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/RetryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.time.Duration; 4 | import java.util.Random; 5 | import java.util.function.Function; 6 | import java.util.stream.LongStream; 7 | import org.junit.Test; 8 | import com.github.skopylov58.functional.FPUtils.Backoff; 9 | 10 | public class RetryTest { 11 | 12 | 13 | @Test 14 | public void testExponentialBackoff() { 15 | 16 | // Backoff backoff = exponentialBackoff(10, 1000) 17 | // .withJitter(simpleJitter(Duration.ofMillis(40))); 18 | // System.out.println(backoff); 19 | 20 | Backoff exponentialBackoff = FPUtils.exponentialBackoff(10, 1000); 21 | for (long i = 0; i < 35; i++) { 22 | Duration dur = exponentialBackoff.apply(i); 23 | System.out.println(dur); 24 | } 25 | } 26 | 27 | 28 | Duration jitter(Duration d, Random r) { 29 | long rand = r.nextLong(0, d.toMillis()); 30 | return Duration.ofMillis(rand); 31 | } 32 | 33 | @Test 34 | public void testWithJitter() throws Exception { 35 | Function fixedDelay = i -> Duration.ofHours(1); 36 | Function fixedDelayWithJitter = fixedDelay.andThen(d -> d.plus(jitter(d, new Random()))); 37 | 38 | var res = LongStream.range(0, 20).mapToObj(i -> fixedDelayWithJitter.apply(i)).toList(); 39 | System.out.println(res); 40 | 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/RunnableTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | import static org.junit.Assert.fail; 6 | 7 | import java.io.FileNotFoundException; 8 | 9 | import org.junit.Test; 10 | 11 | public class RunnableTest { 12 | 13 | @Test 14 | public void testRunnable() throws Exception { 15 | 16 | Try run = Try.of(() -> System.out.println("Runnable")); 17 | assertTrue(run.isSuccess()); 18 | assertEquals(null, run.get()); 19 | 20 | run = Try.of(() -> {throw new FileNotFoundException();}); 21 | assertTrue(run.isFailure()); 22 | run.onFailure(e -> assertTrue(e instanceof FileNotFoundException)); 23 | 24 | try { 25 | run.get(); 26 | fail(); 27 | } catch (RuntimeException e) { 28 | //expected 29 | //FileNotFoundException wrapped into RunException 30 | assertEquals(FileNotFoundException.class, e.getCause().getClass()); 31 | } 32 | } 33 | 34 | @Test 35 | public void testPureRunnable() throws Exception { 36 | Runnable r = new Runnable() { 37 | @Override 38 | public void run() { 39 | System.out.println("Run runnable"); 40 | } 41 | }; 42 | assertTrue(Try.of(() -> r.run()).isSuccess()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/StateTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import static org.junit.Assert.*; 4 | import java.util.Objects; 5 | import org.junit.Test; 6 | 7 | public class StateTest { 8 | 9 | 10 | @Test 11 | public void testName() throws Exception { 12 | State state = State.pure("foo"); 13 | 14 | // var run = state.apply(1); 15 | // System.out.println(run); 16 | 17 | 18 | var state2 = state.flatMap(s -> { 19 | return i -> new Tuple<>(s + ", bar", i + 1); 20 | }); 21 | 22 | // System.out.println(state2.apply(1)); 23 | 24 | var state3 = state2.flatMap(s -> { 25 | return i -> new Tuple<>(s + ", zoo", i + 1); 26 | }); 27 | 28 | 29 | Tuple apply = state3.apply(1); 30 | System.out.println(apply); 31 | 32 | 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/SupplierTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | import static org.junit.Assert.fail; 6 | 7 | import java.util.function.Supplier; 8 | 9 | import org.junit.Test; 10 | 11 | public class SupplierTest { 12 | @Test 13 | public void testSupplier() throws Exception { 14 | 15 | Try tr = Try.of(() -> 20/4); 16 | assertTrue(tr.isSuccess()); 17 | assertEquals(Integer.valueOf(5), tr.get()); 18 | 19 | tr = Try.of(() -> {throw new NullPointerException();}); 20 | assertTrue(tr.isFailure()); 21 | 22 | tr.onFailure(e -> assertTrue(e instanceof NullPointerException)); 23 | try { 24 | tr.get(); 25 | fail(); 26 | } catch (NullPointerException npe) { 27 | //expected 28 | } 29 | } 30 | 31 | @Test 32 | public void testPureSupplier() { 33 | Supplier s = () -> 1; 34 | // Try.of(s); - will not compile 35 | Try t = Try.of(() -> s.get()); 36 | assertEquals(true, t.isSuccess()); 37 | assertEquals(Integer.valueOf(1), t.get()); 38 | } 39 | 40 | @Test 41 | public void testSupplierWithNull() { 42 | Try t = Try.of(() -> null); 43 | assertTrue(t.isSuccess()); 44 | assertTrue(t.optional().isEmpty()); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/TryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import static org.junit.Assert.assertTrue; 4 | import static org.junit.Assert.fail; 5 | 6 | import java.io.FileNotFoundException; 7 | 8 | import javax.naming.NoPermissionException; 9 | 10 | import org.junit.Test; 11 | 12 | public class TryTest { 13 | 14 | 15 | @Test 16 | public void testFinally() throws Exception { 17 | Try tr = Try.of(() -> 1).andFinally(() -> System.out.println("Finally")); 18 | assertTrue(tr.isSuccess()); 19 | 20 | Try.CheckedRunnable er = () -> {throw new FileNotFoundException();}; 21 | tr = Try.of(() -> 1).andFinally(er); 22 | assertTrue(tr.isFailure()); 23 | } 24 | 25 | @Test 26 | public void testPeek() throws Exception { 27 | Try.of(() -> 1).peek(t -> assertTrue(t.isSuccess())); 28 | 29 | Try tr = Try.of(() -> 1).peek(t -> {throw new NoPermissionException();}); 30 | assertTrue(tr.isFailure()); 31 | } 32 | 33 | @Test 34 | public void testError() throws Exception { 35 | try { 36 | Try t = Try.of(() -> {throw new AssertionError();}); 37 | fail(); 38 | } catch (Error er) { 39 | // ok we do not handle error exceptions !!! 40 | } 41 | } 42 | 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/TupleTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import java.util.Date; 4 | 5 | import org.junit.Test; 6 | 7 | public class TupleTest { 8 | 9 | @Test 10 | public void test() throws Exception { 11 | Tuple3 t = new Tuple3<>(1, "foo", new Date()); 12 | String string = t.toString(); 13 | System.out.println(string); 14 | } 15 | 16 | @Test 17 | public void testNull() throws Exception { 18 | Tuple3 t = new Tuple3<>(null, null, null); 19 | String string = t.toString(); 20 | System.out.println(string); 21 | } 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/ZIOTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional; 2 | 3 | import static org.junit.Assert.assertTrue; 4 | 5 | import java.io.*; 6 | import java.nio.file.Files; 7 | import java.util.List; 8 | import java.util.stream.Stream; 9 | 10 | import org.junit.Test; 11 | 12 | public class ZIOTest { 13 | 14 | 15 | @Test 16 | public void test() { 17 | 18 | var zio = new ZIO<>((File f) -> Either.catching(() -> Files.lines(f.toPath()).toList())); 19 | 20 | Either> runned = zio.run().apply(new File(".")); 21 | 22 | System.out.println(runned); 23 | 24 | assertTrue(runned.isLeft()); 25 | 26 | 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/ArithmeticExceptionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.fail; 5 | 6 | import org.junit.Test; 7 | 8 | import com.github.skopylov58.functional.Try; 9 | 10 | public class ArithmeticExceptionTest { 11 | 12 | /** 13 | * @param x 14 | * @param y 15 | * @return x/y 16 | * @throws ArithmeticException if y == 0 17 | */ 18 | int div(int x, int y) { 19 | return x / y; 20 | } 21 | 22 | /** 23 | * Recovers ArithmeticException 24 | * 25 | * @param x 26 | * @param y 27 | * @return x/y or Integer.MAX_VALUE or Integer.MIN_VALUE in case of {@link ArithmeticException} 28 | */ 29 | int divWithRecover(int x, int y) { 30 | return Try.of(() -> div(x, y)) 31 | .recover(() -> x > 0 ? Integer.MAX_VALUE : Integer.MIN_VALUE, ArithmeticException.class::isInstance) 32 | .orElseThrow(); 33 | } 34 | 35 | @Test 36 | public void testDivByZero() { 37 | 38 | assertEquals(2, div(4, 2)); 39 | 40 | try { 41 | div(2, 0); 42 | fail(); 43 | } catch (ArithmeticException e) { 44 | // expected 45 | } 46 | 47 | assertEquals(2, divWithRecover(4, 2)); 48 | assertEquals(Integer.MAX_VALUE, divWithRecover(1, 0)); 49 | assertEquals(Integer.MIN_VALUE, divWithRecover(-1, 0)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/BookRecord.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import java.util.function.Consumer; 4 | 5 | public record BookRecord(String isbn, String title, String genre, String author, int published, String description) { 6 | 7 | private BookRecord(Builder builder) { 8 | this(builder.isbn, builder.title, builder.genre, builder.author, builder.published, builder.description); 9 | } 10 | 11 | public static class Builder { 12 | private final String isbn; 13 | private final String title; 14 | String genre; 15 | String author; 16 | int published; 17 | String description; 18 | 19 | public Builder(String isbn, String title) { 20 | this.isbn = isbn; 21 | this.title = title; 22 | } 23 | 24 | public Builder configure(Consumer b) { 25 | b.accept(this); 26 | return this; 27 | } 28 | 29 | public BookRecord build() { 30 | return new BookRecord(this); 31 | } 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/BookRecordTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import org.junit.Test; 4 | import com.github.skopylov58.functional.Validator; 5 | 6 | public class BookRecordTest { 7 | 8 | @Test 9 | public void testBookRecord() throws Exception { 10 | var bookRec = new BookRecord.Builder("1234", "foo bar") 11 | .configure(book -> { 12 | book.author = "author"; 13 | book.description = "desc"; 14 | book.genre = "fiction"; 15 | }) 16 | .build(); 17 | 18 | System.out.println(bookRec); 19 | 20 | Validator.of(bookRec) 21 | .validate(b -> b.genre().equals("sex") , "not allowed") 22 | .notNull(BookRecord::author, "Missing author") 23 | ; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/BuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import static org.junit.Assert.*; 4 | import org.junit.Test; 5 | 6 | public class BuilderTest { 7 | 8 | 9 | 10 | 11 | @Test 12 | public void testName() throws Exception { 13 | 14 | var b = Person.builder() 15 | .setAddress("") 16 | .setName("") 17 | .build(); 18 | 19 | Person person = Person.builder() 20 | .configure(c -> { 21 | c.address = ""; 22 | c.name = ""; 23 | }).build(); 24 | 25 | 26 | } 27 | 28 | 29 | 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/LogExceptionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.FileInputStream; 5 | import java.io.InputStream; 6 | 7 | import org.junit.Test; 8 | 9 | import com.github.skopylov58.functional.Try; 10 | 11 | public class LogExceptionTest { 12 | 13 | 14 | @Test 15 | public void testName() throws Exception { 16 | 17 | InputStream input = Try.of(() -> new FileInputStream("/a/b/c")) 18 | .onFailure(this::logError) 19 | .optional() 20 | .orElse(new ByteArrayInputStream("foo".getBytes())); 21 | } 22 | 23 | void logError(Exception e) { 24 | e.printStackTrace(); 25 | } 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/NumbersTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertFalse; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | import java.io.FileNotFoundException; 8 | import java.io.IOException; 9 | import java.util.LinkedList; 10 | import java.util.List; 11 | import java.util.Objects; 12 | import java.util.function.Function; 13 | import java.util.stream.Collectors; 14 | import java.util.stream.Stream; 15 | 16 | import org.junit.Test; 17 | 18 | import com.github.skopylov58.functional.Try; 19 | import com.github.skopylov58.functional.Tuple; 20 | 21 | /** 22 | * Converts list of strings to numbers. 23 | * @author skopylov@gmail.com 24 | * 25 | */ 26 | public class NumbersTest { 27 | 28 | String[] numbers = { "1", "2", "3", "z" }; 29 | 30 | @Test 31 | public void testNumbers() { 32 | test(this::fromStringArrayTraditional, numbers); 33 | test(this::fromStringArrayWithTry, numbers); 34 | } 35 | 36 | void test(Function> func, String [] param) { 37 | List res = func.apply(param); 38 | assertEquals(3, res.size()); 39 | assertEquals(1, res.get(0)); 40 | System.out.println(res); 41 | } 42 | 43 | List fromStringArrayWithTry(String [] nums) { 44 | return Stream.of(nums) 45 | .map(s -> Try.of(() -> Integer.valueOf(s))) 46 | .peek(t -> t.onFailure(this::logError)) 47 | .flatMap(Try::stream) //stream for Failure is empty 48 | .collect(Collectors.toList()); 49 | } 50 | 51 | List fromStringArrayWithTry0(String [] nums) { 52 | return Stream.of(nums) 53 | .filter(Objects::nonNull) 54 | .map(s -> Try.success(s).map(i-> Integer.valueOf(i))) 55 | .flatMap(Try::stream) //stream for Failure is empty 56 | .collect(Collectors.toList()); 57 | } 58 | 59 | List fromStringArrayTraditional(String [] nums) { 60 | List res = new LinkedList<>(); 61 | for (String s : nums) { 62 | if (s == null) { 63 | continue; 64 | } 65 | try { 66 | Number n = Integer.valueOf(s); 67 | res.add(n); 68 | } catch (NumberFormatException e) { 69 | logError(e); 70 | } 71 | } 72 | return res; 73 | } 74 | 75 | void logError(Exception e) { 76 | System.out.println(e.getClass().getName() + " " + e.getMessage()); 77 | } 78 | 79 | //With higher order function 80 | List fromStringArrayWithHOF(String [] nums) { 81 | return Stream.of(nums) 82 | .filter(Objects::nonNull) 83 | .map(Try.catching(Integer::valueOf)) 84 | .flatMap(Try::stream) //stream for Failure is empty 85 | .collect(Collectors.toList()); 86 | } 87 | 88 | List fromStringArrayWithTuple(String [] nums) { 89 | return Stream.of(nums) 90 | .map(s -> new Tuple>(s,Try.of(() ->Integer.valueOf(s)))) 91 | .collect(Collectors.groupingBy(t -> t.second.isSuccess())) 92 | .get(true) 93 | .stream() 94 | .map(t -> t.second.get()) 95 | .collect(Collectors.toList()); 96 | } 97 | 98 | 99 | 100 | @Test 101 | public void testAssign() { 102 | assertTrue(IOException.class.isAssignableFrom(FileNotFoundException.class)); 103 | assertFalse(IOException.class.isAssignableFrom(IndexOutOfBoundsException.class)); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/Person.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import java.util.function.Consumer; 4 | 5 | public class Person { 6 | final String name; 7 | final String address; 8 | 9 | private Person(String name, String address) { 10 | this.name = name; 11 | this.address = address; 12 | } 13 | 14 | public static Person.Builder builder() { 15 | return new Person.Builder(); 16 | } 17 | 18 | public static class Builder { 19 | String name = ""; 20 | String address = ""; 21 | 22 | public Builder configure(Consumer c) { 23 | c.accept(this); 24 | return this; 25 | } 26 | 27 | 28 | public Builder setName(String name) { 29 | this.name = name; 30 | return this; 31 | } 32 | 33 | public Builder setAddress(String address) { 34 | this.address = address; 35 | return this; 36 | } 37 | 38 | public Person build() { 39 | return new Person(name, address); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/PropertiesTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNotNull; 5 | 6 | import java.io.FileInputStream; 7 | import java.io.IOException; 8 | import java.util.Properties; 9 | 10 | import org.junit.Test; 11 | 12 | import com.github.skopylov58.functional.Try; 13 | 14 | public class PropertiesTest { 15 | 16 | final static String fileName = "/a/b/c"; 17 | 18 | @Test 19 | public void testProperties() { 20 | Properties props = fromFileTraditional(fileName); 21 | assertNotNull(props); 22 | assertEquals(0, props.size()); 23 | 24 | props = fromFileWithTry(fileName); 25 | assertNotNull(props); 26 | assertEquals(0, props.size()); 27 | } 28 | 29 | public Properties fromFileTraditional(String fileName) { 30 | Properties p = new Properties(); 31 | try (FileInputStream in = new FileInputStream(fileName)) { 32 | p.load(in); 33 | } catch (IOException e) { 34 | System.out.println(e.getMessage()); 35 | } 36 | return p; 37 | } 38 | 39 | public Properties fromFileWithTry(String fileName) { 40 | Properties p = new Properties(); 41 | try (var input = Try.of(() -> new FileInputStream(fileName))) { 42 | return input.map(in -> {p.load(in);return p;}) 43 | .onFailure(System.out::println) 44 | .optional() 45 | .orElse(p); 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/ReadURLTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import static org.junit.Assert.assertTrue; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | import java.net.MalformedURLException; 10 | import java.net.URL; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.Objects; 14 | import java.util.Optional; 15 | import java.util.stream.Collectors; 16 | 17 | import org.junit.Test; 18 | 19 | import com.github.skopylov58.functional.Try; 20 | 21 | /** 22 | * Example which reads the text from the URL. 23 | * @author skopylov@gmail.com 24 | * 25 | */ 26 | public class ReadURLTest { 27 | 28 | String urlString = "http://www.google.com/"; 29 | 30 | @Test 31 | public void readURLTraditional() { 32 | List list = readURLTraditional(urlString); 33 | System.out.println(list); 34 | } 35 | 36 | @Test 37 | public void readURLWithTry() { 38 | List list = readURLWithTry(urlString); 39 | System.out.println(list); 40 | } 41 | 42 | public List readURLTraditional(String urlString) { 43 | if (urlString == null) { 44 | return Collections.emptyList(); 45 | } 46 | URL url; 47 | try { 48 | url = new URL(urlString); 49 | } catch (MalformedURLException e) { 50 | System.out.println(e.getMessage()); 51 | return Collections.emptyList(); 52 | } 53 | try (InputStream inputStream = url.openStream()) { 54 | BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 55 | return reader.lines().collect(Collectors.toList()); 56 | } catch (IOException e) { 57 | System.out.println(e.getMessage()); 58 | return Collections.emptyList(); 59 | } 60 | } 61 | 62 | public List readURLWithTry(String urlString) { 63 | try(var reader = Try.success(urlString) 64 | .filter(Objects::nonNull) 65 | .map(URL::new) 66 | .map(URL::openStream) 67 | .map(i -> new BufferedReader(new InputStreamReader(i)))) { 68 | 69 | return reader.map(r -> r.lines().collect(Collectors.toList())) 70 | .onFailure(e -> System.out.println(e.getMessage())) 71 | .optional() 72 | .orElse(Collections.emptyList()); 73 | } 74 | } 75 | 76 | @Test 77 | public void testURL() throws Exception { 78 | Optional optional = Try.of(() -> new URL("foo.bar")).optional(); 79 | assertTrue(optional.isEmpty()); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/RecoverTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import java.io.FileInputStream; 7 | import java.io.FileNotFoundException; 8 | import java.io.IOException; 9 | import java.util.Objects; 10 | import java.util.Optional; 11 | import java.util.Properties; 12 | import java.util.stream.Stream; 13 | 14 | import org.junit.Test; 15 | 16 | import com.github.skopylov58.functional.Try; 17 | 18 | public class RecoverTest { 19 | 20 | @Test 21 | public void testName() throws Exception { 22 | Optional optional = getPropertyWithTry("/a/b/c", "propName"); 23 | assertTrue(optional.isEmpty()); 24 | 25 | optional = getPropertyWithTryAndStream("/a/b/c", "propName"); 26 | assertTrue(optional.isEmpty()); 27 | } 28 | 29 | @Test 30 | public void testName1() throws Exception { 31 | 32 | System.setProperty("propName", "foo"); 33 | 34 | Optional optional = getPropertyWithTry("/a/b/c", "propName"); 35 | assertEquals("foo", optional.get()); 36 | 37 | optional = getPropertyWithTryAndStream("/a/b/c", "propName"); 38 | assertEquals("foo", optional.get()); 39 | 40 | System.clearProperty("propName"); 41 | 42 | } 43 | 44 | Optional getPropertyWithTry(String fileName, String propName) { 45 | return Try.of(() -> readPropertyFromFile(fileName, propName)) 46 | .onFailure(System.err::println) 47 | .filter(Objects::nonNull) 48 | .recover(() -> readPropertyFromFile(System.getProperty("user.home")+"/"+ fileName, propName)) 49 | .onFailure(System.err::println) 50 | .filter(Objects::nonNull) 51 | .recover(() -> System.getenv(propName)) 52 | .filter(Objects::nonNull) 53 | .recover(() -> System.getProperty(propName)) 54 | .filter(Objects::nonNull) 55 | .optional(); 56 | } 57 | 58 | Optional getPropertyWithTryAndStream(String fileName, String propName) { 59 | return Stream.of((Try.CheckedSupplier) 60 | () -> readPropertyFromFile(fileName, propName), 61 | () -> readPropertyFromFile(System.getProperty("user.home")+"/"+ fileName, propName), 62 | () -> System.getenv(propName), 63 | () -> System.getProperty(propName)) 64 | .map(Try::of) 65 | .peek(t -> t.onFailure(System.err::println)) 66 | .flatMap(Try::stream) 67 | .filter(Objects::nonNull) 68 | .findFirst(); 69 | } 70 | 71 | Optional getPropertyTraditional(String fileName, String propName) { 72 | String res = null; 73 | String [] files = new String[] {fileName, System.getProperty("user.home")+"/"+ fileName}; 74 | for (int i = 0; i < files.length; i++) { 75 | try { 76 | res = readPropertyFromFile(files[i], propName); 77 | if (res != null) { 78 | return Optional.of(res); 79 | } 80 | } catch (IOException e) { 81 | System.err.println(e); 82 | } 83 | } 84 | 85 | res = System.getenv(propName); 86 | if (res != null) { 87 | return Optional.of(res); 88 | } 89 | res = System.getProperty(propName); 90 | if (res != null) { 91 | return Optional.of(res); 92 | } 93 | return Optional.empty(); 94 | } 95 | 96 | String readPropertyFromFile(String fileName, String propName) throws FileNotFoundException, IOException { 97 | try(FileInputStream in = new FileInputStream(fileName)) { 98 | Properties p = new Properties(); 99 | p.load(in); 100 | return p.getProperty(propName); 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/SimpleValidator.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import com.github.skopylov58.functional.Validator; 4 | 5 | public interface SimpleValidator extends Validator{ 6 | 7 | static SimpleValidator of(T t) { 8 | return (SimpleValidator) Validator.of(t); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/SocketTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import java.io.OutputStream; 4 | import java.net.Socket; 5 | 6 | import org.junit.Test; 7 | 8 | import com.github.skopylov58.functional.Try; 9 | 10 | public class SocketTest { 11 | 12 | 13 | @Test 14 | public void testTraditional() { 15 | try (Socket s = new Socket("host", 8888)) { 16 | OutputStream outputStream = s.getOutputStream(); 17 | outputStream.write(new byte[] {1,2,3}); 18 | } catch (Exception e) { 19 | System.out.println(e); 20 | } 21 | } 22 | 23 | @Test 24 | public void testWithTry() { 25 | try (var s = Try.of(() -> new Socket("host", 8888))) { 26 | s.flatMap(Try.catching(Socket::getOutputStream)) 27 | .flatMap(Try.consumeCatching(out -> out.write(new byte[] {1,2,3}))) 28 | .onSuccess(out -> out.write(new byte[] {1,2,3})) 29 | .onFailure(e -> System.out.println(e)); 30 | } 31 | } 32 | 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/URLStreamTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.net.MalformedURLException; 6 | import java.net.URL; 7 | import java.util.List; 8 | import java.util.Objects; 9 | import java.util.function.Function; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.Stream; 12 | 13 | import org.junit.Test; 14 | 15 | import com.github.skopylov58.functional.Try; 16 | 17 | /** 18 | * Example of converting list of strings to list of URLs 19 | *

20 | * Given: list/array of string URLS 21 | *

22 | * Required: list of URL objects 23 | * 24 | * 25 | * @author skopylov@gmail.com 26 | * 27 | */ 28 | 29 | public class URLStreamTest { 30 | 31 | final static String goocom = "http://google.com"; 32 | final static String[] urls = { "foo", goocom, "bar" }; 33 | 34 | @Test 35 | public void testURLStream() { 36 | test(this::urlListWithTry, urls); 37 | test(this::urlListTraditional, urls); 38 | } 39 | 40 | void test(Function> func, String [] param) { 41 | List list = func.apply(param); 42 | assertEquals(1, list.size()); 43 | URL url = list.get(0); 44 | assertEquals(goocom, url.toString()); 45 | System.out.println(url); 46 | } 47 | 48 | private List urlListWithTry(String[] urls) { 49 | return Stream.of(urls).map(Try.catching(URL::new)) 50 | .map(t -> t.onFailure(e -> System.out.println(e.getMessage()) )) 51 | .flatMap(Try::stream) 52 | .collect(Collectors.toList()); 53 | } 54 | 55 | private List urlListTraditional(String[] urls) { 56 | return Stream.of(urls).map(s -> { 57 | try { 58 | return new URL(s); 59 | } catch (MalformedURLException me) { 60 | System.out.println(me.getMessage()); 61 | return null; 62 | } 63 | }).filter(Objects::nonNull) 64 | .collect(Collectors.toList()); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/com/github/skopylov58/functional/samples/ValidationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.skopylov58.functional.samples; 2 | 3 | import org.junit.Test; 4 | 5 | import com.github.skopylov58.functional.Validation; 6 | 7 | public class ValidationTest { 8 | 9 | record Person(String name, int age, String email, Address address) { 10 | Person() { this(null, 0, null, null); } 11 | Person(String name) { this(name, 0, null, null); } 12 | Person(String name, int age) { this(name, age, null, null); } 13 | Person(String name, int age, String email) { this(name, age, email, null); } 14 | } 15 | 16 | record Address(String country, String city, int zipCode) {} 17 | 18 | enum PersonError { 19 | TOO_YOUNG, 20 | MISSING_NAME, 21 | NAME_TO_LONG 22 | } 23 | 24 | sealed interface IPersonErrors permits TooYoung, TooLongName, MissingName {} 25 | record TooYoung(int age) implements IPersonErrors{} 26 | record TooLongName(String name) implements IPersonErrors{} 27 | record MissingName() implements IPersonErrors {} 28 | 29 | @Test 30 | public void testStrErr() throws Exception { 31 | 32 | var addressValidation = Validation.builder() 33 | .addValidation(a -> a.city == null, "Missing city") 34 | .addValidation(a -> a.zipCode <= 0, "Invalid zip code") 35 | .build(); 36 | 37 | 38 | var personValidation = Validation.builder() 39 | .addValidation(p -> p.age < 18, "Too young") 40 | .addValidation(p -> p.name == null, "Missing name") 41 | .addValidation(p -> (p.name != null) && (p.name.length() > 16), "Too long name") 42 | .addValidation(p -> p.email == null, "Missing e-mail") 43 | .addValidation(Person::address, addressValidation) 44 | .build(); 45 | 46 | personValidation.validate(new Person(null)) 47 | .forEach(System.out::println); 48 | 49 | personValidation.validate(new Person("Very very long name to be checked ", 30)) 50 | .forEach(System.out::println); 51 | } 52 | 53 | @Test 54 | public void testEnumErr() throws Exception { 55 | var validation = Validation.builder() 56 | .addValidation(p -> p.age < 18, PersonError.TOO_YOUNG) 57 | .addValidation(p -> p.name == null, PersonError.MISSING_NAME) 58 | .addValidation(p -> (p.name != null) && (p.name.length() > 16), PersonError.NAME_TO_LONG) 59 | .build(); 60 | 61 | validation.validate(new Person(null, 0, "foo@bar")) 62 | .forEach(System.out::println); 63 | 64 | validation.validate(new Person("Very very long name to be checked", 30, "foo@bar")) 65 | .forEach(System.out::println); 66 | } 67 | 68 | @Test 69 | public void testSealedErr() throws Exception { 70 | var validation = Validation.builder() 71 | .addValidation(p -> p.age < 18, p -> new TooYoung(p.age)) 72 | .addValidation(p -> p.name == null, new MissingName()) 73 | .addValidation(p -> (p.name != null) && (p.name.length() > 16), p -> new TooLongName(p.name)) 74 | .build(); 75 | 76 | validation.validate(new Person(null, 15, "foo@bar")) 77 | .forEach(System.out::println); 78 | 79 | validation.validate(new Person("Very very long name to be checked ", 30, "foo@bar")) 80 | .forEach(System.out::println); 81 | } 82 | 83 | @Test 84 | public void testPassword() throws Exception { 85 | String special = "@#$%^&*-_!+=[]{}|\\:’,.?/`~\"();"; 86 | var passwordValidation = Validation.builder() 87 | .addValidation(p -> p.length() <8, "Too short password") 88 | .addValidation(p -> p.length() > 16, "Too long password") 89 | .addValidation(p -> p.chars().anyMatch(Character::isWhitespace), "Spaces are not allowed in the password") 90 | .addValidation(p -> !p.chars().anyMatch(Character::isDigit), "Missing digits in the password") 91 | .addValidation(p -> !p.chars().anyMatch(Character::isUpperCase), "Missing upper case chars") 92 | .addValidation(p -> !p.chars().anyMatch(Character::isLowerCase), "Missing lower case chars") 93 | .build(); 94 | } 95 | 96 | 97 | } 98 | --------------------------------------------------------------------------------