diff --git a/monad/etc/monad.png b/monad/etc/monad.png new file mode 100644 index 000000000..f82e7a3e4 Binary files /dev/null and b/monad/etc/monad.png differ diff --git a/monad/etc/monad.ucls b/monad/etc/monad.ucls new file mode 100644 index 000000000..9c98df7c6 --- /dev/null +++ b/monad/etc/monad.ucls @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/monad/index.md b/monad/index.md new file mode 100644 index 000000000..82deba922 --- /dev/null +++ b/monad/index.md @@ -0,0 +1,35 @@ +--- +layout: pattern +title: Monad +folder: monad +permalink: /patterns/monad/ +categories: Other +tags: + - Java + - Difficulty-Advanced + - Functional +--- + +## Intent + +Monad pattern based on monad from linear algebra represents the way of chaining operations +together step by step. Binding functions can be described as passing one's output to another's input +basing on the 'same type' contract. Formally, monad consists of a type constructor M and two +operations: +bind - that takes monadic object and a function from plain object to monadic value and returns monadic value +return - that takes plain type object and returns this object wrapped in a monadic value. + +![alt text](./etc/monad.png "Monad") + +## Applicability + +Use the Monad in any of the following situations + +* when you want to chain operations easily +* when you want to apply each function regardless of the result of any of them + +## Credits + +* [Design Pattern Reloaded by Remi Forax](https://youtu.be/-k2X7guaArU) +* [Brian Beckman: Don't fear the Monad](https://channel9.msdn.com/Shows/Going+Deep/Brian-Beckman-Dont-fear-the-Monads) +* [Monad on Wikipedia](https://en.wikipedia.org/wiki/Monad_(functional_programming)) \ No newline at end of file diff --git a/monad/pom.xml b/monad/pom.xml new file mode 100644 index 000000000..ec5a14a8c --- /dev/null +++ b/monad/pom.xml @@ -0,0 +1,43 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.11.0-SNAPSHOT + + monad + + + junit + junit + test + + + + diff --git a/monad/src/main/java/com/iluwatar/monad/App.java b/monad/src/main/java/com/iluwatar/monad/App.java new file mode 100644 index 000000000..e330cfa64 --- /dev/null +++ b/monad/src/main/java/com/iluwatar/monad/App.java @@ -0,0 +1,35 @@ +package com.iluwatar.monad; + +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * The Monad pattern defines a monad structure, that enables chaining operations + * in pipelines and processing data step by step. + * Formally, monad consists of a type constructor M and two operations: + *
bind - that takes monadic object and a function from plain object to the + * monadic value and returns monadic value. + *
return - that takes plain type object and returns this object wrapped in a monadic value. + *

+ * In the given example, the Monad pattern is represented as a {@link Validator} that takes an instance + * of a plain object with {@link Validator#of(Object)} + * and validates it {@link Validator#validate(Function, Predicate, String)} against given predicates. + *

As a validation result {@link Validator#get()} it either returns valid object {@link Validator#t} + * or throws a list of exceptions {@link Validator#exceptions} collected during validation. + */ +public class App { + + /** + * Program entry point. + * + * @param args @param args command line args + */ + public static void main(String[] args) { + User user = new User("user", 24, Sex.FEMALE, "foobar.com"); + System.out.println(Validator.of(user).validate(User::getName, Objects::nonNull, "name is null") + .validate(User::getName, name -> !name.isEmpty(), "name is empty") + .validate(User::getEmail, email -> !email.contains("@"), "email doesn't containt '@'") + .validate(User::getAge, age -> age > 20 && age < 30, "age isn't between...").get().toString()); + } +} diff --git a/monad/src/main/java/com/iluwatar/monad/Sex.java b/monad/src/main/java/com/iluwatar/monad/Sex.java new file mode 100644 index 000000000..8b7e43f3c --- /dev/null +++ b/monad/src/main/java/com/iluwatar/monad/Sex.java @@ -0,0 +1,5 @@ +package com.iluwatar.monad; + +public enum Sex { + MALE, FEMALE +} diff --git a/monad/src/main/java/com/iluwatar/monad/User.java b/monad/src/main/java/com/iluwatar/monad/User.java new file mode 100644 index 000000000..edd299643 --- /dev/null +++ b/monad/src/main/java/com/iluwatar/monad/User.java @@ -0,0 +1,38 @@ +package com.iluwatar.monad; + +public class User { + + private String name; + private int age; + private Sex sex; + private String email; + + /** + * @param name - name + * @param age - age + * @param sex - sex + * @param email - email address + */ + public User(String name, int age, Sex sex, String email) { + this.name = name; + this.age = age; + this.sex = sex; + this.email = email; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public Sex getSex() { + return sex; + } + + public String getEmail() { + return email; + } +} diff --git a/monad/src/main/java/com/iluwatar/monad/Validator.java b/monad/src/main/java/com/iluwatar/monad/Validator.java new file mode 100644 index 000000000..2298a4d8e --- /dev/null +++ b/monad/src/main/java/com/iluwatar/monad/Validator.java @@ -0,0 +1,91 @@ +package com.iluwatar.monad; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Class representing Monad design pattern. Monad is a way of chaining operations on the + * given object together step by step. In Validator each step results in either success or + * failure indicator, giving a way of receiving each of them easily and finally getting + * validated object or list of exceptions. + * + * @param Placeholder for an object. + */ +public class Validator { + /** + * Object that is validated + */ + private final T t; + + /** + * List of exception thrown during validation. + */ + private final List exceptions = new ArrayList<>(); + + /** + * Creates a monadic value of given object. + * @param t object to be validated + */ + private Validator(T t) { + this.t = t; + } + + /** + * Creates validator against given object + * + * @param t object to be validated + * @param object's type + * @return new instance of a validator + */ + public static Validator of(T t) { + return new Validator<>(Objects.requireNonNull(t)); + } + + /** + * @param validation one argument boolean-valued function that + * represents one step of validation. Adds exception to main validation exception + * list when single step validation ends with failure. + * @param message error message when object is invalid + * @return this + */ + public Validator validate(Predicate validation, String message) { + if (!validation.test(t)) { + exceptions.add(new IllegalStateException(message)); + } + return this; + } + + /** + * Extension for the {@link Validator#validate(Function, Predicate, String)} method, + * dedicated for objects, that need to be projected before requested validation. + * + * @param projection function that gets an objects, and returns projection representing + * element to be validated. + * @param validation see {@link Validator#validate(Function, Predicate, String)} + * @param message see {@link Validator#validate(Function, Predicate, String)} + * @param see {@link Validator#validate(Function, Predicate, String)} + * @return this + */ + public Validator validate(Function projection, Predicate validation, + String message) { + return validate(projection.andThen(validation::test)::apply, message); + } + + /** + * Receives validated object or throws exception when invalid. + * + * @return object that was validated + * @throws IllegalStateException when any validation step results with failure + */ + public T get() throws IllegalStateException { + if (exceptions.isEmpty()) { + return t; + } + IllegalStateException e = new IllegalStateException(); + exceptions.forEach(e::addSuppressed); + throw e; + } +} diff --git a/monad/src/test/java/com/iluwatar/monad/AppTest.java b/monad/src/test/java/com/iluwatar/monad/AppTest.java new file mode 100644 index 000000000..5d23b41a6 --- /dev/null +++ b/monad/src/test/java/com/iluwatar/monad/AppTest.java @@ -0,0 +1,13 @@ +package com.iluwatar.monad; + +import org.junit.Test; + +public class AppTest { + + @Test + public void testMain() { + String[] args = {}; + App.main(args); + } + +} diff --git a/monad/src/test/java/com/iluwatar/monad/MonadTest.java b/monad/src/test/java/com/iluwatar/monad/MonadTest.java new file mode 100644 index 000000000..ae78572f8 --- /dev/null +++ b/monad/src/test/java/com/iluwatar/monad/MonadTest.java @@ -0,0 +1,42 @@ +package com.iluwatar.monad; + + +import junit.framework.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.Objects; + +public class MonadTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testForInvalidName() { + thrown.expect(IllegalStateException.class); + User tom = new User(null, 21, Sex.MALE, "tom@foo.bar"); + Validator.of(tom).validate(User::getName, Objects::nonNull, "name cannot be null").get(); + } + + @Test + public void testForInvalidAge() { + thrown.expect(IllegalStateException.class); + User john = new User("John", 17, Sex.MALE, "john@qwe.bar"); + Validator.of(john).validate(User::getName, Objects::nonNull, "name cannot be null") + .validate(User::getAge, age -> age > 21, "user is underaged") + .get(); + } + + @Test + public void testForValid() { + User sarah = new User("Sarah", 42, Sex.FEMALE, "sarah@det.org"); + User validated = Validator.of(sarah).validate(User::getName, Objects::nonNull, "name cannot be null") + .validate(User::getAge, age -> age > 21, "user is underaged") + .validate(User::getSex, sex -> sex == Sex.FEMALE, "user is not female") + .validate(User::getEmail, email -> email.contains("@"), "email does not contain @ sign") + .get(); + Assert.assertSame(validated, sarah); + } +} diff --git a/pom.xml b/pom.xml index e3bb0d51d..555844660 100644 --- a/pom.xml +++ b/pom.xml @@ -122,6 +122,7 @@ factory-kit feature-toggle value-object + monad