diff --git a/src/main/java/com/github/tonivade/vavr/effect/CallStack.java b/src/main/java/com/github/tonivade/vavr/effect/CallStack.java new file mode 100644 index 0000000..84a71d9 --- /dev/null +++ b/src/main/java/com/github/tonivade/vavr/effect/CallStack.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024, Antonio Gabriel Muñoz Conejo + * Distributed under the terms of the MIT License + */ +package com.github.tonivade.vavr.effect; + +import javax.annotation.Nullable; + +import io.vavr.PartialFunction; +import io.vavr.control.Option; + +final class CallStack { + + @Nullable + private StackItem top = new StackItem<>(); + + void push() { + if (top != null) { + top.push(); + } + } + + void pop() { + if (top == null) { + return; + } + if (top.count() > 0) { + top.pop(); + } else { + top = top.prev(); + } + } + + void add(PartialFunction> mapError) { + if (top == null) { + return; + } + if (top.count() > 0) { + top.pop(); + top = new StackItem<>(top); + } + top.add(mapError); + } + + Option> tryHandle(Throwable error) { + while (top != null) { + top.reset(); + Option> result = top.tryHandle(error); + + if (result.isDefined()) { + return result; + } else { + top = top.prev(); + } + } + return Option.none(); + } + + // XXX: https://www.baeldung.com/java-sneaky-throws + @SuppressWarnings("unchecked") + R sneakyThrow(Throwable t) throws X { + throw (X) t; + } +} diff --git a/src/main/java/com/github/tonivade/vavr/effect/IO.java b/src/main/java/com/github/tonivade/vavr/effect/IO.java index 33b477e..40162b1 100644 --- a/src/main/java/com/github/tonivade/vavr/effect/IO.java +++ b/src/main/java/com/github/tonivade/vavr/effect/IO.java @@ -7,8 +7,6 @@ import static io.vavr.Function1.identity; import java.time.Duration; -import java.util.ArrayDeque; -import java.util.Deque; import java.util.NoSuchElementException; import java.util.Objects; import java.util.concurrent.CancellationException; @@ -18,13 +16,9 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; -import java.util.function.UnaryOperator; - -import javax.annotation.Nullable; import io.vavr.CheckedConsumer; import io.vavr.CheckedFunction0; @@ -672,282 +666,6 @@ } } -final class Repeat { - - private final IO current; - private final ScheduleImpl schedule; - private final Function2, IO> orElse; - - @SuppressWarnings("unchecked") - Repeat(IO current, Schedule schedule, Function2, IO> orElse) { - this.current = Objects.requireNonNull(current); - this.schedule = (ScheduleImpl) Objects.requireNonNull(schedule); - this.orElse = Objects.requireNonNull(orElse); - } - - IO> run() { - return current.attempt().flatMap(either -> either.fold( - error -> orElse.apply(error, Option.none()).map(Either::left), - a -> schedule.initial().flatMap(s -> loop(a, s)))); - } - - private IO> loop(A later, S state) { - return schedule.update(later, state).flatMap(decision -> decision.fold( - ignore -> IO.pure(Either.right(schedule.extract(later, state))), - s -> current.attempt().flatMap(either -> either.fold( - e -> orElse.apply(e, Option.some(schedule.extract(later, state))).map(Either::left), - a -> loop(a, s))))); - } -} - -final class Retry { - - private final IO current; - private final ScheduleImpl schedule; - private final Function2> orElse; - - @SuppressWarnings("unchecked") - Retry(IO current, Schedule schedule, Function2> orElse) { - this.current = Objects.requireNonNull(current); - this.schedule = (ScheduleImpl) Objects.requireNonNull(schedule); - this.orElse = Objects.requireNonNull(orElse); - } - - public IO> run() { - return schedule.initial().flatMap(this::loop); - } - - private IO> loop(S state) { - return current.attempt().flatMap(either -> either.fold( - error -> { - IO> update = schedule.update(error, state); - return update.flatMap(decision -> decision.fold( - ignore -> orElse.apply(error, state).map(Either::left), - this::loop)); - }, - a -> IO.pure(Either.right(a))) - ); - } -} - -sealed interface IOConnection { - - IOConnection UNCANCELLABLE = new Uncancellable(); - - boolean isCancellable(); - - void setCancelToken(IO cancel); - - void cancelNow(); - - void cancel(); - - StateIO updateState(UnaryOperator update); - - static IOConnection cancellable() { - return new Cancellable(); - } - - static final class Uncancellable implements IOConnection { - - private Uncancellable() { } - - @Override - public boolean isCancellable() { - return false; - } - - @Override - public void setCancelToken(IO cancel) { - // uncancellable - } - - @Override - public void cancelNow() { - // uncancellable - } - - @Override - public void cancel() { - // uncancellable - } - - @Override - public StateIO updateState(UnaryOperator update) { - return StateIO.INITIAL; - } - } - - static final class Cancellable implements IOConnection { - - private IO cancelToken = IO.UNIT; - private final AtomicReference state = new AtomicReference<>(StateIO.INITIAL); - - private Cancellable() { } - - @Override - public boolean isCancellable() { - return true; - } - - @Override - public void setCancelToken(IO cancel) { - this.cancelToken = Objects.requireNonNull(cancel); - } - - @Override - public void cancelNow() { - cancelToken.runAsync(); - } - - @Override - public void cancel() { - if (state.getAndUpdate(StateIO::cancellingNow).isCancelable()) { - cancelNow(); - - state.set(StateIO.CANCELLED); - } - } - - @Override - public StateIO updateState(UnaryOperator update) { - return state.updateAndGet(update::apply); - } - } -} - -record StateIO(boolean isCancelled, boolean isCancellingNow, boolean isStartingNow) { - - static final StateIO INITIAL = new StateIO(false, false, false); - static final StateIO CANCELLED = new StateIO(true, false, false); - - StateIO cancellingNow() { - return new StateIO(isCancelled, true, isStartingNow); - } - - StateIO startingNow() { - return new StateIO(isCancelled, isCancellingNow, true); - } - - StateIO notStartingNow() { - return new StateIO(isCancelled, isCancellingNow, false); - } - - boolean isCancelable() { - return !isCancelled && !isCancellingNow && !isStartingNow; - } - - boolean isRunnable() { - return !isCancelled && !isCancellingNow; - } -} - -final class CallStack { - - @Nullable - private StackItem top = new StackItem<>(); - - void push() { - if (top != null) { - top.push(); - } - } - - void pop() { - if (top == null) { - return; - } - if (top.count() > 0) { - top.pop(); - } else { - top = top.prev(); - } - } - - void add(PartialFunction> mapError) { - if (top == null) { - return; - } - if (top.count() > 0) { - top.pop(); - top = new StackItem<>(top); - } - top.add(mapError); - } - - Option> tryHandle(Throwable error) { - while (top != null) { - top.reset(); - Option> result = top.tryHandle(error); - - if (result.isDefined()) { - return result; - } else { - top = top.prev(); - } - } - return Option.none(); - } - - // XXX: https://www.baeldung.com/java-sneaky-throws - @SuppressWarnings("unchecked") - R sneakyThrow(Throwable t) throws X { - throw (X) t; - } -} - -final class StackItem { - - private int count = 0; - private final Deque>> recover = new ArrayDeque<>(); - - @Nullable - private final StackItem prev; - - StackItem() { - this(null); - } - - StackItem(@Nullable StackItem prev) { - this.prev = prev; - } - - @Nullable - StackItem prev() { - return prev; - } - - int count() { - return count; - } - - void push() { - count++; - } - - void pop() { - count--; - } - - void reset() { - count = 0; - } - - void add(PartialFunction> mapError) { - recover.addFirst(mapError); - } - - Option> tryHandle(Throwable error) { - while (!recover.isEmpty()) { - var mapError = recover.removeFirst(); - if (mapError.isDefinedAt(error)) { - return Option.some(IO.narrowK(mapError.apply(error))); - } - } - return Option.none(); - } -} - interface FutureModule { ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(0); diff --git a/src/main/java/com/github/tonivade/vavr/effect/IOConnection.java b/src/main/java/com/github/tonivade/vavr/effect/IOConnection.java new file mode 100644 index 0000000..0f7816b --- /dev/null +++ b/src/main/java/com/github/tonivade/vavr/effect/IOConnection.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024, Antonio Gabriel Muñoz Conejo + * Distributed under the terms of the MIT License + */ +package com.github.tonivade.vavr.effect; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.UnaryOperator; + +sealed interface IOConnection { + + IOConnection UNCANCELLABLE = new Uncancellable(); + + boolean isCancellable(); + + void setCancelToken(IO cancel); + + void cancelNow(); + + void cancel(); + + StateIO updateState(UnaryOperator update); + + static IOConnection cancellable() { + return new Cancellable(); + } + + static final class Uncancellable implements IOConnection { + + private Uncancellable() { } + + @Override + public boolean isCancellable() { + return false; + } + + @Override + public void setCancelToken(IO cancel) { + // uncancellable + } + + @Override + public void cancelNow() { + // uncancellable + } + + @Override + public void cancel() { + // uncancellable + } + + @Override + public StateIO updateState(UnaryOperator update) { + return StateIO.INITIAL; + } + } + + static final class Cancellable implements IOConnection { + + private IO cancelToken = IO.UNIT; + private final AtomicReference state = new AtomicReference<>(StateIO.INITIAL); + + private Cancellable() { } + + @Override + public boolean isCancellable() { + return true; + } + + @Override + public void setCancelToken(IO cancel) { + this.cancelToken = Objects.requireNonNull(cancel); + } + + @Override + public void cancelNow() { + cancelToken.runAsync(); + } + + @Override + public void cancel() { + if (state.getAndUpdate(StateIO::cancellingNow).isCancelable()) { + cancelNow(); + + state.set(StateIO.CANCELLED); + } + } + + @Override + public StateIO updateState(UnaryOperator update) { + return state.updateAndGet(update::apply); + } + } +} diff --git a/src/main/java/com/github/tonivade/vavr/effect/Repeat.java b/src/main/java/com/github/tonivade/vavr/effect/Repeat.java new file mode 100644 index 0000000..3f4aeee --- /dev/null +++ b/src/main/java/com/github/tonivade/vavr/effect/Repeat.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024, Antonio Gabriel Muñoz Conejo + * Distributed under the terms of the MIT License + */ +package com.github.tonivade.vavr.effect; + +import java.util.Objects; + +import io.vavr.Function2; +import io.vavr.control.Either; +import io.vavr.control.Option; + +final class Repeat { + + private final IO current; + private final ScheduleImpl schedule; + private final Function2, IO> orElse; + + @SuppressWarnings("unchecked") + Repeat(IO current, Schedule schedule, Function2, IO> orElse) { + this.current = Objects.requireNonNull(current); + this.schedule = (ScheduleImpl) Objects.requireNonNull(schedule); + this.orElse = Objects.requireNonNull(orElse); + } + + IO> run() { + return current.attempt().flatMap(either -> either.fold( + error -> orElse.apply(error, Option.none()).map(Either::left), + a -> schedule.initial().flatMap(s -> loop(a, s)))); + } + + private IO> loop(A later, S state) { + return schedule.update(later, state).flatMap(decision -> decision.fold( + ignore -> IO.pure(Either.right(schedule.extract(later, state))), + s -> current.attempt().flatMap(either -> either.fold( + e -> orElse.apply(e, Option.some(schedule.extract(later, state))).map(Either::left), + a -> loop(a, s))))); + } +} diff --git a/src/main/java/com/github/tonivade/vavr/effect/Retry.java b/src/main/java/com/github/tonivade/vavr/effect/Retry.java new file mode 100644 index 0000000..8c67c74 --- /dev/null +++ b/src/main/java/com/github/tonivade/vavr/effect/Retry.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024, Antonio Gabriel Muñoz Conejo + * Distributed under the terms of the MIT License + */ +package com.github.tonivade.vavr.effect; + +import java.util.Objects; + +import io.vavr.Function2; +import io.vavr.control.Either; + +final class Retry { + + private final IO current; + private final ScheduleImpl schedule; + private final Function2> orElse; + + @SuppressWarnings("unchecked") + Retry(IO current, Schedule schedule, Function2> orElse) { + this.current = Objects.requireNonNull(current); + this.schedule = (ScheduleImpl) Objects.requireNonNull(schedule); + this.orElse = Objects.requireNonNull(orElse); + } + + public IO> run() { + return schedule.initial().flatMap(this::loop); + } + + private IO> loop(S state) { + return current.attempt().flatMap(either -> either.fold( + error -> { + IO> update = schedule.update(error, state); + return update.flatMap(decision -> decision.fold( + ignore -> orElse.apply(error, state).map(Either::left), + this::loop)); + }, + a -> IO.pure(Either.right(a))) + ); + } +} diff --git a/src/main/java/com/github/tonivade/vavr/effect/StackItem.java b/src/main/java/com/github/tonivade/vavr/effect/StackItem.java new file mode 100644 index 0000000..f567ecd --- /dev/null +++ b/src/main/java/com/github/tonivade/vavr/effect/StackItem.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024, Antonio Gabriel Muñoz Conejo + * Distributed under the terms of the MIT License + */ +package com.github.tonivade.vavr.effect; + +import java.util.ArrayDeque; +import java.util.Deque; + +import javax.annotation.Nullable; + +import io.vavr.PartialFunction; +import io.vavr.control.Option; + +final class StackItem { + + private int count = 0; + private final Deque>> recover = new ArrayDeque<>(); + + @Nullable + private final StackItem prev; + + StackItem() { + this(null); + } + + StackItem(@Nullable StackItem prev) { + this.prev = prev; + } + + @Nullable + StackItem prev() { + return prev; + } + + int count() { + return count; + } + + void push() { + count++; + } + + void pop() { + count--; + } + + void reset() { + count = 0; + } + + void add(PartialFunction> mapError) { + recover.addFirst(mapError); + } + + Option> tryHandle(Throwable error) { + while (!recover.isEmpty()) { + var mapError = recover.removeFirst(); + if (mapError.isDefinedAt(error)) { + return Option.some(IO.narrowK(mapError.apply(error))); + } + } + return Option.none(); + } +} diff --git a/src/main/java/com/github/tonivade/vavr/effect/StateIO.java b/src/main/java/com/github/tonivade/vavr/effect/StateIO.java new file mode 100644 index 0000000..5ea5205 --- /dev/null +++ b/src/main/java/com/github/tonivade/vavr/effect/StateIO.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024, Antonio Gabriel Muñoz Conejo + * Distributed under the terms of the MIT License + */ +package com.github.tonivade.vavr.effect; + +record StateIO(boolean isCancelled, boolean isCancellingNow, boolean isStartingNow) { + + static final StateIO INITIAL = new StateIO(false, false, false); + static final StateIO CANCELLED = new StateIO(true, false, false); + + StateIO cancellingNow() { + return new StateIO(isCancelled, true, isStartingNow); + } + + StateIO startingNow() { + return new StateIO(isCancelled, isCancellingNow, true); + } + + StateIO notStartingNow() { + return new StateIO(isCancelled, isCancellingNow, false); + } + + boolean isCancelable() { + return !isCancelled && !isCancellingNow && !isStartingNow; + } + + boolean isRunnable() { + return !isCancelled && !isCancellingNow; + } +} diff --git a/src/test/java/com/github/tonivade/vavr/effect/ScheduleTest.java b/src/test/java/com/github/tonivade/vavr/effect/ScheduleTest.java index 260e441..00b06f2 100644 --- a/src/test/java/com/github/tonivade/vavr/effect/ScheduleTest.java +++ b/src/test/java/com/github/tonivade/vavr/effect/ScheduleTest.java @@ -85,7 +85,7 @@ .thenReturn("hola"); IO read = IO.task(console::get); - IO retry = read.retry(Schedule.recurs(1)); + IO retry = read.retry(Schedule.once()); String provide = retry.unsafeRunSync();