io.rouz.flo.TaskContext Maven / Gradle / Ivy
package io.rouz.flo;
import static io.rouz.flo.TaskContextWithTask.withTask;
import io.rouz.flo.context.AsyncContext;
import io.rouz.flo.context.InMemImmediateContext;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A context for controlling {@link Task} evaluation and {@link Value} computation.
*/
public interface TaskContext {
Logger LOG = LoggerFactory.getLogger(TaskContext.class);
/**
* The entry point for evaluating a {@link Value} from a {@link Task}.
*
* All upstreams to the task will also be evaluated through this same method.
*
*
The implementation should define how and where evaluation happens.
*
*
This method is a good place to define how for instance value memoization happens.
*
* @param task The task to evaluate
* @param The type of the task result
* @return A value of the task result
*/
default Value evaluate(Task task) {
return evaluateInternal(task, withTask(this, task));
}
/**
* A variant of {@link #evaluate(Task)} that allows the caller to specify the {@link TaskContext}
* that should be used within the graph during evaluation.
*
* This is intended to be called from {@link TaskContext} implementations that form a
* composition of other contexts.
*
* @param task The task to evaluate
* @param context The context to use in further evaluation
* @param The type of the task result
* @return A value of the task result
*/
default Value evaluateInternal(Task task, TaskContext context) {
return task.code().eval(context);
}
/**
* Invoke the process function of a task.
*
* This method will be called when the process function of a task is ready to be invoked. This
* gives this {@link TaskContext} the responsibility of invoking user code. By overriding this
* method, one can intercept the evaluation flow just at the moment between inputs being ready
* and when the user supplied function for task processing is being invoked.
*
*
The default implementation will simply invoke the function immediately.
*
*
Interception of curried process functions will gate the innermost function, while
* for regular arity 1-3 process functions gating happens around the whole function.
*
* todo: reconsider difference between arity and curried function interception.
*
* @param taskId The id of the task being invoked
* @param processFn A lazily evaluated handle to the process function
* @param The task value type
* @return The value of the process function invocation
*/
default Value invokeProcessFn(TaskId taskId, Fn> processFn) {
return processFn.get();
}
/**
* When called from within any of the functions passed to {@code processWithContext}, this
* method will return the {@link Task} currently being processed. Otherwise an empty value
* will be returned.
*
* The return value of this method is stable for each instance of {@link TaskContext} that is
* passed into the process functions. Calls from multiple threads will see the same result as
* longs as the calls are made to the same instance.
*
* @return The task that is being evaluated or empty if called from outside of a process function
*/
default Optional> currentTask() {
return Optional.empty();
}
/**
* Create a {@link Value} with semantics defined by this {@link TaskContext}
*
* @param value A value value supplier
* @param The type of the value
* @return A value with added semantics
*/
Value value(Fn value);
/**
* Create a {@link Value} with semantics defined by this {@link TaskContext}
*
* @param value An actual value to wrap
* @param The type of the value
* @return A value with added semantics
*/
default Value immediateValue(T value) {
return value(() -> value);
}
/**
* Create a promise for a value that can be fulfilled somewhere else.
*
* @param The type of the promised value
* @return A promise
*/
Promise promise();
/**
* A wrapped value with additional semantics for how the enclosed value becomes available and
* how computations on that value are executed.
*
* Value is a Monad and the implementor should minimally need to implement
* {@link TaskContext#value(Fn)}, {@link Value#flatMap(Function)} and
* {@link Value#consume(Consumer)} to get a working context with it's associated value type.
*
* @param The enclosed type
*/
interface Value {
/**
* The {@link TaskContext} that created this value.
*
* @return The context
*/
TaskContext context();
/**
* Consume the enclosed value.
*
* @param consumer The code that should consume the value
*/
void consume(Consumer consumer);
/**
* Consume any error the occurred while constructing the enclosed value.
*
* @param errorConsumer The code that should consume the error
*/
void onFail(Consumer errorConsumer);
/**
* Map the enclosed value through a function and return a {@link Value} enclosing that result.
*
* @param fn The function to map the enclosed value through
* @param The type of the new enclosed value
* @return A new value with a different type
*/
default Value map(Function super T, ? extends U> fn) {
return flatMap(fn.andThen(context()::immediateValue));
}
/**
* Map the enclosed value through a function that return another {@link Value}.
*
* The returned other value could be from a different {@link TaskContext} so the implementor
* of this context should take care of how to bridge the value semantics to the returned value.
*
* @param fn The function to map the enclosed value through
* @param The type of the new enclosed value
* @return A new value with a different type
*/
Value flatMap(Function super T, ? extends Value extends U>> fn);
}
/**
* A promise for a {@link Value} that is supposed to be {@link #set(Object)}.
*
*
This is supposed to be used where the processing for producing a value happens in a
* different environment but is needed by values produced in this context.
*
* @param The type of the promised value
*/
interface Promise {
/**
* The value for this promise. When this promise is fulfilled, the value will become available.
*
* @return The value corresponding to this promise
*/
Value value();
/**
* Fulfill the promise.
*
* @param value The value to fulfill the promise with
* @throws IllegalStateException if the promise was already fulfilled
*/
void set(T value);
/**
* Fail the promise.
*
* @param throwable The exception that is the cause of the failure
*/
void fail(Throwable throwable);
}
/**
* Create a default, in-memory, immediate {@link TaskContext}. The values produced by this
* context should behave like synchronously created values. All graph memoization is done
* completely in memory.
*
* @return The context
*/
static TaskContext inmem() {
return InMemImmediateContext.create();
}
/**
* Create an asynchronous {@link TaskContext} that executes all evaluation on the given
* {@link Executor}.
*
* @param executor The executor to run evaluations on
* @return The asynchronous context
*/
static TaskContext async(Executor executor) {
return AsyncContext.create(executor);
}
}