All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hydra.Flows Maven / Gradle / Ivy

package hydra;

import hydra.compute.Flow;
import hydra.compute.FlowState;
import hydra.compute.Trace;
import hydra.core.Unit;
import hydra.tier1.Tier1;
import hydra.tools.FlowException;
import hydra.tools.Function3;
import hydra.tools.Function4;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;


/**
 * A collection of convenience methods for constructing and composing flows, or stateful computations.
 */
public interface Flows {
    Trace EMPTY_TRACE
            = new Trace(Collections.emptyList(), Collections.emptyList(), Collections.emptyMap());

    /**
     * Apply a function flow to a domain value flow.
     */
    static  Flow apply(Flow> mapping, Flow input) {
        return new Flow<>(s0 -> t0 -> {
            FlowState> fs1 = mapping.value.apply(s0).apply(t0);
            Optional> mf = fs1.value;
            return mf.isPresent()
                    ? map(mf.get(), input).value.apply(fs1.state).apply(fs1.trace)
                    : new FlowState<>(Optional.empty(), fs1.state, fs1.trace);
        });
    }

    /**
     * Monadic bind function for flows.
     */
    static  Flow bind(Flow p, Function> f) {
        return new Flow<>(s0 -> t0 -> {
            FlowState fs1 = p.value.apply(s0).apply(t0);
            Optional a = fs1.value;
            return a.isPresent()
                    ? f.apply(a.get()).value.apply(fs1.state).apply(fs1.trace)
                    : new FlowState<>(Optional.empty(), fs1.state, fs1.trace);
        });
    }

    /**
     * Monadic bind with reversed arguments.
     */
    static  Flow bind(Function> f, Flow p) {
        return bind(f, p);
    }

    /**
     * Variant of monadic bind which takes two monadic arguments and a binary function.
     */
    static  Flow bind2(Flow p1, Flow p2, BiFunction> f) {
        return Flows.bind(p1, a -> Flows.bind(p2, b -> f.apply(a, b)));
    }

    /**
     * Two-argument monadic bind with reversed arguments.
     */
    static  Flow bind2(BiFunction> f, Flow p1, Flow p2) {
        return Flows.bind2(p1, p2, f);
    }

    /**
     * Variant of monadic bind which takes three monadic arguments and an arity-3 function.
     */
    static  Flow bind3(Flow p1,
                                            Flow p2,
                                            Flow p3,
                                            Function3> f) {
        return Flows.bind(p1, a -> Flows.bind2(p2, p3, (b, c) -> f.apply(a, b, c)));
    }

    /**
     * Three-argument monadic bind with reversed arguments.
     */
    static  Flow bind3(Function3> f,
                                            Flow p1,
                                            Flow p2,
                                            Flow p3) {
        return Flows.bind3(p1, p2, p3, f);
    }

    /**
     * Check whether a given value satisfies a list of predicates, returning the value itself if all checks are
     * successful, or a failure flow for the first predicate that fails.
     */
    static  Flow check(A input, Function>... predicates) {
        for (Function> predicate : predicates) {
            Optional msg = predicate.apply(input);
            if (msg.isPresent()) {
                return Flows.fail(msg.get());
            }
        }

        return pure(input);
    }

    /**
     * Compose two monadic functions, feeding the output of the first into the second.
     */
    static  Function> compose(Function> f, Function> g) {
        return a -> Flows.bind(f.apply(a), g);
    }

    /**
     * Evaluate a flow and consume the result.
     */
    static  Flow consume(Flow x, Consumer consumer) {
        return map(x, a -> {
            consumer.accept(a);
            return new Unit();
        });
    }

    /**
     * Produce a failure flow with the provided message.
     */
    static  Flow fail(String msg) {
        return new Flow<>(s -> trace -> {
            String errMsg = "Error: " + msg; // TODO: include stack trace
            List messages = new ArrayList<>(trace.messages);
            messages.add(errMsg);
            return new FlowState<>(Optional.empty(), s, trace.withMessages(messages));
        });
    }

    /**
     * Produce a failure flow with the provided message and additional information from a Throwable.
     */
    static  Flow fail(String msg, Throwable cause) {
        return fail(msg + ": " + cause.getMessage());
    }

    /**
     * Extract the value from a flow, returning a default value instead if the flow failed.
     */
    static  A fromFlow(A dflt, S state, Flow flow) {
        Function, A>> helper = Tier1.fromFlow(dflt);
        return helper.apply(state).apply(flow);
    }

    /**
     * Extract the value from a flow, throwing an exception if the flow failed.
     */
    static  A fromFlow(S state, Flow flow) throws FlowException {
        FlowState result = flow.value.apply(state).apply(EMPTY_TRACE);
        if (result.value.isPresent()) {
            return result.value.get();
        } else {
            throw new FlowException(result.trace);
        }
    }

    /**
     * Extract the value from a stateless flow, throwing an exception if the flow failed.
     */
    static  A fromFlow(Flow flow) throws FlowException {
        return fromFlow(new Unit(), flow);
    }

    /**
     * Extract the state from a flow.
     */
    static  Flow getState() {
        return new Flow<>(s0 -> t0 -> new FlowState<>(Optional.of(s0), s0, t0));
    }

    /**
     * Map a function over a flow.
     */
    static  Flow map(Function f, Flow x) {
        return new Flow<>(s -> trace -> {
            FlowState result = x.value.apply(s).apply(trace);
            return new FlowState<>(result.value.map(f), result.state, result.trace);
        });
    }

    /**
     * Map a function over a flow, with reversed arguments.
     */
    static  Flow map(Flow x, Function f) {
        return map(f, x);
    }

    /**
     * Map a monadic function over a list, producing a flow of lists.
     */
    static  Flow> mapM(List as, Function> f) {
        Flow> result = pure(new ArrayList<>());
        for (A a : as) {
            result = bind(result, ys -> map(f.apply(a), b -> {
                ys.add(b); // Modify in place
                return ys;
            }));
        }
        return result;
    }

    /**
     * Map a monadic function over an array, producing a flow of lists.
     */
    static  Flow> mapM(A[] xs, Function> f) {
        return mapM(Arrays.asList(xs), f);
    }

    /**
     * Map a monadic function over the keys of a map, and another monadic function over the values of a map,
     * producing a flow of maps.
     */
    static  Flow> mapM(Map xs,
                                                         Function> kf,
                                                         Function> vf) {
        Set> entries1 = xs.entrySet();
        Flow>> entries2 = mapM(entries1,
                e -> bind(kf.apply(e.getKey()), k2 -> map(vf.apply(e.getValue()),
                        v2 -> new AbstractMap.SimpleEntry<>(k2, v2))));
        return map(entries2, entries -> entries.stream()
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
    }

    /**
     * Map a monadic function over an optional value, producing a flow of optionals.
     */
    static  Flow> mapM(Optional xs, Function> f) {
        return xs.map(a -> map(f.apply(a), Optional::of)).orElseGet(() -> pure(Optional.empty()));
    }

    /**
     * Map a monadic function over a set, producing a flow of sets.
     */
    static  Flow> mapM(Set as, Function> f) {
        Flow> result = pure(new HashSet<>(as.size()));
        for (A a : as) {
            result = bind(result, ys -> map(f.apply(a), b -> {
                ys.add(b); // Modify in place
                return ys;
            }));
        }
        return result;
    }

    /**
     * Map a bifunction over two flows, producing a flow.
     */
    static  Flow map2(Flow x, Flow y, BiFunction f) {
        return Flows.bind(x, a1 -> Flows.bind(y, b1 -> Flows.pure(f.apply(a1, b1))));
    }

    /**
     * Map an arity-3 function over three flows, producing a flow.
     */
    static  Flow map3(Flow a, Flow b, Flow c, Function3 f) {
        return Flows.bind(a,
                a1 -> Flows.bind(b,
                        b1 -> Flows.bind(c,
                                c1 -> Flows.pure(f.apply(a1, b1, c1)))));
    }

    /**
     * Map an arity-4 function over four flows, producing a flow.
     */
    static  Flow map4(Flow a,
                                              Flow b,
                                              Flow c,
                                              Flow d,
                                              Function4 f) {
        return Flows.bind(a,
                a1 -> Flows.bind(b,
                        b1 -> Flows.bind(c,
                                c1 -> Flows.bind(d,
                                        d1 -> Flows.pure(f.apply(a1, b1, c1, d1))))));
    }

    /**
     * Produce a given object as a pure flow; the value is guaranteed to be present,
     * and neither state nor trace are modified.
     */
    static  Flow pure(A obj) {
        return new Flow<>(s -> trace -> new FlowState<>(Optional.of(obj), s, trace));
    }

    /**
     * Modify the state of a flow.
     */
    static  Flow putState(S snew) {
        // Note: for lack of a unit value other than null,
        // we use use a boolean as the ignorable value output of putState()
        return new Flow<>(s0 -> t0 -> new FlowState<>(Optional.of(true), snew, t0));
    }

    /**
     * Test an optional value, producing a flow with the value if present, or an error flow if absent.
     */
    static  Flow require(Optional optValue, String category) {
        return optValue.isPresent()
                ? Flows.pure(optValue.get())
                : Flows.fail("require " + category + " is missing/null");
    }

    /**
     * Evaluate each flow from left to right, and produce a flow of the resulting list.
     * Analogous to the sequence function in Haskell.
     */
    static  Flow> sequence(List> elements) {
        Flow> result = Flows.pure(new ArrayList<>(elements.size()));
        for (Flow element : elements) {
            result = Flows.bind(result, xs -> Flows.map(element, x -> {
                xs.add(x); // Modify in place
                return xs;
            }));
        }
        return result;
    }

    /**
     * Produce an error flow indicating an unexpected value.
     * For example, if you expect a string but find an integer, use unexpected("string", myInt).
     */
    static  Flow unexpected(String cat, Object obj) {
        return fail("expected " + cat + " but found: " + obj);
    }

    /**
     * Produce an error flow indicating an unexpected class of value.
     */
    static  Flow unexpectedClass(String cat, Object obj) {
        return fail("expected " + cat + " but found an instance of " + obj.getClass().getName());
    }

    /**
     * Continue a flow after adding a warning message.
     */
    static  Flow warn(String message, Flow flow) {
        Function, Flow> f = Tier1.warn(message);
        return f.apply(flow);
    }
}