Skip to content

nikolavojicic/recipe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Introduction

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.

Installation

Requires Java 8 or higher.

Maven

<dependency>
  <groupId>io.sourceforge.recipe</groupId>
  <artifactId>recipe</artifactId>
  <version>1.0.3</version>
</dependency>

Gradle

implementation 'io.sourceforge.recipe:recipe:1.0.3'

Examples

Preparation

Compile project

mvn clean compile

Add imports needed for examples

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.*;

Creating recipes

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.

Lambda expression

Supplier<String> sup = () -> "foo";
Recipe  <String> rec = () -> "foo";

System.out.println(
    sup.get().equals(rec.get())
);

Results:

true

Method reference

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

Function to Recipe

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

Reference Recipe

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.

Enum Recipe

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);

Composing recipes

Union

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

Filtering

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.

Transformation

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]

Binding

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:

Second recipe depends on values produced by the first recipe

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.

Recipes do not depend on each other

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]
Second recipe produces results of function application

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]

Wrapping

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

Flattening

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

Generating numbers

Bounded

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());
}

Without bound params

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);

Generating text

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

Generating POJOs or records

To generate builders and withers for Java 14 records, you can use this library.

Builders

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);

Withers

public static final Recipe<Account> REC_ACCOUNT_WITH_INVALID_ID =
    REC_ACCOUNT
        .bind(recfn(REC_INT_NEG), Account::withId);

Setters

public static final Recipe<Role> REC_ROLE = Recipe
    .of(Role::new)
    .bind(recfn(REC_UUID_STR), biFirst(Role::setName));

Constructors

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()));

Dependency injection

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.

License

For the source code see the LICENSE file.

You can freely copy any code from this README.org file.