Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Branching when composing lambdas from other lambdas

I'm trying to experiment with lambdas for fun. I created a functor which allows the composition of a lambda. But, the means of composition only allow a linear transformation, and does not allow branching.

The idea is that I know I will have, in the future, an effectively immutable state data structure. I want to compose a transform that will extract a value from the state; and will perform a series of steps, which may or may not require the state, to perform the transform.

To this end, I create two classes. The functional interface which works like java.util.function.Function, but takes a BiFunction in the andThen method, which allows the state parameter to be passed from lambda to lambda.

import java.util.Objects;
import java.util.function.BiFunction;

@FunctionalInterface
public interface Procedure<S, T> {

    T procede(S stateStructure);

    default <R> Procedure<S, R> andThen(BiFunction<S, T, R> after) {
        Objects.requireNonNull(after);
        return (param) -> after.apply(param, procede(param));
    }
}

The functor is fairly straightforward, having two mapping functions (one which utilizes the state, and one which doesn't), and two terminating methods that finalize the transformations (again, with and without state).

import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

public class ProcedureContainer<S, T> {

    protected final Procedure<S, T> procedure;

    protected ProcedureContainer(final Procedure<S, T> procedure) {
        this.procedure = procedure;
    }

    public static <S, R> ProcedureContainer<S, R> initializeContainer(
            final Function<S, R> initialDataRetriever) {

        return new ProcedureContainer<>(initialDataRetriever::apply);
    }

    public <R> ProcedureContainer<S, R> map(final BiFunction<S, T, R> mapper) {
        return new ProcedureContainer<>(procedure.andThen(mapper));
    }

    public <R> ProcedureContainer<S, R> map(final Function<T, R> mapper) {
        BiFunction<S, T, R> subMapper =
                (ignored, stagedData) -> mapper.apply(stagedData);
        return new ProcedureContainer<>(procedure.andThen(subMapper));
    }

    public Consumer<S> terminate(final BiConsumer<S, T> consumer) {
        return (param) -> consumer.accept(param, procedure.procede(param));
    }

    public Consumer<S> terminate(final Consumer<T> consumer) {
        return (param) -> consumer.accept(procedure.procede(param));
    }
}

A brief (contrived) example:

StateStruct state = new StateStruct();
state.setJson("{\"data\":\"meow, meow, I'm a cow\"}");
state.setRequestedField("data");

Consumer<StateStruct> consumer = ProcedureContainer
    .initializeContainer(SateStruct::getJson)
    .map(JSONObject::new)
    .map((state, jsonObj) -> jsonObject.getString(state.getRequsetedField()))
    .terminate(System.out::singLoudly);

consumer.accept(state);

Does anyone have any ideas of how I could implement a branch method on the ProcedureContainer that would allow a conditional branch in the execution of the final consumer. I'm thinking something that would make this example work:

StateStruct state = new StateStruct();
state.setJson("{\"data\":\"meow, meow, I'm a cow\"}");
state.setRequestedField("data");
state.setDefaultMessage("There is no data... only sheep");

Consumer<StateStruct> consumer = ProcedureContainer
    .initializeContainer(SateStruct::getJson)
    .map(JSONObject::new)

    .branch((state, jsonObj) -> !jsonObject.getString(state.getRequsetedField()))
    .terminateBranch((state, json) -> System.out.lament(state.getDefaultMessage()))

    .map((state, jsonObj) -> jsonObject.getString(state.getRequsetedField()))
    .terminate(System.out::singLoudly);

consumer.accept(state);

I've attempted by creating a new BranchProcedureContainer, which has a map and terminateBranch method. This issue is that I don't know how to merge the two branches in such a way that only the branch gets run.

There are no restrictions on creating new classes, or adding methods to existing classes.

like image 848
JRogerC Avatar asked May 27 '17 17:05

JRogerC


1 Answers

I was able to put a solution together. But, I don't find it particularly elegant. So, please feel free to submit other solutions (or more ingenuitive version of this solution).

I originally tried create a state container, which contained a boolean stating whether a specific branch was used or not. This was a no-go as the state isn't passed around correctly. So, instead I created a value container:

class ValueContainer<T> {

    private final T value;
    private final Boolean terminated;

    private ValueContainer(final T value, final Boolean terminated) {
        this.value = value;
        this.terminated = terminated;
    }

    public static <T> ValueContainer<T> of(final T value) {
        return new ValueContainer<>(value, false);
    }

    public static <T> ValueContainer<T> terminated() {
        return new ValueContainer<>((T) null, true);
    }

    //...getters
}

I then rewrote the functional interface to make use of the new container:

@FunctionalInterface
public interface Procedure<S, T> {

    ValueContainer<T> procede(S stateStructure);
}

With the addition of the ValueContainer, I didn't want the user to have unwrap the value for each method. Therefore, the default method was extended to account for the container. In addition, logic was added to deal with the case where the procedure is part of an unused/terminated branch.

default <R> Procedure<S, R> andThen(BiFunction<S, T, R> after) {
    Objects.requireNonNull(after);
    return (param) -> {
        ValueContainer<T> intermediateValue = procede(param);
        if (intermediateValue.isTerminated())
            return ValueContainer.<R>terminated();
        R returnValue = after.apply(param, intermediateValue.getValue());
        return ValueContainer.of(returnValue);
    };
}

From there, I just had to expand the ProcedureContainer to have a branch method; and reworked the terminate method to account for the ValueContainer and the terminated-branch case. (The code below leaves off overloaded methods)

public class ProcedureContainer<S, T> {

    protected final Procedure<S, T> procedure;

    protected ProcedureContainer(final Procedure<S, T> procedure) {
        this.procedure = procedure;
    }

    public static <S, R> ProcedureContainer<S, R> initializeContainer(
            final Function<S, R> initialDataRetriever) {

        Procedure<S, R> initializer = (paramContainer) -> {
            R initialValue = initialDataRetriever.apply(paramContainer);
            return ValueContainer.of(initialValue);
        };
        return new ProcedureContainer<>(initializer);
    }

    public <R> ProcedureContainer<S, R> map(final BiFunction<S, T, R> mapper) {
        return new ProcedureContainer<>(procedure.andThen(mapper));
    }

    public BranchProcedureContainer<S, T, T> branch(final BiPredicate<S, T> predicate) {

        return BranchProcedureContainer.branch(procedure, predicate);
    }

    public Consumer<S> terminate(final BiConsumer<S, T> consumer) {
        return (param) -> {
            ValueContainer<T> finalValue = procedure.procede(param);
            if (finalValue.isTerminated())
                return;

            consumer.accept(param, finalValue.getValue());
        };
    }
}

The branch method returns a new class, BranchProcedureContainer, which handles building the branch. This class has an endBranch method which ties back into a new instance of ProcedureContainer. (Again, overloaded methods are left off)

public class BranchProcedureContainer<S, T, R> {

    private final Procedure<S, T> baseProcedure;
    private final BiPredicate<S, T> predicate;
    private final BiFunction<S, ValueContainer<T>, ValueContainer<R>> branchProcedure;

    private BranchProcedureContainer(
            final Procedure<S, T> baseProcedure,
            final BiPredicate<S, T> predicate,
            final BiFunction<S, ValueContainer<T>, ValueContainer<R>> branchProcedure) {

        this.baseProcedure = baseProcedure;
        this.predicate = predicate;
        this.branchProcedure = branchProcedure;
    }

    protected static <S, T> BranchProcedureContainer<S, T, T> branch(
            final Procedure<S, T> baseProcedure,
            final BiPredicate<S, T> predicate) {

        return new BranchProcedureContainer<>(baseProcedure, predicate, (s, v) -> v);
    }

    public <RR> BranchProcedureContainer<S, T, RR> map(
            final BiFunction<S, R, RR> mapper) {

        BiFunction<S, ValueContainer<T>, ValueContainer<RR>> fullMapper = (s, vT) -> {
            if (vT.isTerminated())
                return ValueContainer.<RR>terminated();

            ValueContainer<R> intermediateValue = branchProcedure.apply(s, vT);
            if (intermediateValue.isTerminated())
                return ValueContainer.<RR>terminated();

            RR finalValue = mapper.apply(s, intermediateValue.getValue());
            return ValueContainer.of(finalValue);
        };
        return new BranchProcedureContainer<>(baseProcedure, predicate, fullMapper);
    }

    public ProcedureContainer<S, T> endBranch(final BiConsumer<S, R> consumer) {

        Procedure<S, T> mergedBranch = (state) -> {
            ValueContainer<T> startingPoint = baseProcedure.procede(state);
            if (startingPoint.isTerminated())
                return ValueContainer.<T>terminated();

            if (!predicate.test(state, startingPoint.getValue()))
                return startingPoint;

            ValueContainer<R> intermediateValue = branchProcedure.apply(state, startingPoint);
            if (intermediateValue.isTerminated())
                return ValueContainer.<T>terminated();
            consumer.accept(state, intermediateValue.getValue());
            return ValueContainer.<T>terminated();
        };

        return new ProcedureContainer<>(mergedBranch);
    }
}

The one problem I see with this approach (though I'm sure there are many) is the repeated call to determine whether a branch is terminated or not. It would be nice if that check was only done at the branching point.

The full code can be found my github page.

NOTE: I understand that I've used 'terminate' to both designate when a branch is completed, and when a branch is never run. I'm still trying to think of a better naming convention. Open to suggestions.

like image 190
JRogerC Avatar answered Nov 15 '22 19:11

JRogerC