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

com.github.tonivade.purefun.effect.PureIO Maven / Gradle / Ivy

/*
 * Copyright (c) 2018-2021, Antonio Gabriel Muñoz Conejo 
 * Distributed under the terms of the MIT License
 */
package com.github.tonivade.purefun.effect;

import static com.github.tonivade.purefun.Function1.identity;
import static com.github.tonivade.purefun.Function2.first;
import static com.github.tonivade.purefun.Function2.second;
import static com.github.tonivade.purefun.Matcher1.always;
import static com.github.tonivade.purefun.Precondition.checkNonNull;
import static com.github.tonivade.purefun.Producer.cons;

import java.time.Duration;
import java.util.Deque;
import java.util.LinkedList;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;

import com.github.tonivade.purefun.CheckedRunnable;
import com.github.tonivade.purefun.Consumer1;
import com.github.tonivade.purefun.Consumer2;
import com.github.tonivade.purefun.Effect;
import com.github.tonivade.purefun.Function1;
import com.github.tonivade.purefun.Function2;
import com.github.tonivade.purefun.HigherKind;
import com.github.tonivade.purefun.Kind;
import com.github.tonivade.purefun.Nothing;
import com.github.tonivade.purefun.Operator1;
import com.github.tonivade.purefun.PartialFunction1;
import com.github.tonivade.purefun.Producer;
import com.github.tonivade.purefun.Recoverable;
import com.github.tonivade.purefun.Tuple;
import com.github.tonivade.purefun.Tuple2;
import com.github.tonivade.purefun.Unit;
import com.github.tonivade.purefun.concurrent.Future;
import com.github.tonivade.purefun.concurrent.Promise;
import com.github.tonivade.purefun.data.ImmutableList;
import com.github.tonivade.purefun.data.Sequence;
import com.github.tonivade.purefun.type.Either;
import com.github.tonivade.purefun.type.EitherOf;
import com.github.tonivade.purefun.type.Option;
import com.github.tonivade.purefun.type.Try;
import com.github.tonivade.purefun.typeclasses.Fiber;

@HigherKind(sealed = true)
public interface PureIO extends PureIOOf, Effect, E>, A> {

  default Either provide(R env) {
    return runAsync(env).getOrElseThrow();
  }

  default Future> runAsync(R env) {
    return Future.from(ZIOModule.runAsync(env, this, ZIOConnection.UNCANCELLABLE));
  }

  default Future> runAsync(R env, Executor executor) {
    return PureIO.forked(executor).andThen(this).runAsync(env);
  }

  default void provideAsync(R env, Consumer1>> callback) {
    runAsync(env).onComplete(callback::accept);
  }

  default PureIO swap() {
    return foldM(PureIO::pure, PureIO::raiseError);
  }

  @Override
  default  PureIO map(Function1 map) {
    return flatMap(map.andThen(PureIO::pure));
  }

  default  PureIO mapError(Function1 map) {
    return flatMapError(map.andThen(PureIO::raiseError));
  }

  default  PureIO bimap(Function1 mapError, Function1 map) {
    return foldM(mapError.andThen(PureIO::raiseError), map.andThen(PureIO::pure));
  }

  @Override
  default  PureIO flatMap(Function1, E>, ? extends B>> map) {
    return foldM(PureIO::raiseError, map.andThen(PureIOOf::narrowK));
  }

  default  PureIO flatMapError(Function1, F>, ? extends A>> map) {
    return foldM(map, PureIO::pure);
  }

  default  PureIO foldM(
      Function1, F>, ? extends B>> left, 
      Function1, F>, ? extends B>> right) {
    return new ZIOModule.FlatMapped<>(this, left.andThen(PureIOOf::narrowK), right.andThen(PureIOOf::narrowK));
  }

  @Override
  default  PureIO andThen(Kind, E>, ? extends B> next) {
    return flatMap(ignore -> next);
  }

  @Override
  default  PureIO ap(Kind, E>, Function1> apply) {
    return parMap2(this, apply.fix(PureIOOf.toPureIO()), (v, a) -> a.apply(v));
  }

  default  URIO fold(
      Function1 mapError, Function1 map) {
    return new URIO<>(foldM(mapError.andThen(PureIO::pure), map.andThen(PureIO::pure)));
  }

  default URIO recover(Function1 mapError) {
    return fold(mapError, identity());
  }

  default PureIO orElse(Kind, E>, ? extends A> other) {
    return foldM(Function1.cons(other), Function1.cons(this));
  }
  
  @Override
  default  PureIO> zip(Kind, E>, ? extends B> other) {
    return zipWith(other, Tuple::of);
  }
  
  @Override
  default  PureIO zipLeft(Kind, E>, ? extends B> other) {
    return zipWith(other, first());
  }
  
  @Override
  default  PureIO zipRight(Kind, E>, ? extends B> other) {
    return zipWith(other, second());
  }
  
  @Override
  default  PureIO zipWith(Kind, E>, ? extends B> other, 
      Function2 mapper) {
    return parMap2(this, other.fix(PureIOOf.toPureIO()), mapper);
  }

  @Override
  default PureIO repeat() {
    return repeat(1);
  }

  @Override
  default PureIO repeat(int times) {
    return repeat(Schedule.recurs(times).zipRight(Schedule.identity()));
  }

  @Override
  default PureIO repeat(Duration delay) {
    return repeat(delay, 1);
  }

  @Override
  default PureIO repeat(Duration delay, int times) {
    return repeat(Schedule.recursSpaced(delay, times).zipRight(Schedule.identity()));
  }
  
  default  PureIO repeat(Schedule schedule) {
    return repeatOrElse(schedule, (e, b) -> raiseError(e));
  }
  
  default  PureIO repeatOrElse(
      Schedule schedule,
      Function2, Kind, E>, B>> orElse) {
    return repeatOrElseEither(schedule, orElse).map(Either::merge);
  }

  default  PureIO> repeatOrElseEither(
      Schedule schedule,
      Function2, Kind, E>, C>> orElse) {
    return new Repeat<>(this, schedule, orElse.andThen(PureIOOf::narrowK)).run();
  }

  @Override
  default PureIO retry() {
    return retry(1);
  }

  @Override
  default PureIO retry(int maxRetries) {
    return retry(Schedule.recurs(maxRetries));
  }

  @Override
  default PureIO retry(Duration delay) {
    return retry(delay, 1);
  }

  @Override
  default PureIO retry(Duration delay, int maxRetries) {
    return retry(Schedule.recursSpaced(delay, maxRetries));
  }
  
  default  PureIO retry(Schedule schedule) {
    return retryOrElse(schedule, (e, b) -> raiseError(e));
  }

  default  PureIO retryOrElse(
      Schedule schedule,
      Function2, E>, A>> orElse) {
    return retryOrElseEither(schedule, orElse).map(Either::merge);
  }

  default  PureIO> retryOrElseEither(
      Schedule schedule,
      Function2, E>, B>> orElse) {
    return new Retry<>(this, schedule, orElse.andThen(PureIOOf::narrowK)).run();
  }

  @Override
  default PureIO> timed() {
    return PureIO.later(System::nanoTime).flatMap(
      start -> map(result -> Tuple.of(Duration.ofNanos(System.nanoTime() - start), result)));
  }
  
  default PureIO, E>, A>> fork() {
    return async((env, callback) -> {
      ZIOConnection connection = ZIOConnection.cancellable();
      Promise> promise = ZIOModule.runAsync(env, this, connection);
      
      PureIO join = fromPromise(promise);
      PureIO cancel = run(connection::cancel);
      
      callback.accept(Try.success(Either.right(Fiber.of(join, cancel))));
    });
  }

  default PureIO timeout(Duration duration) {
    return timeout(Future.DEFAULT_EXECUTOR, duration);
  }
  
  default PureIO timeout(Executor executor, Duration duration) {
    return racePair(executor, this, sleep(duration)).flatMap(either -> either.fold(
        ta -> ta.get2().cancel().fix(PureIOOf.toPureIO()).map(x -> ta.get1()),
        tb -> tb.get1().cancel().fix(PureIOOf.toPureIO()).flatMap(x -> PureIO.throwError(new TimeoutException()))));
  }
  
  @SuppressWarnings("unchecked")
  default  PureIO refineOrDie(Class type) {
    return flatMapError(error -> {
      if (type.isAssignableFrom(error.getClass())) {
        return PureIO.raiseError((X) error);
      }
      return PureIO.throwError(new ClassCastException(error.getClass() + " not asignable to " + type));
    });
  }
  
  default URIO toURIO() {
    return new URIO<>(mapError(error -> {
      if (error instanceof Throwable) {
        throw (Throwable) error;
      }
      throw new ClassCastException(error.getClass() + " is not throwable");
    }));
  }
  
  default RIO toRIO() {
    return new RIO<>(refineOrDie(Throwable.class));
  }
  
  default Managed toManaged() {
    return Managed.pure(this);
  }
  
  default Managed toManaged(Consumer1 release) {
    return Managed.from(this, release);
  }

  static  PureIO accessM(Function1, E>, ? extends A>> map) {
    return new ZIOModule.AccessM<>(map.andThen(PureIOOf::narrowK));
  }

  static  PureIO access(Function1 map) {
    return accessM(map.andThen(PureIO::pure));
  }

  static  PureIO env() {
    return access(identity());
  }
  
  static  PureIO forked(Executor executor) {
    return async((env, callback) -> executor.execute(() -> callback.accept(Try.success(Either.right(Unit.unit())))));
  }

  static  PureIO parMap2(Kind, E>, ? extends A> za, Kind, E>, ? extends B> zb, 
      Function2 mapper) {
    return parMap2(Future.DEFAULT_EXECUTOR, za, zb, mapper);
  }

  static  PureIO parMap2(Executor executor, Kind, E>, ? extends A> za, Kind, E>, ? extends B> zb, 
      Function2 mapper) {
    return cancellable((env, callback) -> {
      
      ZIOConnection connection1 = ZIOConnection.cancellable();
      ZIOConnection connection2 = ZIOConnection.cancellable();
      
      Promise> promiseA = ZIOModule.runAsync(env, PureIO.forked(executor).andThen(za), connection1);
      Promise> promiseB = ZIOModule.runAsync(env, PureIO.forked(executor).andThen(zb), connection2);
      
      promiseA.onComplete(a -> promiseB.onComplete(
        b -> callback.accept(Try.map2(a, b, (e1, e2) -> EitherOf.narrowK(Either.map2(e1, e2, mapper))))));
      
      return PureIO.exec(() -> {
        try {
          connection1.cancel();
        } finally {
          connection2.cancel();
        }
      });
    });
  }
  
  static  PureIO> race(Kind, E>, ? extends A> fa, Kind, E>, ? extends B> fb) {
    return race(Future.DEFAULT_EXECUTOR, fa, fb);
  }
  
  static  PureIO> race(Executor executor, Kind, E>, ? extends A> fa, Kind, E>, ? extends B> fb) {
    return racePair(executor, fa, fb).flatMap(either -> either.fold(
        ta -> ta.get2().cancel().fix(PureIOOf.toPureIO()).map(x -> Either.left(ta.get1())),
        tb -> tb.get1().cancel().fix(PureIOOf.toPureIO()).map(x -> Either.right(tb.get2()))));
  }
  
  static  PureIO, E>, B>>, Tuple2, E>, A>, B>>> 
      racePair(Executor executor, Kind, E>, ? extends A> fa, Kind, E>, ? extends B> fb) {
    return cancellable((env, callback) -> {
      
      ZIOConnection connection1 = ZIOConnection.cancellable();
      ZIOConnection connection2 = ZIOConnection.cancellable();
      
      Promise> promiseA = ZIOModule.runAsync(env, PureIO.forked(executor).andThen(fa), connection1);
      Promise> promiseB = ZIOModule.runAsync(env, PureIO.forked(executor).andThen(fb), connection2);
      
      promiseA.onComplete(result -> {
        PureIO fromPromiseB = PureIO.fromPromise(promiseB);
        PureIO cancelB = PureIO.run(connection2::cancel);
        Fiber, E>, B> fiberB = Fiber.of(fromPromiseB, cancelB);
        callback.accept(result.map(
          either -> either.map(
            a -> Either., E>, B>>, Tuple2, E>, A>, B>>left(Tuple.of(a, fiberB)))));
      });
      
      promiseB.onComplete(result -> {
        PureIO fromPromiseA = PureIO.fromPromise(promiseA);
        PureIO cancelA = PureIO.run(connection2::cancel);
        Fiber, E>, A> fiberA = Fiber.of(fromPromiseA, cancelA);
        callback.accept(result.map(
          either -> either.map(
            b -> Either., E>, B>>, Tuple2, E>, A>, B>>right(Tuple.of(fiberA, b)))));
      });

      return PureIO.exec(() -> {
        try {
          connection1.cancel();
        } finally {
          connection2.cancel();
        }
      });
    });
  }

  static  PureIO absorb(Kind, E>, Either> value) {
    return value.fix(PureIOOf::narrowK).flatMap(either -> either.fold(PureIO::raiseError, PureIO::pure));
  }

  static  Function1> lift(Function1 function) {
    return value -> pure(function.apply(value));
  }

  static  Function1> liftOption(Function1> function) {
    return value -> fromOption(function.apply(value));
  }

  static  Function1> liftTry(Function1> function) {
    return value -> fromTry(function.apply(value));
  }

  static  Function1> liftEither(Function1> function) {
    return value -> fromEither(function.apply(value));
  }

  static  PureIO fromOption(Option task) {
    return fromOption(cons(task));
  }

  static  PureIO fromOption(Producer> task) {
    return fromEither(task.andThen(Option::toEither));
  }

  static  PureIO fromTry(Try task) {
    return fromTry(cons(task));
  }

  static  PureIO fromTry(Producer> task) {
    return fromEither(task.andThen(Try::toEither));
  }

  static  PureIO fromEither(Either task) {
    return fromEither(cons(task));
  }

  static  PureIO fromEither(Producer> task) {
    return new ZIOModule.Delay<>(task);
  }
  
  static  PureIO fromPromise(Promise> promise) {
    Consumer1>>> callback = promise::onComplete;
    return async((env, cb) -> callback.accept(cb));
  }

  static  PureIO exec(CheckedRunnable task) {
    return new ZIOModule.Attempt<>(task.asProducer());
  }

  static  PureIO run(Runnable task) {
    return fromEither(() -> { task.run(); return Either.right(Unit.unit()); });
  }

  static  PureIO pure(A value) {
    return new ZIOModule.Pure<>(value);
  }

  static  PureIO defer(Producer, E>, ? extends A>> lazy) {
    return new ZIOModule.Suspend<>(lazy.andThen(PureIOOf::narrowK));
  }

  static  PureIO task(Producer task) {
    return new ZIOModule.Attempt<>(task);
  }

  static  PureIO later(Producer task) {
    return fromEither(task.andThen(Either::right));
  }
  
  static  PureIO never() {
    return async((env, cb) -> {});
  }
  
  static  PureIO async(Consumer2>>> consumer) {
    return cancellable(consumer.asFunction().andThen(PureIO::pure));
  }
  
  static  PureIO cancellable(Function2>>, PureIO> consumer) {
    return new ZIOModule.Async<>(consumer);
  }

  static  PureIO raiseError(E error) {
    return new ZIOModule.Failure<>(error);
  }

  static  PureIO throwError(Throwable error) {
    return new ZIOModule.Throw<>(error);
  }

  static  PureIO redeem(Kind, Nothing>, ? extends A> value) {
    return new ZIOModule.Recover<>(value.fix(PureIOOf::narrowK), PartialFunction1.of(always(), PureIO::raiseError));
  }

  static  PureIO sleep(Duration delay) {
    return cancellable((env, callback) -> {
      Future sleep = Future.sleep(delay)
        .onComplete(result -> callback.accept(Try.success(Either.right(Unit.unit()))));
      return PureIO.exec(() -> sleep.cancel(true));
    });  
  }

  static  PureIO> traverse(Sequence, E>, A>> sequence) {
    return traverse(Future.DEFAULT_EXECUTOR, sequence);
  }

  static  PureIO> traverse(Executor executor, Sequence, E>, A>> sequence) {
    return sequence.foldLeft(pure(ImmutableList.empty()), 
        (Kind, E>, Sequence> xs, Kind, E>, A> a) -> parMap2(executor, xs, a, Sequence::append));
  }

  static  PureIO bracket(Kind, E>, ? extends A> acquire,
                                                                 Function1, E>, ? extends B>> use) {
    return bracket(acquire, use, AutoCloseable::close);
  }

  static  PureIO bracket(Kind, E>, ? extends A> acquire,
                                           Function1, E>, ? extends B>> use,
                                           Consumer1 release) {
    return bracket(acquire, use, release.asFunction().andThen(PureIO::pure));
  }

  static  PureIO bracket(Kind, E>, ? extends A> acquire,
                                           Function1, E>, ? extends B>> use,
                                           Function1, E>, Unit>> release) {
    // TODO: cancel
    return cancellable((env, callback) -> {
      
      ZIOConnection cancellable = ZIOConnection.cancellable();
      
      Promise> promise = ZIOModule.runAsync(env, acquire.fix(PureIOOf::narrowK), cancellable);
      
      promise
        .onFailure(e -> callback.accept(Try.failure(e)))
        .onSuccess(either -> either.fold(error -> {
          callback.accept(Try.success(Either.left(error)));
          return Unit.unit();
        }, resource -> {
          Function1> andThen = use.andThen(PureIOOf::narrowK);
          Promise> runAsync = ZIOModule.runAsync(env, andThen.apply(resource), cancellable);
          
          runAsync
            .onFailure(e -> callback.accept(Try.failure(e)))
            .onSuccess(result -> {

              Promise> run = ZIOModule.runAsync(env, release.andThen(PureIOOf::narrowK).apply(resource), cancellable);
              
              run.onComplete(ignore -> result.fold(error -> {
                callback.accept(Try.success(Either.left(error)));
                return Unit.unit();
              }, b -> {
                callback.accept(Try.success(Either.right(b)));
                return Unit.unit();
              }));
          });
          return Unit.unit();
        }));

      return PureIO.run(cancellable::cancel);
    });
  }

  @SuppressWarnings("unchecked")
  static  PureIO unit() {
    return (PureIO) ZIOModule.UNIT;
  }
}
  
final class Repeat {
  
  private final PureIO current;
  private final ScheduleImpl schedule;
  private final Function2, PureIO> orElse;

  @SuppressWarnings("unchecked")
  protected Repeat(PureIO current, Schedule schedule, Function2, PureIO> orElse) {
    this.current = checkNonNull(current);
    this.schedule = (ScheduleImpl) checkNonNull(schedule);
    this.orElse = checkNonNull(orElse);
  }
  
  protected PureIO> run() {
    return current.foldM(error -> {
      PureIO apply = orElse.apply(error, Option.none());
      return apply.map(Either::left);
    }, value -> {
      PureIO zio = schedule.initial().toPureIO();
      return zio.flatMap(s -> loop(value, s));
    });
  }

  private PureIO> loop(A later, S state) {
    return schedule.update(later, state)
      .foldM(error -> PureIO.pure(Either.right(schedule.extract(later, state))), 
        s -> current.foldM(
          e -> orElse.apply(e, Option.some(schedule.extract(later, state))).map(Either::left), 
          a -> loop(a, s)));
  }
}

final class Retry {
  
  private final PureIO current;
  private final ScheduleImpl schedule;
  private final Function2> orElse;

  @SuppressWarnings("unchecked")
  protected Retry(PureIO current, Schedule schedule, Function2> orElse) {
    this.current = checkNonNull(current);
    this.schedule = (ScheduleImpl) checkNonNull(schedule);
    this.orElse = checkNonNull(orElse);
  }

  public PureIO> run() {
    return schedule.initial().toPureIO().flatMap(this::loop);
  }

  private PureIO> loop(S state) {
    return current.foldM(error -> {
      PureIO update = schedule.update(error, state);
      return update.foldM(
        e -> orElse.apply(error, state).map(Either::left), this::loop);
    }, value -> PureIO.pure(Either.right(value)));
  }
}

interface ZIOModule {

  PureIO UNIT = PureIO.pure(Unit.unit());

  static  Promise> runAsync(R env, PureIO current, ZIOConnection connection) {
    return runAsync(env, current, connection, new CallStack<>(), Promise.make());
  }

  @SuppressWarnings("unchecked")
  static  Promise> runAsync(R env, PureIO current, ZIOConnection connection, CallStack stack, Promise> promise) {
    while (true) {
      try {
        current = unwrap(env, current, stack, identity());
        
        if (current instanceof Pure) {
          Pure pure = (Pure) current;
          return promise.succeeded(Either.right(pure.value));
        }
        
        if (current instanceof Failure) {
          Failure failure = (Failure) current;
          return promise.succeeded(Either.left(failure.error));
        }
        
        if (current instanceof Async) {
          return executeAsync(env, (Async) current, connection, promise);
        }
        
        if (current instanceof FlatMapped) {
          stack.push();
          
          FlatMapped flatMapped = (FlatMapped) current;
          PureIO source = unwrap(env, flatMapped.current, stack, b -> b.foldM(flatMapped.nextError, flatMapped.next));
          
          if (source instanceof Async) {
            Promise> nextPromise = Promise.make();
            
            nextPromise.then(either -> {
              Function1> andThen = flatMapped.next.andThen(PureIOOf::narrowK);
              Function1> andThenError = flatMapped.nextError.andThen(PureIOOf::narrowK);
              PureIO fold = either.fold(andThenError, andThen);
              runAsync(env, fold, connection, stack, promise);
            });
            
            executeAsync(env, (Async) source, connection, nextPromise);
            
            return promise;
          }

          if (source instanceof Pure) {
            Pure pure = (Pure) source;
            Function1> andThen = flatMapped.next.andThen(PureIOOf::narrowK);
            current = andThen.apply(pure.value);
          } else if (source instanceof Failure) {
            Failure failure = (Failure) source;
            Function1> andThen = flatMapped.nextError.andThen(PureIOOf::narrowK);
            current = andThen.apply(failure.error);
          } else if (source instanceof FlatMapped) {
            FlatMapped flatMapped2 = (FlatMapped) source;
            
            current = flatMapped2.current.foldM(
              e -> flatMapped2.nextError.apply(e).foldM(flatMapped.nextError, flatMapped.next), 
              a -> flatMapped2.next.apply(a).foldM(flatMapped.nextError, flatMapped.next));
          }
        } else {
          stack.pop();
        }
      } catch (Throwable error) {
        Option> result = stack.tryHandle(error);
        
        if (result.isPresent()) {
          current = result.get();
        } else {
          return promise.failed(error);
        }
      }
    }
  }

  @SuppressWarnings("unchecked")
  static  PureIO unwrap(R env, PureIO current, CallStack stack, Function1, PureIO> next) {
    while (true) {
      if (current instanceof Failure) {
        return current;
      } else if (current instanceof Pure) {
        return current;
      } else if (current instanceof FlatMapped) {
        return current;
      } else if (current instanceof Async) {
        return current;
      } else if (current instanceof Throw) {
        Throw throwError = (Throw) current;
        return stack.sneakyThrow(throwError.error);
      } else if (current instanceof Recover) {
        Recover recover = (Recover) current;
        stack.add((PartialFunction1>) recover.mapper.andThen(next));
        current = (PureIO) recover.current;
      } else if (current instanceof AccessM) {
        AccessM accessM = (AccessM) current;
        Function1> andThen = accessM.function.andThen(PureIOOf::narrowK);
        current = andThen.apply(env);
      } else if (current instanceof Suspend) {
        Suspend suspend = (Suspend) current;
        Producer> andThen = suspend.lazy.andThen(PureIOOf::narrowK);
        current = andThen.get();
      } else if (current instanceof Delay) {
        Delay delay = (Delay) current;
        Either value = delay.task.get();
        return value.fold(PureIO::raiseError, PureIO::pure);
      } else if (current instanceof Attempt) {
        Attempt attempt = (Attempt) current;
        Either either = (Either) attempt.current.liftEither().get();
        return either.fold(PureIO::raiseError, PureIO::pure);
      } else {
        throw new IllegalStateException("not supported: " + current);
      }
    }
  }

  static  Promise> executeAsync(R env, Async current, ZIOConnection connection, Promise> promise) {
    if (connection.isCancellable() && !connection.updateState(StateIO::startingNow).isRunnable()) {
      return promise.cancel();
    }
    
    connection.setCancelToken(current.callback.apply(env, result -> promise.tryComplete(result.map(EitherOf::narrowK))));
    
    promise.thenRun(() -> connection.setCancelToken(UNIT));
    
    if (connection.isCancellable() && connection.updateState(StateIO::notStartingNow).isCancellingNow()) {
      connection.cancelNow();
    }

    return promise;
  }

  final class Pure implements SealedPureIO {

    private final A value;

    protected Pure(A value) {
      this.value = checkNonNull(value);
    }

    @Override
    public String toString() {
      return "Pure(" + value + ")";
    }
  }

  final class Failure implements SealedPureIO {

    private final E error;

    protected Failure(E error) {
      this.error = checkNonNull(error);
    }

    @Override
    public String toString() {
      return "Failure(" + error + ")";
    }
  }

  final class Throw implements SealedPureIO {

    private final Throwable error;

    protected Throw(Throwable error) {
      this.error = checkNonNull(error);
    }

    @Override
    public String toString() {
      return "Throw(" + error + ")";
    }
  }

  final class FlatMapped implements SealedPureIO {

    private final PureIO current;
    private final Function1> nextError;
    private final Function1> next;

    protected FlatMapped(PureIO current,
                         Function1> nextError,
                         Function1> next) {
      this.current = checkNonNull(current);
      this.nextError = checkNonNull(nextError);
      this.next = checkNonNull(next);
    }

    @Override
    public String toString() {
      return "FlatMapped(" + current + ", ?, ?)";
    }
  }

  final class Delay implements SealedPureIO {

    private final Producer> task;

    protected Delay(Producer> task) {
      this.task = checkNonNull(task);
    }

    @Override
    public String toString() {
      return "Delay(?)";
    }
  }

  final class Suspend implements SealedPureIO {

    private final Producer> lazy;

    protected Suspend(Producer> lazy) {
      this.lazy = checkNonNull(lazy);
    }

    @Override
    public String toString() {
      return "Suspend(?)";
    }
  }

  final class Async implements SealedPureIO {

    private final Function2>>, PureIO> callback;

    protected Async(Function2>>, PureIO> callback) {
      this.callback = checkNonNull(callback);
    }

    @Override
    public String toString() {
      return "Async(?)";
    }
  }

  final class Attempt implements SealedPureIO {

    private final Producer current;

    protected Attempt(Producer current) {
      this.current = checkNonNull(current);
    }

    @Override
    public String toString() {
      return "Attempt(" + current + ")";
    }
  }

  final class Recover implements SealedPureIO {

    private final PureIO current;
    private final PartialFunction1> mapper;

    protected Recover(PureIO current, PartialFunction1> mapper) {
      this.current = checkNonNull(current);
      this.mapper = checkNonNull(mapper);
    }

    @Override
    public String toString() {
      return "Recover(" + current + ", ?)";
    }
  }

  final class AccessM implements SealedPureIO {

    private final Function1> function;

    protected AccessM(Function1> function) {
      this.function = checkNonNull(function);
    }

    @Override
    public String toString() {
      return "AccessM(?)";
    }
  }
}

interface ZIOConnection {
  
  ZIOConnection UNCANCELLABLE = new ZIOConnection() {
    @Override
    public boolean isCancellable() { return false; }

    @Override
    public void setCancelToken(PureIO cancel) { /* nothing to do */ }

    @Override
    public void cancelNow() { /* nothing to do */ }

    @Override
    public void cancel() { /* nothing to do */ }

    @Override
    public StateIO updateState(Operator1 update) {
      return StateIO.INITIAL;
    }
  };
  
  boolean isCancellable();
  
  void setCancelToken(PureIO cancel);
  
  void cancelNow();
  
  void cancel();
  
  StateIO updateState(Operator1 update);
  
  static ZIOConnection cancellable() {
    return new ZIOConnection() {
      
      private PureIO cancelToken;
      private final AtomicReference state = new AtomicReference<>(StateIO.INITIAL);
      
      @Override
      public boolean isCancellable() { return true; }
      
      @Override
      public void setCancelToken(PureIO cancel) { this.cancelToken = checkNonNull(cancel); }
      
      @Override
      public void cancelNow() { cancelToken.runAsync(null); }
      
      @Override
      public void cancel() {
        if (state.getAndUpdate(StateIO::cancellingNow).isCancelable()) {
          cancelNow();
        
          state.set(StateIO.CANCELLED);
        }
      }
      
      @Override
      public StateIO updateState(Operator1 update) {
        return state.updateAndGet(update::apply);
      }
    };
  }
}

final class StateIO {
  
  public static final StateIO INITIAL = new StateIO(false, false, false);
  public static final StateIO CANCELLED = new StateIO(true, false, false);
  
  private final boolean isCancelled;
  private final boolean cancellingNow;
  private final boolean startingNow;
  
  public StateIO(boolean isCancelled, boolean cancellingNow, boolean startingNow) {
    this.isCancelled = isCancelled;
    this.cancellingNow = cancellingNow;
    this.startingNow = startingNow;
  }
  
  public boolean isCancelled() {
    return isCancelled;
  }
  
  public boolean isCancellingNow() {
    return cancellingNow;
  }
  
  public boolean isStartingNow() {
    return startingNow;
  }
  
  public StateIO cancellingNow() {
    return new StateIO(isCancelled, true, startingNow);
  }
  
  public StateIO startingNow() {
    return new StateIO(isCancelled, cancellingNow, true);
  }
  
  public StateIO notStartingNow() {
    return new StateIO(isCancelled, cancellingNow, false);
  }
  
  public boolean isCancelable() {
    return !isCancelled && !cancellingNow && !startingNow;
  }
  
  public boolean isRunnable() {
    return !isCancelled && !cancellingNow;
  }
}

final class CallStack implements Recoverable {
  
  private StackItem top = new StackItem<>();
  
  public void push() {
    top.push();
  }

  public void pop() {
    if (top.count() > 0) {
      top.pop();
    } else {
      top = top.prev();
    }
  }
  
  public void add(PartialFunction1> mapError) {
    if (top.count() > 0) {
      top.pop();
      top = new StackItem<>(top);
    }
    top.add(mapError);
  }
  
  public Option> tryHandle(Throwable error) {
    while (top != null) {
      top.reset();
      Option> result = top.tryHandle(error);
      
      if (result.isPresent()) {
        return result;
      } else {
        top = top.prev();
      }
    }
    return Option.none();
  }
}

final class StackItem {
  
  private int count = 0;
  private final Deque>> recover = new LinkedList<>();

  private final StackItem prev;

  public StackItem() {
    this(null);
  }

  public StackItem(StackItem prev) {
    this.prev = prev;
  }
  
  public StackItem prev() {
    return prev;
  }
  
  public int count() {
    return count;
  }
  
  public void push() {
    count++;
  }
  
  public void pop() {
    count--;
  }
  
  public void reset() {
    count = 0;
  }
  
  public void add(PartialFunction1> mapError) {
    recover.addFirst(mapError);
  }

  public Option> tryHandle(Throwable error) {
    while (!recover.isEmpty()) {
      PartialFunction1> mapError = recover.removeFirst();
      if (mapError.isDefinedAt(error)) {
        PartialFunction1> andThen = mapError.andThen(PureIOOf::narrowK);
        return Option.some(andThen.apply(error));
      }
    }
    return Option.none();
  }
}