Extends java.util.function.Supplier with higher-order methods for composition with other suppliers and transformation and filtering of the results produced by its functional method get
.
This project is stable, there won’t be any breaking changes in new releases.
Requires Java 8 or higher.
<dependency>
<groupId>io.sourceforge.recipe</groupId>
<artifactId>recipe</artifactId>
<version>1.0.3</version>
</dependency>
implementation 'io.sourceforge.recipe:recipe:1.0.3'
mvn clean compile
import io.sourceforge.recipe.Recipe;
import io.sourceforge.recipe.exception.RecipeFilterException;
import io.sourceforge.recipe.util.Pair;
import static io.sourceforge.recipe.util.Fn.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.*;
import java.util.stream.Stream;
import static java.math.BigDecimal.ZERO;
import static java.util.Arrays.asList;
import static java.util.Collections.synchronizedList;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.ThreadLocalRandom.current;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.*;
Recipe is a functional interface that extends the Supplier interface.
Both represent functions that accept nothing and return some value.
These functions are executed by calling their functional method get
.
Supplier<String> sup = () -> "foo";
Recipe <String> rec = () -> "foo";
System.out.println(
sup.get().equals(rec.get())
);
Results:
true
Recipe<UUID> rec = UUID::randomUUID;
System.out.println(rec.get());
System.out.println(rec.get());
Results:
96a3732a-70ae-4d4a-9944-4bd39daf0af4 5d858623-8653-4c8a-90a9-b7cfdae043a5
Recipe extends Supplier by providing default methods for composition with other suppliers / recipes and transformation and filtering of the results produced by its method get
.
There is no way to chain those default method calls directly on the lambda expression / method reference such that the whole definition of a recipe is written as a single expression. One way to achieve this is to use the static method of
as wrapper.
public static final Recipe<String>
REC_UUID_STR = Recipe
.of(UUID::randomUUID)
.map(UUID::toString);
All Recipe’s methods that accept other recipes as arguments, accept any extension of the Supplier instead of the concrete Recipe
type. That’s why the method of
can also be used as converter to the Recipe
type.
Supplier<UUID> sup = UUID::randomUUID;
Recipe <Integer> rec = Recipe
.of(sup)
.map(UUID::toString)
.map(String::length);
System.out.println(rec.get());
Results:
36
Recipe that constantly returns the given reference can be created with the static method ofValue
.
Recipe<List<Integer>> rec = Recipe
.ofValue(asList(1, 2, 3));
System.out.println(
rec.get() == rec.get()
);
Results:
true
This method is also useful for creating stateful recipes.
Recipe<Long> rec = Recipe
.ofValue(new AtomicLong())
.map(AtomicLong::incrementAndGet);
System.out.println(rec.get());
System.out.println(rec.get());
System.out.println(rec.get());
Results:
1 2 3
Recipe that constantly returns null
can be created with the static method ofNull
.
Recipe that randomly chooses between constants of an Enum can be created with the static method ofEnum
.
public static enum ContactType {
EMAIL, MOBILE_PHONE, LANDLINE
}
public static final Recipe<ContactType>
REC_CONTACT_TYPE = Recipe
.ofEnum(ContactType.class);
Recipe that randomly chooses a value from two recipes can be created with the method or
.
Recipe<Integer> rec = Recipe
.of(() -> current().nextInt())
.or(() -> null);
System.out.println(rec.get());
System.out.println(rec.get());
System.out.println(rec.get());
Results:
-1658766505 614352061 null
For a union of variable number of recipes, use the static method oneOf
.
Recipe<String> rec = Recipe
.oneOf(() -> "foo",
REC_UUID_STR,
Recipe.ofValue("bar"),
REC_CONTACT_TYPE.map(ContactType::name));
System.out.println(rec.get());
System.out.println(rec.get());
System.out.println(rec.get());
System.out.println(rec.get());
Results:
bar 37d16e60-e337-47c4-b214-41bcbbb962d1 be1dcfc8-f5f1-476e-87e6-0ec8ff0e24e9 MOBILE_PHONE
Use the method filter
to create a new recipe that returns only those values, produced by the underlying recipe, that match a predicate.
Recipe<Double> rec = Recipe
.of(() -> current().nextDouble())
.filter(x -> x > 0)
.filter(x -> !x.isInfinite());
System.out.println(rec.get());
System.out.println(rec.get());
Results:
0.12826014636372696 0.3920702845614149
When the given predicate cannot be satisfied after 100 tries, runtime exception RecipeFilterException
is thrown.
Recipe<Integer> rec = Recipe
.of(() -> current().nextInt())
.filter(x -> x > 0 && x < 6);
try {
rec.get();
} catch (RecipeFilterException ex) {
System.out.println(ex.getMessage());
}
Results:
Couldn't satisfy predicate after 100 tries.
This can be fixed either by improving the underlying recipe (e.g., use nextInt(1, 6)
instead of unbounded nextInt()
) or by relaxing the predicate.
Use the method map
to create a new recipe that applies a function to the values produced by the underlying recipe.
If the given function has side effects, it is often void
or returns some other type that you might want to ignore.
Use the helper method Fn#doto to apply the function to the object and return that same object.
Recipe<List<Integer>> rec = Recipe
.ofValue(asList(1, 2, 3, 4, 5)) // unmodifiable list
.map(ArrayList::new) // make modifiable copy
.map(doto(list -> list.add(42))) // add method returns boolean
.map(doto(Collections::shuffle)) // shuffle method is void
.map(Collections::unmodifiableList) // make unmodifiable list
.map(doto(System.out::println)); // println method is void
rec.get();
rec.get();
Results:
[3, 42, 5, 2, 1, 4] [2, 5, 3, 1, 4, 42]
Use the method bind
to create a new recipe that takes values produced by two recipes and combines them with a binary function. There are three common cases:
For example, recipe that takes a random element from a list - the first recipe produces a list of elements, the second an index based on the size of the produced list. Binary function uses those values to retrieve an element.
Recipe<Object> rec = Recipe
.oneOf(() -> asList(1, 2, 3, 4),
() -> asList("foo", "bar", "baz"))
.bind(list -> () -> current().nextInt(0, list.size()), List::get);
System.out.println(rec.get());
System.out.println(rec.get());
System.out.println(rec.get());
Results:
bar baz 4
This is a very common pattern. To make it more readable, creation of the second recipe can be written as a method reference, with the help of static method recIndex
.
public static Recipe<String>
REC_EMAIL_DOMAIN = Recipe
.ofValue(asList("gmail.com", "yahoo.com", "proton.me"))
.bind(Examples::recIndex, List::get);
public static Recipe<Integer> recIndex(List<?> list) {
if (list.isEmpty())
throw new IllegalArgumentException("empty list");
return () -> current().nextInt(0, list.size());
}
If the first recipe depends on the value produced by the second recipe, then swap their order.
For example, an email address consists of two parts, a local part and a domain, joined with the symbol @
. Both parts can be generated independently. Helper method Fn#recfn can be used instead of function with ignored argument.
Recipe<String> recEmail =
REC_UUID_STR
.map(local -> local.substring(0, 8))
.bind(recfn(REC_EMAIL_DOMAIN), // __ -> REC_EMAIL_DOMAIN
(local, domain) -> local + "@" + domain);
System.out.println(recEmail.get());
System.out.println(recEmail.get());
Results:
[email protected] [email protected]
For example, recipe that produces a pair of a list and the maximum element from that list. Helper method Fn#fnrec can be used to wrap a function so that it returns a constant recipe of its result.
Recipe<Pair<List<Integer>, Integer>> rec = Recipe
.ofValue(asList(1, 9, 3, 5, 7))
// list -> Recipe.ofValue(Collections.max(list))
.bind(fnrec(Collections::max), Pair::new);
System.out.println(rec.get());
Results:
Pair[first=[1, 9, 3, 5, 7], second=9]
In any of the cases above, it may happen that the binary function has side effects. Such function is often void
or returns some other type that you might want to ignore. Use the helper method Fn#dotwo to apply the function to the given objects and return the pair of those objects. If you need only the first argument provided to the binary function, use Fn#biFirst. If you need only the second argument, use Fn#biSecond.
Recipe<List<Integer>> rec = Recipe
.ofValue(synchronizedList(new ArrayList<Integer>()))
.bind(recfn(Recipe
.ofValue(new AtomicInteger())
.map(AtomicInteger::incrementAndGet)),
biFirst(List::add)) // add returns boolean
// .map(List::copyOf) in Java 10+
.map(ArrayList::new)
.map(Collections::unmodifiableList);
System.out.println(rec.get());
System.out.println(rec.get());
System.out.println(rec.get());
Results:
[1] [1, 2] [1, 2, 3]
Use the method wrap
to create a new recipe that applies a function to the underlying recipe.
This is commonly used with Stream#generate, which creates an infinite stream of values produced by the provided supplier / recipe.
Recipe<List<Integer>> rec = recInt(-100, 100)
.wrap(Stream::generate)
.bind(recfn(recInt(1, 6)), Stream::limit)
.map(stream -> stream.collect(toList()));
System.out.println(rec.get());
System.out.println(rec.get());
System.out.println(rec.get());
Results:
[26, -68, -90, -23, 65] [85, -30, 82, 10, -4] [69, 64, -58]
Handling runtime exceptions is another use case, e.g., with JUnit’s Assertions#assertThrows.
REC_INT
.map(x -> x / 0) // divide by zero!
.wrap(rec -> assertThrows(ArithmeticException.class, rec::get))
.map(ArithmeticException::getMessage)
.map(doto(System.out::println))
.get();
Results:
/ by zero
Whenever a (binary) function provided to map
or bind
returns a recipe, the overall recipe type can be flattened (from Recipe<Recipe<T>>
to Recipe<T>
) by chaining an additional .map(Recipe::get)
call, while preserving the same behavior.
Recipe for recipes is useful for creating unions of recipes. The example bellow is similar to the method oneOf
but it accepts weight for each recipe to determine the frequency of picking that recipe.
public static <T> Recipe<T> recFreq(Map<? extends Supplier<? extends T>, Integer> frequencies) {
// naive implementation
List<Recipe<? extends T>> recipes = frequencies
.entrySet().stream()
.filter(e -> e.getValue() > 0)
.flatMap(e -> Stream.generate(e::getKey).limit(e.getValue()))
.map(Recipe::of)
.collect(toList());
if (recipes.isEmpty())
throw new IllegalArgumentException("No positive frequencies.");
return Recipe
.ofValue(recipes)
.bind(Examples::recIndex, List::get)
.map(Recipe::get); // flatten
}
For example, recipe that produces foo
50%, bar
20% and baz
30% of the time.
Map<Recipe<String>, Integer> frequencies = new HashMap<>();
frequencies.put(() -> "foo", 5);
frequencies.put(() -> "bar", 2);
frequencies.put(() -> "baz", 3);
Stream
.generate(recFreq(frequencies))
.limit(1000)
.collect(groupingBy(identity(), counting()))
.forEach((k, v) -> System.out.printf("%s was generated %d times\n", k, v));
Results:
foo was generated 494 times bar was generated 199 times baz was generated 307 times
Methods of the class ThreadLocalRandom are used here, but you can delegate to any number generator.
public static Recipe<Integer> recInt(int origin, int bound) {
current().nextInt(origin, bound); // check constraints
return () -> current().nextInt(origin, bound);
}
public static Recipe<Long> recLong(long origin, long bound) {
current().nextLong(origin, bound); // check constraints
return () -> current().nextLong(origin, bound);
}
public static Recipe<Double> recDouble(double origin, double bound) {
current().nextDouble(origin, bound); // check constraints
return () -> current().nextDouble(origin, bound);
}
public static Recipe<BigDecimal> recBigdec(double origin, double bound) {
return recDouble(origin, bound)
.filter(Double::isFinite)
.map(BigDecimal::valueOf);
}
public static Recipe<BigDecimal> recBigdec(BigDecimal origin, BigDecimal bound) {
return recBigdec(origin.doubleValue(), bound.doubleValue());
}
Note that these recipes produce less random values, e.g., any union (or
, oneOf
) with the _ZERO_
recipe will generate zeros more frequently. This behavior can be useful though, because zero is an edge case.
public static final Recipe<Integer>
REC_INT_POS = recInt(0, Integer.MAX_VALUE).map(x -> x + 1),
REC_INT_NEG = recInt(Integer.MIN_VALUE, 0),
REC_INT_ZERO = Recipe.ofValue(0),
REC_INT_NAT = REC_INT_POS.or(REC_INT_ZERO),
REC_INT = Recipe.oneOf(REC_INT_NEG, REC_INT_ZERO, REC_INT_POS);
public static final Recipe<Long>
REC_LONG_POS = recLong(0, Long.MAX_VALUE).map(x -> x + 1),
REC_LONG_NEG = recLong(Long.MIN_VALUE, 0),
REC_LONG_ZERO = Recipe.ofValue(0L),
REC_LONG_NAT = REC_LONG_POS.or(REC_LONG_ZERO),
REC_LONG = Recipe.oneOf(REC_LONG_NEG, REC_LONG_ZERO, REC_LONG_POS);
public static final Recipe<Double>
REC_DOUBLE_POS = Recipe.of(() -> current().nextDouble()).filter(x -> x != 0).map(Math::abs),
REC_DOUBLE_NEG = REC_DOUBLE_POS.map(x -> -x),
REC_DOUBLE_ZERO = Recipe.ofValue(0.0),
REC_DOUBLE_NAT = REC_DOUBLE_POS.or(REC_DOUBLE_ZERO),
REC_DOUBLE = Recipe.oneOf(REC_DOUBLE_NEG, REC_DOUBLE_ZERO, REC_DOUBLE_POS);
public static final Recipe<BigDecimal>
REC_BIGDEC_POS = REC_DOUBLE_POS.filter(Double::isFinite).map(BigDecimal::valueOf),
REC_BIGDEC_NEG = REC_BIGDEC_POS.map(BigDecimal::negate),
REC_BIGDEC_ZERO = Recipe.ofValue(ZERO),
REC_BIGDEC_NAT = REC_BIGDEC_POS.or(REC_BIGDEC_ZERO),
REC_BIGDEC = Recipe.oneOf(REC_BIGDEC_NEG, REC_BIGDEC_ZERO, REC_BIGDEC_POS);
public static final Recipe<Number>
REC_NUMBER_POS = Recipe.oneOf(REC_INT_POS, REC_LONG_POS, REC_DOUBLE_POS, REC_BIGDEC_POS),
REC_NUMBER_NEG = Recipe.oneOf(REC_INT_NEG, REC_LONG_NEG, REC_DOUBLE_NEG, REC_BIGDEC_NEG),
REC_NUMBER_ZERO = Recipe.oneOf(REC_INT_ZERO, REC_LONG_ZERO, REC_DOUBLE_ZERO, REC_BIGDEC_ZERO),
REC_NUMBER_NAT = Recipe.oneOf(REC_INT_NAT, REC_LONG_NAT, REC_DOUBLE_NAT, REC_BIGDEC_NAT),
REC_NUMBER = Recipe.oneOf(REC_INT, REC_LONG, REC_DOUBLE, REC_BIGDEC);
Because strings are arrays of characters, the most general approach is to start with recipes for characters. Recipe for strings can than wrap
recipe for characters to produce a stream of characters and collect that stream into a string by concatenation.
public static final List<Character>
NUMERIC = asList
('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'),
ALPHABET = asList
('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'),
WHITESPACE = asList
(' ', '\t', '\n', '\r');
public static final Recipe<Character>
REC_ALPHA_UPPER = Recipe
.ofValue(ALPHABET)
.bind(Examples::recIndex, List::get),
REC_ALPHA_LOWER = REC_ALPHA_UPPER
.map(Character::toLowerCase),
REC_ALPHA = REC_ALPHA_UPPER
.or(REC_ALPHA_LOWER),
REC_NUMERIC = Recipe
.ofValue(NUMERIC)
.bind(Examples::recIndex, List::get),
REC_ALPHANUMERIC = REC_ALPHA
.or(REC_NUMERIC),
REC_WHITESPACE = Recipe
.ofValue(WHITESPACE)
.bind(Examples::recIndex, List::get);
public static Recipe<String> recString(Supplier<? extends Character> recipe, int maxLength) {
return Recipe
.of(recipe)
.map(Object::toString)
.wrap(Stream::generate)
.bind(recfn(recInt(0, maxLength).map(x -> x + 1)), Stream::limit)
.map(s -> s.collect(joining()));
}
Method recString
accepts any recipe for characters (or any union of such recipes) and the maximum length of the produced string. Here we are generating alphanumeric text with whitespaces:
Map<Recipe<Character>, Integer> frequencies = new HashMap<>();
frequencies.put(REC_ALPHANUMERIC, 9);
frequencies.put(REC_WHITESPACE, 1);
recString(recFreq(frequencies), 500)
.map(doto(System.out::println))
.get();
Results:
Hm18Cr 98 l526de54d7T160 5QO23BaxV1Uz54539SDFU4C0p T3fs1TvV437oDT012Ny120fjIy6 E 68eZyb S7V 3wZ7G0b6W972 6Kg4y333t0l i f31W5b U6pT908yYm1P 2llro2jC8m0989Mq9869 mj05aE17z 8P9
To generate builders and withers for Java 14 records, you can use this library.
public static final Recipe<Account> REC_ACCOUNT = Recipe
.of(Account::builder)
.bind(recfn(recString(REC_ALPHANUMERIC, 10)), Account.Builder::username)
.bind(recfn(LocalDateTime::now), Account.Builder::createdAt)
.map(Account.Builder::build);
public static final Recipe<Account> REC_ACCOUNT_WITH_INVALID_ID =
REC_ACCOUNT
.bind(recfn(REC_INT_NEG), Account::withId);
public static final Recipe<Role> REC_ROLE = Recipe
.of(Role::new)
.bind(recfn(REC_UUID_STR), biFirst(Role::setName));
Constructors with one argument are functions that can be used with the method map
.
Recipe<BigDecimal> rec = Recipe.ofValue("3.14").map(BigDecimal::new);
Constructors with two arguments are binary functions that can be used with the method bind
.
Recipe<Role> rec = REC_INT_POS.bind(recfn(REC_UUID_STR), Role::new);
If the only option is a constructor with more than two arguments, generation is still possible, but ugly.
Recipe<Account> rec = Recipe
.of(() -> new Account(null,
recString(REC_ALPHANUMERIC, 10).get(),
LocalDateTime.now()));
When testing, I prefer to keep reusable recipes as static final
fields in separate, non-instantiable classes.
Recipes may depend on “injected” objects that provide logic for side effects, such as selecting data from the database, inserting new data etc. Even though dependency injection in Spring leans toward non-static fields, there is a workaround that enables injection into static fields.
// Uncomment comments below
// @org.springframework.stereotype.Component
public class Beans {
public static RoleService roleService;
public static AccountService accountService;
// @org.springframework.beans.factory.annotation.Autowired
public Beans(RoleService roleService,
AccountService accountService)
{
Beans.roleService = roleService;
Beans.accountService = accountService;
}
}
Now it is possible to use these services in static contexts.
public static final Recipe<Role> REC_ROLE_SAVED = Recipe
.ofValue(roleService.selectAll())
.bind(Examples::recIndex, List::get);
public static final Recipe<Account> REC_ACCOUNT_SAVED =
REC_ACCOUNT
.map(accountService::save);
public static final Recipe<Pair<Account, Role>> REC_ACCOUNT_WITH_ROLE =
REC_ACCOUNT_SAVED
.bind(recfn(REC_ROLE_SAVED), dotwo(accountService::addRole));
Note that REC_ROLE_SAVED
selects all the roles only once, because ofValue
is used.
REC_ACCOUNT_WITH_ROLE
.map(doto(System.out::println))
.get();
Results:
Pair[first=Account[id=1, username=a6mSFp5, createdAt=2022-07-26T04:04:52.020284], second=Role[id=2, name=editor]]
Some people warn against this workaround mostly because of possible initialization gotchas. In my experience so far, there won’t be any problems if you keep static injections, static recipe definitions and tests in separate classes.
For the source code see the LICENSE file.
You can freely copy any code from this README.org file.