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

io.rouz.task.TaskContext Maven / Gradle / Ivy

package io.rouz.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Stream;

import io.rouz.task.context.AsyncContext;
import io.rouz.task.context.InMemImmediateContext;
import io.rouz.task.dsl.TaskBuilder.F0;

/**
 * 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 task.code().eval(this);
  }

  /**
   * 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, F0> processFn) {
    return processFn.get();
  }

  /**
   * 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(F0 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 {@link Collector} that collects a {@link Stream} of {@link Value}s into a {@link Value}
   * of a {@link List}.
   *
   * The semantics of joining {@link Value}s is decided by this {@link TaskContext}.
   */
  default  Collector, ?, Value>> toValueList() {
    return Collector.of(
        ArrayList::new, List::add, (a,b) -> { a.addAll(b); return a; },
        ValueFold.inContext(this));
  }

  /**
   * 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(F0)}, {@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);

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

    // todo: set error
  }

  /**
   * 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 ExecutorService}.
   *
   * @param executorService  The executor service to run evaluations on
   * @return The asynchronous context
   */
  static TaskContext async(ExecutorService executorService) {
    return AsyncContext.create(executorService);
  }
}