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 5379e95..42d9f98 100644 --- a/src/main/java/com/github/tonivade/vavr/effect/IO.java +++ b/src/main/java/com/github/tonivade/vavr/effect/IO.java @@ -23,6 +23,7 @@ import java.util.function.Supplier; import java.util.function.UnaryOperator; import io.vavr.CheckedConsumer; +import io.vavr.CheckedFunction0; import io.vavr.CheckedRunnable; import io.vavr.Function1; import io.vavr.Function2; @@ -256,7 +257,7 @@ return new Failure<>(error); } - static IO delay(Duration delay, Supplier lazy) { + static IO delay(Duration delay, CheckedFunction0 lazy) { return sleep(delay).andThen(task(lazy)); } @@ -317,7 +318,7 @@ return task(asSupplier(task)); } - static IO task(Supplier producer) { + static IO task(CheckedFunction0 producer) { return new Delay<>(producer); } @@ -346,7 +347,7 @@ } static IO>> memoize(Executor executor, Function1> function) { - var ref = Ref.make(HashMap.>empty()); + var ref = Reference.make(HashMap.>empty()); return ref.map(r -> { Function1>> result = a -> r.modify(map -> map.get(a).fold(() -> { Promise promise = Promise.make(); @@ -471,7 +472,7 @@ }; } - private static Supplier asSupplier(CheckedRunnable task) { + private static CheckedFunction0 asSupplier(CheckedRunnable task) { return () -> { task.unchecked().run(); return Unit.unit(); @@ -549,7 +550,7 @@ Supplier> andThen = () -> IO.narrowK(suspend.lazy.get()); current = andThen.get(); } else if (current instanceof Delay delay) { - return IO.pure(delay.task.get()); + return IO.pure(delay.task.unchecked().get()); } else if (current instanceof Pure) { return current; } else if (current instanceof FlatMapped) { @@ -625,9 +626,9 @@ final class Delay implements IO { - private final Supplier task; + private final CheckedFunction0 task; - private Delay(Supplier task) { + private Delay(CheckedFunction0 task) { this.task = Objects.requireNonNull(task); } diff --git a/src/main/java/com/github/tonivade/vavr/effect/Ref.java b/src/main/java/com/github/tonivade/vavr/effect/Ref.java deleted file mode 100644 index 5dd2abf..0000000 --- a/src/main/java/com/github/tonivade/vavr/effect/Ref.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2022, 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; - -import io.vavr.Function1; -import io.vavr.Tuple2; - -public final class Ref { - - private final AtomicReference value; - - private Ref(AtomicReference value) { - this.value = Objects.requireNonNull(value); - } - - public IO get() { - return IO.task(value::get); - } - - public IO set(A newValue) { - return IO.task(() -> { value.set(newValue); return Unit.unit(); }); - } - - public IO modify(Function1> change) { - return IO.task(() -> { - var loop = true; - B result = null; - while (loop) { - A current = value.get(); - var tuple = change.apply(current); - result = tuple._1(); - loop = !value.compareAndSet(current, tuple._2()); - } - return result; - }); - } - - public IO lazySet(A newValue) { - return IO.task(() -> { value.lazySet(newValue); return Unit.unit(); }); - } - - public IO getAndSet(A newValue) { - return IO.task(() -> value.getAndSet(newValue)); - } - - public IO updateAndGet(UnaryOperator update) { - return IO.task(() -> value.updateAndGet(update::apply)); - } - - public IO getAndUpdate(UnaryOperator update) { - return IO.task(() -> value.getAndUpdate(update::apply)); - } - - public static IO> make(A value) { - return IO.pure(of(value)); - } - - public static Ref of(A value) { - return new Ref<>(new AtomicReference<>(value)); - } - - @Override - public String toString() { - return String.format("Ref(%s)", value.get()); - } -} diff --git a/src/main/java/com/github/tonivade/vavr/effect/Reference.java b/src/main/java/com/github/tonivade/vavr/effect/Reference.java new file mode 100644 index 0000000..ce66193 --- /dev/null +++ b/src/main/java/com/github/tonivade/vavr/effect/Reference.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022, 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; + +import io.vavr.Function1; +import io.vavr.Tuple2; + +public final class Reference { + + private final AtomicReference value; + + private Reference(AtomicReference value) { + this.value = Objects.requireNonNull(value); + } + + public IO get() { + return IO.task(value::get); + } + + public IO set(A newValue) { + return IO.task(() -> { value.set(newValue); return Unit.unit(); }); + } + + public IO modify(Function1> change) { + return IO.task(() -> { + var loop = true; + B result = null; + while (loop) { + A current = value.get(); + var tuple = change.apply(current); + result = tuple._1(); + loop = !value.compareAndSet(current, tuple._2()); + } + return result; + }); + } + + public IO lazySet(A newValue) { + return IO.task(() -> { value.lazySet(newValue); return Unit.unit(); }); + } + + public IO getAndSet(A newValue) { + return IO.task(() -> value.getAndSet(newValue)); + } + + public IO updateAndGet(UnaryOperator update) { + return IO.task(() -> value.updateAndGet(update::apply)); + } + + public IO getAndUpdate(UnaryOperator update) { + return IO.task(() -> value.getAndUpdate(update::apply)); + } + + public static IO> make(A value) { + return IO.pure(of(value)); + } + + public static Reference of(A value) { + return new Reference<>(new AtomicReference<>(value)); + } + + @Override + public String toString() { + return String.format("Ref(%s)", value.get()); + } +} diff --git a/src/main/java/com/github/tonivade/vavr/effect/Resource.java b/src/main/java/com/github/tonivade/vavr/effect/Resource.java new file mode 100644 index 0000000..802230b --- /dev/null +++ b/src/main/java/com/github/tonivade/vavr/effect/Resource.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2018-2021, 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.CheckedConsumer; +import io.vavr.Function1; +import io.vavr.Tuple; +import io.vavr.Tuple2; + +public final class Resource { + + private final IO>> managed; + + Resource(IO>> resource) { + this.managed = Objects.requireNonNull(resource); + } + + public Resource map(Function1 mapper) { + return flatMap(t -> pure(mapper.andThen(IO::pure).apply(t))); + } + + public Resource flatMap(Function1> mapper) { + return new Resource<>(managed.flatMap( + t -> { + Resource apply = mapper.apply(t._1()); + return apply.managed.map( + r -> Tuple.of(r._1(), (CheckedConsumer) ignore -> releaseAndThen(t, r))); + })); + } + + public IO use(Function1> use) { + return IO.bracket(managed, t -> use.apply(t._1()), release()); + } + + public Resource> combine(Resource other) { + return new Resource<>(IO.bracket(managed, + t -> IO.bracket(other.managed, + r -> IO.pure(Tuple.of(Tuple.of(t._1(), r._1()), x -> {})), + release()), + release())); + } + + public static Resource pure(IO acquire) { + return new Resource<>(acquire.map(t -> Tuple.of(t, x -> {}))); + } + + public static Resource from(IO acquire, CheckedConsumer release) { + return new Resource<>(acquire.map(t -> Tuple.of(t, release))); + } + + public static Resource from(IO acquire) { + return from(acquire, AutoCloseable::close); + } + + private static void releaseAndThen( + Tuple2> outter, Tuple2> inner) { + try { + Resource.release().unchecked().accept(inner); + } finally { + Resource.release().unchecked().accept(outter); + } + } + + private static CheckedConsumer>> release() { + return t -> t._2().accept(t._1()); + } +} diff --git a/src/test/java/com/github/tonivade/vavr/effect/IOTest.java b/src/test/java/com/github/tonivade/vavr/effect/IOTest.java index 59d9477..70452b1 100644 --- a/src/test/java/com/github/tonivade/vavr/effect/IOTest.java +++ b/src/test/java/com/github/tonivade/vavr/effect/IOTest.java @@ -22,11 +22,11 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; -import java.util.function.Supplier; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import io.vavr.CheckedFunction0; import io.vavr.CheckedFunction1; import io.vavr.Function1; import io.vavr.Tuple2; @@ -150,28 +150,31 @@ } @Test - void retry(@Mock Supplier computation) { - when(computation.get()).thenThrow(UnsupportedOperationException.class); + void retry(@Mock CheckedFunction0 computation) throws Throwable { + when(computation.unchecked()).thenCallRealMethod(); + when(computation.apply()).thenThrow(UnsupportedOperationException.class); Try retry = IO.task(computation).retry().safeRunSync(); assertTrue(retry.isFailure()); - verify(computation, times(2)).get(); + verify(computation, times(2)).apply(); } @Test - void retryFailure(@Mock Supplier computation) { - when(computation.get()).thenThrow(UnsupportedOperationException.class); + void retryFailure(@Mock CheckedFunction0 computation) throws Throwable { + when(computation.unchecked()).thenCallRealMethod(); + when(computation.apply()).thenThrow(UnsupportedOperationException.class); Try retry = IO.task(computation).retry(Duration.ofMillis(100), 3).safeRunSync(); assertTrue(retry.isFailure()); - verify(computation, times(4)).get(); + verify(computation, times(4)).apply(); } @Test - void retrySuccess(@Mock Supplier computation) { - when(computation.get()) + void retrySuccess(@Mock CheckedFunction0 computation) throws Throwable { + when(computation.unchecked()).thenCallRealMethod(); + when(computation.apply()) .thenThrow(UnsupportedOperationException.class) .thenThrow(UnsupportedOperationException.class) .thenThrow(UnsupportedOperationException.class) @@ -180,37 +183,40 @@ Try retry = IO.task(computation).retry(Duration.ofMillis(100), 3).safeRunSync(); assertEquals("hola", retry.get()); - verify(computation, times(4)).get(); + verify(computation, times(4)).apply(); } @Test - void repeatSuccess(@Mock Supplier computation) { - when(computation.get()).thenReturn("hola"); + void repeatSuccess(@Mock CheckedFunction0 computation) throws Throwable { + when(computation.unchecked()).thenCallRealMethod(); + when(computation.apply()).thenReturn("hola"); Try repeat = IO.task(computation).repeat(Duration.ofMillis(100), 3).safeRunSync(); assertEquals("hola", repeat.get()); - verify(computation, times(4)).get(); + verify(computation, times(4)).apply(); } @Test - void repeatFailure(@Mock Supplier computation) { - when(computation.get()).thenReturn("hola").thenThrow(UnsupportedOperationException.class); + void repeatFailure(@Mock CheckedFunction0 computation) throws Throwable { + when(computation.unchecked()).thenCallRealMethod(); + when(computation.apply()).thenReturn("hola").thenThrow(UnsupportedOperationException.class); Try repeat = IO.task(computation).repeat(Duration.ofMillis(100), 3).safeRunSync(); assertTrue(repeat.isFailure()); - verify(computation, times(2)).get(); + verify(computation, times(2)).apply(); } @Test - void repeat(@Mock Supplier computation) { - when(computation.get()).thenReturn("hola"); + void repeat(@Mock CheckedFunction0 computation) throws Throwable { + when(computation.unchecked()).thenCallRealMethod(); + when(computation.apply()).thenReturn("hola"); Try repeat = IO.task(computation).repeat().safeRunSync(); assertEquals("hola", repeat.get()); - verify(computation, times(2)).get(); + verify(computation, times(2)).apply(); } @Test @@ -381,7 +387,7 @@ } private IO> currentThreadIO() { - Ref> ref = Ref.of(List.empty()); + Reference> ref = Reference.of(List.empty()); IO> currentThread = ref.updateAndGet(list -> list.append(Thread.currentThread().getName())); diff --git a/src/test/java/com/github/tonivade/vavr/effect/RefeferenceTest.java b/src/test/java/com/github/tonivade/vavr/effect/RefeferenceTest.java new file mode 100644 index 0000000..5da3358 --- /dev/null +++ b/src/test/java/com/github/tonivade/vavr/effect/RefeferenceTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2018-2021, Antonio Gabriel Muñoz Conejo + * Distributed under the terms of the MIT License + */ +package com.github.tonivade.vavr.effect; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class RefeferenceTest { + + @Test + void get() { + Reference ref = Reference.of("Hello World!"); + + IO result = ref.get(); + + assertEquals("Hello World!", result.unsafeRunSync()); + } + + @Test + void set() { + Reference ref = Reference.of("Hello World!"); + + IO set = ref.set("Something else"); + IO result = set.andThen(ref.get()); + + assertEquals("Something else", result.unsafeRunSync()); + } + + @Test + void lazySet() { + Reference ref = Reference.of("Hello World!"); + + IO lazySet = ref.lazySet("Something else"); + IO result = lazySet.andThen(ref.get()); + + assertEquals("Something else", result.unsafeRunSync()); + } + + @Test + void getAndSet() { + Reference ref = Reference.of("Hello World!"); + + IO result = ref.getAndSet("Something else"); + IO afterUpdate = result.andThen(ref.get()); + + assertEquals("Hello World!", result.unsafeRunSync()); + assertEquals("Something else", afterUpdate.unsafeRunSync()); + } + + @Test + void getAndUpdate() { + Reference ref = Reference.of("Hello World!"); + + IO result = ref.getAndUpdate(String::toUpperCase); + IO afterUpdate = result.andThen(ref.get()); + + assertEquals("Hello World!", result.unsafeRunSync()); + assertEquals("HELLO WORLD!", afterUpdate.unsafeRunSync()); + } + + @Test + void updateAndGet() { + Reference ref = Reference.of("Hello World!"); + + IO result = ref.updateAndGet(String::toUpperCase); + + assertEquals("HELLO WORLD!", result.unsafeRunSync()); + } +} diff --git a/src/test/java/com/github/tonivade/vavr/effect/ResourceTest.java b/src/test/java/com/github/tonivade/vavr/effect/ResourceTest.java new file mode 100644 index 0000000..544a807 --- /dev/null +++ b/src/test/java/com/github/tonivade/vavr/effect/ResourceTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2018-2021, Antonio Gabriel Muñoz Conejo + * Distributed under the terms of the MIT License + */ +package com.github.tonivade.vavr.effect; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import javax.sql.DataSource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import io.vavr.CheckedConsumer; +import io.vavr.Tuple2; + +@ExtendWith(MockitoExtension.class) +class ResourceTest { + + private Resource makeResource(IO acquire) { + return makeResource(acquire, AutoCloseable::close); + } + + private Resource makeResource(IO acquire, CheckedConsumer release) { + return Resource.from(acquire, release); + } + + @Test + void use(@Mock CheckedConsumer release) throws Throwable { + Resource resource = makeResource(IO.pure("hola"), release); + + IO use = resource.use(string -> IO.pure(string.toUpperCase())); + + assertEquals("HOLA", use.unsafeRunSync()); + verify(release).accept("hola"); + } + + @Test + void map(@Mock CheckedConsumer release) throws Throwable { + Resource resource = makeResource(IO.pure("hola"), release).map(String::toUpperCase); + + IO use = resource.use(string -> IO.pure(string.length())); + + assertEquals(4, use.unsafeRunSync()); + verify(release).accept("hola"); + } + + @Test + void flatMap(@Mock DataSource dataSource, @Mock Connection connection, + @Mock PreparedStatement statement, @Mock ResultSet resultSet) throws SQLException { + when(dataSource.getConnection()).thenReturn(connection); + when(connection.prepareStatement("sql")).thenReturn(statement); + when(statement.executeQuery()).thenReturn(resultSet); + when(resultSet.getString(0)).thenReturn("result"); + + Resource flatMap = makeResource(IO.task(dataSource::getConnection)) + .flatMap(conn -> makeResource(IO.task(() -> conn.prepareStatement("sql")))) + .flatMap(stmt -> makeResource(IO.task(() -> stmt.executeQuery()))); + + IO use = flatMap.use(rs -> IO.task(() -> rs.getString(0))); + + assertEquals("result", use.unsafeRunSync()); + InOrder inOrder = inOrder(resultSet, statement, connection); + inOrder.verify(resultSet).close(); + inOrder.verify(statement).close(); + inOrder.verify(connection).close(); + } + + @Test + void combine(@Mock CheckedConsumer release1, @Mock CheckedConsumer release2) throws Throwable { + Resource res1 = makeResource(IO.pure("hola"), release1); + Resource res2 = makeResource(IO.pure(5), release2); + + Resource> combine = res1.combine(res2); + + IO use = combine.use(tuple -> IO.task(tuple::toString)); + + assertEquals("(hola, 5)", use.unsafeRunSync()); + verify(release1).accept("hola"); + verify(release2).accept(5); + } +}