From 2a4ac1ca72ab795c0c1998abe4d424227e2b18a0 Mon Sep 17 00:00:00 2001 From: Ivan Dugalic Date: Sun, 24 Apr 2022 18:06:54 +0200 Subject: [PATCH 1/5] Arrow - from `either` to `Effect` - Materialized View --- .../MaterializedViewArrowExtension.kt | 55 +++++++++++-------- .../application/MaterializedViewTest.kt | 12 ++-- gradle.properties | 4 +- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/MaterializedViewArrowExtension.kt b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/MaterializedViewArrowExtension.kt index 552cf8f8..40d41c16 100644 --- a/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/MaterializedViewArrowExtension.kt +++ b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/MaterializedViewArrowExtension.kt @@ -17,9 +17,9 @@ package com.fraktalio.fmodel.application import arrow.core.Either -import arrow.core.Either.Companion.catch -import arrow.core.Either.Left -import arrow.core.computations.either +import arrow.core.continuations.Effect +import arrow.core.continuations.effect +import arrow.core.nonFatalOrThrow import com.fraktalio.fmodel.application.Error.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch @@ -33,17 +33,21 @@ import kotlinx.coroutines.flow.map * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ -suspend fun MaterializedView.handleEither(event: E): Either { +suspend fun MaterializedView.handleEither(event: E): Effect { /** * Inner function - Computes new State based on the Event or fails. * * @param event of type [E] * @return The newly computed state of type [S] or [Error] */ - fun S?.eitherComputeNewStateOrFail(event: E): Either = - catch { - computeNewState(event) - }.mapLeft { throwable -> CalculatingNewViewStateFailed(this, event, throwable) } + fun S?.eitherComputeNewStateOrFail(event: E): Effect = + effect { + try { + computeNewState(event) + } catch (t: Throwable) { + shift(CalculatingNewViewStateFailed(this@eitherComputeNewStateOrFail, event, t.nonFatalOrThrow())) + } + } /** * Inner function - Fetch state - either version @@ -51,10 +55,14 @@ suspend fun MaterializedView.handleEither(event: E): Either, S?> = - catch { - fetchState() - }.mapLeft { throwable -> FetchingViewStateFailed(this, throwable) } + suspend fun E.eitherFetchStateOrFail(): Effect = + effect { + try { + fetchState() + } catch (t: Throwable) { + shift(FetchingViewStateFailed(this@eitherFetchStateOrFail, t.nonFatalOrThrow())) + } + } /** * Inner function - Save state - either version @@ -62,13 +70,16 @@ suspend fun MaterializedView.handleEither(event: E): Either, S> = - catch { - this.save() - }.mapLeft { throwable -> StoringStateFailed(this, throwable) } + suspend fun S.eitherSaveOrFail(): Effect = + effect { + try { + this@eitherSaveOrFail.save() + } catch (t: Throwable) { + shift(StoringStateFailed(this@eitherSaveOrFail, t.nonFatalOrThrow())) + } + } - // Arrow provides a Monad instance for Either. Except for the types signatures, our program remains unchanged when we compute over Either. All values on the left side assume to be Right biased and, whenever a Left value is found, the computation short-circuits, producing a result that is compatible with the function type signature. - return either { + return effect { event.eitherFetchStateOrFail().bind() .eitherComputeNewStateOrFail(event).bind() .eitherSaveOrFail().bind() @@ -83,10 +94,10 @@ suspend fun MaterializedView.handleEither(event: E): Either MaterializedView.handleEither(events: Flow): Flow> = +fun MaterializedView.handleEither(events: Flow): Flow> = events .map { handleEither(it) } - .catch { emit(Left(EventPublishingFailed(it))) } + .catch { emit(effect { shift(EventPublishingFailed(it)) }) } /** @@ -97,7 +108,7 @@ fun MaterializedView.handleEither(events: Flow): Flow E.publishEitherTo(materializedView: MaterializedView): Either = +suspend fun E.publishEitherTo(materializedView: MaterializedView): Effect = materializedView.handleEither(this) /** @@ -108,5 +119,5 @@ suspend fun E.publishEitherTo(materializedView: MaterializedView): * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ -fun Flow.publishEitherTo(materializedView: MaterializedView): Flow> = +fun Flow.publishEitherTo(materializedView: MaterializedView): Flow> = materializedView.handleEither(this) diff --git a/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/MaterializedViewTest.kt b/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/MaterializedViewTest.kt index 92b7f556..84a0b069 100644 --- a/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/MaterializedViewTest.kt +++ b/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/MaterializedViewTest.kt @@ -1,6 +1,7 @@ package com.fraktalio.fmodel.application import arrow.core.Either +import arrow.core.continuations.Effect import com.fraktalio.fmodel.application.examples.numbers.NumberViewRepository import com.fraktalio.fmodel.application.examples.numbers.even.query.EvenNumberViewRepository import com.fraktalio.fmodel.application.examples.numbers.even.query.evenNumberViewRepository @@ -22,7 +23,7 @@ import io.kotest.matchers.types.shouldBeInstanceOf /** * DSL - Given */ -private suspend fun IView.given(repository: ViewStateRepository, event: () -> E): Either = +private suspend fun IView.given(repository: ViewStateRepository, event: () -> E): Effect = materializedView( view = this, viewStateRepository = repository @@ -37,10 +38,11 @@ private fun IView.whenEvent(event: E): E = event /** * DSL - Then */ -private infix fun Either.thenState(expected: S) { - val state = when (this) { - is Either.Right -> value - is Either.Left -> throw AssertionError("Expected Either.Right, but found Either.Left with value ${this.value}") +private suspend infix fun Effect.thenState(expected: S) { + val result = this.toEither() + val state = when (result) { + is Either.Right -> result.value + is Either.Left -> throw AssertionError("Expected Either.Right, but found Either.Left with value ${result.value}") } return state shouldBe expected } diff --git a/gradle.properties b/gradle.properties index 9bdb0309..896e2a68 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,7 @@ kotlin.code.style=official -kotlin.mpp.enableGranularSourceSetsMetadata=true -kotlin.native.enableDependencyPropagation=false kotlin.js.generate.executable.default=false kotlin.mpp.stability.nowarn=true -kotlin.native.ignoreDisabledTargets=true +#kotlin.native.ignoreDisabledTargets=true kotlin.incremental=true # Kotlin Test configuration kotest.framework.parallelism=1 From 79337c7dcbe9e9040064af9e0764c3cc89f25ff9 Mon Sep 17 00:00:00 2001 From: Ivan Dugalic Date: Fri, 3 Jun 2022 19:37:56 +0200 Subject: [PATCH 2/5] To workaround the issue, added `-Xlazy-ir-for-caches=disable` --- application-arrow/build.gradle.kts | 3 +++ application-vanilla/build.gradle.kts | 3 +++ application/build.gradle.kts | 4 +++- domain/build.gradle.kts | 3 +++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/application-arrow/build.gradle.kts b/application-arrow/build.gradle.kts index f809e16e..1d0345c7 100644 --- a/application-arrow/build.gradle.kts +++ b/application-arrow/build.gradle.kts @@ -50,6 +50,9 @@ kotlin { nativeTarget.compilations.all { kotlinOptions.verbose = true } + nativeTarget.binaries.all { + freeCompilerArgs += "-Xlazy-ir-for-caches=disable" + } sourceSets { val commonMain by getting { diff --git a/application-vanilla/build.gradle.kts b/application-vanilla/build.gradle.kts index bbd7d693..24be2349 100644 --- a/application-vanilla/build.gradle.kts +++ b/application-vanilla/build.gradle.kts @@ -50,6 +50,9 @@ kotlin { nativeTarget.compilations.all { kotlinOptions.verbose = true } + nativeTarget.binaries.all { + freeCompilerArgs += "-Xlazy-ir-for-caches=disable" + } sourceSets { val commonMain by getting { diff --git a/application/build.gradle.kts b/application/build.gradle.kts index 03fcd97c..eb6ab735 100644 --- a/application/build.gradle.kts +++ b/application/build.gradle.kts @@ -28,7 +28,9 @@ kotlin { nativeTarget.compilations.all { kotlinOptions.verbose = true } - + nativeTarget.binaries.all { + freeCompilerArgs += "-Xlazy-ir-for-caches=disable" + } sourceSets { val commonMain by getting { dependencies { diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 39c52a77..bb472d37 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -49,6 +49,9 @@ kotlin { nativeTarget.compilations.all { kotlinOptions.verbose = true } + nativeTarget.binaries.all { + freeCompilerArgs += "-Xlazy-ir-for-caches=disable" + } sourceSets { val commonMain by getting { From aab6d3fa3f71ba81aa94a0fcad0ec32a30a6033f Mon Sep 17 00:00:00 2001 From: Ivan Dugalic Date: Sat, 4 Jun 2022 00:23:30 +0200 Subject: [PATCH 3/5] Arrow - from `either` to `Effect` - Aggregates, SagaManager --- .../EventSourcingAggregateArrowExtension.kt | 35 ++++---- .../MaterializedViewArrowExtension.kt | 47 +++++------ .../application/SagaManagerArrowExtension.kt | 32 +++---- .../StateStoredAggregateArrowExtension.kt | 83 ++++++++++--------- .../application/EventSourcedAggregateTest.kt | 10 ++- .../application/MaterializedViewTest.kt | 13 ++- .../application/StateStoredAggregateTest.kt | 21 ++--- 7 files changed, 124 insertions(+), 117 deletions(-) diff --git a/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/EventSourcingAggregateArrowExtension.kt b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/EventSourcingAggregateArrowExtension.kt index 345e7d9d..43a9d79a 100644 --- a/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/EventSourcingAggregateArrowExtension.kt +++ b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/EventSourcingAggregateArrowExtension.kt @@ -16,9 +16,8 @@ package com.fraktalio.fmodel.application -import arrow.core.Either -import arrow.core.Either.Left -import arrow.core.Either.Right +import arrow.core.continuations.Effect +import arrow.core.continuations.effect import com.fraktalio.fmodel.application.Error.CommandHandlingFailed import com.fraktalio.fmodel.application.Error.CommandPublishingFailed import kotlinx.coroutines.FlowPreview @@ -31,55 +30,53 @@ import kotlinx.coroutines.flow.map * Extension function - Handles the command message of type [C] * * @param command Command message of type [C] - * @return [Flow] of [Either] [Error] or Events of type [E] + * @return [Flow] of [Effect] (either [Error] or Events of type [E]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ @FlowPreview -fun EventSourcingAggregate.handleEither(command: C): Flow> = +fun EventSourcingAggregate.handleWithEffect(command: C): Flow> = command .fetchEvents() .computeNewEvents(command) .save() - .map { Right(it) } - .catch> { - emit(Left(CommandHandlingFailed(command))) - } + .map { effect { it } } + .catch { emit(effect { shift(CommandHandlingFailed(command)) }) } /** * Extension function - Handles the flow of command messages of type [C] * * @param commands [Flow] of Command messages of type [C] - * @return [Flow] of [Either] [Error] or Events of type [E] + * @return [Flow] of [Effect] (either [Error] or Events of type [E]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ @FlowPreview -fun EventSourcingAggregate.handleEither(commands: Flow): Flow> = +fun EventSourcingAggregate.handleWithEffect(commands: Flow): Flow> = commands - .flatMapConcat { handleEither(it) } - .catch { emit(Left(CommandPublishingFailed(it))) } + .flatMapConcat { handleWithEffect(it) } + .catch { emit(effect { shift(CommandPublishingFailed(it)) }) } /** * Extension function - Publishes the command of type [C] to the event sourcing aggregate of type [EventSourcingAggregate]<[C], *, [E]> * @receiver command of type [C] * @param aggregate of type [EventSourcingAggregate]<[C], *, [E]> - * @return the [Flow] of [Either] [Error] or successfully stored Events of type [E] + * @return the [Flow] of [Effect] (either [Error] or successfully stored Events of type [E]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ @FlowPreview -fun C.publishEitherTo(aggregate: EventSourcingAggregate): Flow> = - aggregate.handleEither(this) +fun C.publishWithEffect(aggregate: EventSourcingAggregate): Flow> = + aggregate.handleWithEffect(this) /** * Extension function - Publishes [Flow] of commands of type [C] to the event sourcing aggregate of type [EventSourcingAggregate]<[C], *, [E]> * @receiver [Flow] of commands of type [C] * @param aggregate of type [EventSourcingAggregate]<[C], *, [E]> - * @return the [Flow] of [Either] [Error] or successfully stored Events of type [E] + * @return the [Flow] of [Effect] (either [Error] or successfully stored Events of type [E]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ @FlowPreview -fun Flow.publishEitherTo(aggregate: EventSourcingAggregate): Flow> = - aggregate.handleEither(this) +fun Flow.publishWithEffect(aggregate: EventSourcingAggregate): Flow> = + aggregate.handleWithEffect(this) diff --git a/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/MaterializedViewArrowExtension.kt b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/MaterializedViewArrowExtension.kt index 40d41c16..4b2d92cb 100644 --- a/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/MaterializedViewArrowExtension.kt +++ b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/MaterializedViewArrowExtension.kt @@ -16,7 +16,6 @@ package com.fraktalio.fmodel.application -import arrow.core.Either import arrow.core.continuations.Effect import arrow.core.continuations.effect import arrow.core.nonFatalOrThrow @@ -29,23 +28,23 @@ import kotlinx.coroutines.flow.map * Extension function - Handles the event of type [E] * * @param event Event of type [E] to be handled - * @return [Either] [Error] or State of type [S] + * @return [Effect] (either [Error] or State of type [S]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ -suspend fun MaterializedView.handleEither(event: E): Effect { +suspend fun MaterializedView.handleWithEffect(event: E): Effect { /** * Inner function - Computes new State based on the Event or fails. * * @param event of type [E] * @return The newly computed state of type [S] or [Error] */ - fun S?.eitherComputeNewStateOrFail(event: E): Effect = + fun S?.computeNewStateWithEffect(event: E): Effect = effect { try { computeNewState(event) } catch (t: Throwable) { - shift(CalculatingNewViewStateFailed(this@eitherComputeNewStateOrFail, event, t.nonFatalOrThrow())) + shift(CalculatingNewViewStateFailed(this@computeNewStateWithEffect, event, t.nonFatalOrThrow())) } } @@ -53,14 +52,14 @@ suspend fun MaterializedView.handleEither(event: E): Effect = + suspend fun E.fetchStateWithEffect(): Effect = effect { try { fetchState() } catch (t: Throwable) { - shift(FetchingViewStateFailed(this@eitherFetchStateOrFail, t.nonFatalOrThrow())) + shift(FetchingViewStateFailed(this@fetchStateWithEffect, t.nonFatalOrThrow())) } } @@ -68,21 +67,21 @@ suspend fun MaterializedView.handleEither(event: E): Effect = + suspend fun S.saveWithEffect(): Effect = effect { try { - this@eitherSaveOrFail.save() + save() } catch (t: Throwable) { - shift(StoringStateFailed(this@eitherSaveOrFail, t.nonFatalOrThrow())) + shift(StoringStateFailed(this@saveWithEffect, t.nonFatalOrThrow())) } } return effect { - event.eitherFetchStateOrFail().bind() - .eitherComputeNewStateOrFail(event).bind() - .eitherSaveOrFail().bind() + event.fetchStateWithEffect().bind() + .computeNewStateWithEffect(event).bind() + .saveWithEffect().bind() } } @@ -90,13 +89,13 @@ suspend fun MaterializedView.handleEither(event: E): Effect MaterializedView.handleEither(events: Flow): Flow> = +fun MaterializedView.handleWithEffect(events: Flow): Flow> = events - .map { handleEither(it) } + .map { handleWithEffect(it) } .catch { emit(effect { shift(EventPublishingFailed(it)) }) } @@ -104,20 +103,20 @@ fun MaterializedView.handleEither(events: Flow): Flow * @receiver event of type [E] * @param materializedView of type [MaterializedView]<[S], [E]> - * @return [Either] [Error] or the successfully stored State of type [S] + * @return [Effect] (either [Error] or the successfully stored State of type [S]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ -suspend fun E.publishEitherTo(materializedView: MaterializedView): Effect = - materializedView.handleEither(this) +suspend fun E.publishWithEffect(materializedView: MaterializedView): Effect = + materializedView.handleWithEffect(this) /** * Extension function - Publishes the event of type [E] to the materialized view of type [MaterializedView]<[S], [E]> * @receiver [Flow] of events of type [E] * @param materializedView of type [MaterializedView]<[S], [E]> - * @return [Flow] of [Either] [Error] or the successfully stored State of type [S] + * @return [Flow] of [Effect] (either [Error] or the successfully stored State of type [S]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ -fun Flow.publishEitherTo(materializedView: MaterializedView): Flow> = - materializedView.handleEither(this) +fun Flow.publishWithEffect(materializedView: MaterializedView): Flow> = + materializedView.handleWithEffect(this) diff --git a/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/SagaManagerArrowExtension.kt b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/SagaManagerArrowExtension.kt index 86e4068a..65d7bde3 100644 --- a/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/SagaManagerArrowExtension.kt +++ b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/SagaManagerArrowExtension.kt @@ -16,9 +16,8 @@ package com.fraktalio.fmodel.application -import arrow.core.Either -import arrow.core.Either.Left -import arrow.core.Either.Right +import arrow.core.continuations.Effect +import arrow.core.continuations.effect import com.fraktalio.fmodel.application.Error.ActionResultHandlingFailed import com.fraktalio.fmodel.application.Error.ActionResultPublishingFailed import kotlinx.coroutines.FlowPreview @@ -31,51 +30,52 @@ import kotlinx.coroutines.flow.map * Extension function - Handles the action result of type [AR]. * * @param actionResult Action Result represent the outcome of some action you want to handle in some way - * @return [Flow] of [Either] [Error] or Actions of type [A] + * @return [Flow] of [Effect] (either [Error] or Actions of type [A]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ -fun SagaManager.handleEither(actionResult: AR): Flow> = +fun SagaManager.handleWithEffect(actionResult: AR): Flow> = actionResult .computeNewActions() .publish() - .map { Right(it) } - .catch> { emit(Left(ActionResultHandlingFailed(actionResult))) } + .map { effect { it } } + .catch { emit(effect { shift(ActionResultHandlingFailed(actionResult)) }) } /** * Extension function - Handles the the [Flow] of action results of type [AR]. * * @param actionResults Action Results represent the outcome of some action you want to handle in some way - * @return [Flow] of [Either] [Error] or Actions of type [A] + * @return [Flow] of [Effect] (either [Error] or Actions of type [A]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ @FlowPreview -fun SagaManager.handleEither(actionResults: Flow): Flow> = +fun SagaManager.handleWithEffect(actionResults: Flow): Flow> = actionResults - .flatMapConcat { handleEither(it) } - .catch { emit(Left(ActionResultPublishingFailed(it))) } + .flatMapConcat { handleWithEffect(it) } + .catch { emit(effect { shift(ActionResultPublishingFailed(it)) }) } /** * Extension function - Publishes the action result of type [AR] to the saga manager of type [SagaManager]<[AR], [A]> * @receiver action result of type [AR] * @param sagaManager of type [SagaManager]<[AR], [A]> - * @return the [Flow] of [Either] [Error] or successfully published Actions of type [A] + * @return the [Flow] of [Effect] (either [Error] or successfully published Actions of type [A]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ -fun AR.publishEitherTo(sagaManager: SagaManager): Flow> = sagaManager.handleEither(this) +fun AR.publishWithEffect(sagaManager: SagaManager): Flow> = + sagaManager.handleWithEffect(this) /** * Extension function - Publishes the action result of type [AR] to the saga manager of type [SagaManager]<[AR], [A]> * @receiver [Flow] of action results of type [AR] * @param sagaManager of type [SagaManager]<[AR], [A]> - * @return the [Flow] of [Either] [Error] or successfully published Actions of type [A] + * @return the [Flow] of [Effect] (either [Error] or successfully published Actions of type [A]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ @FlowPreview -fun Flow.publishEitherTo(sagaManager: SagaManager): Flow> = - sagaManager.handleEither(this) +fun Flow.publishWithEffect(sagaManager: SagaManager): Flow> = + sagaManager.handleWithEffect(this) diff --git a/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/StateStoredAggregateArrowExtension.kt b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/StateStoredAggregateArrowExtension.kt index b6f0c6fe..ea4039f9 100644 --- a/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/StateStoredAggregateArrowExtension.kt +++ b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/StateStoredAggregateArrowExtension.kt @@ -16,10 +16,9 @@ package com.fraktalio.fmodel.application -import arrow.core.Either -import arrow.core.Either.Companion.catch -import arrow.core.Either.Left -import arrow.core.computations.either +import arrow.core.continuations.Effect +import arrow.core.continuations.effect +import arrow.core.nonFatalOrThrow import com.fraktalio.fmodel.application.Error.* import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow @@ -30,52 +29,62 @@ import kotlinx.coroutines.flow.map * Extension function - Handles the command message of type [C] * * @param command Command message of type [C] - * @return Either [Error] or State of type [S] + * @return [Effect] (either [Error] or State of type [S]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ @FlowPreview -suspend fun StateStoredAggregate.handleEither(command: C): Either { +suspend fun StateStoredAggregate.handleWithEffect(command: C): Effect { /** * Inner function - Computes new State based on the previous State and the [command] or fails. * * @param command of type [C] - * @return [Either] the newly computed state of type [S] or [Error] + * @return [Effect] (either the newly computed state of type [S] or [Error]) */ - suspend fun S?.eitherComputeNewStateOrFail(command: C): Either = - catch { - computeNewState(command) - }.mapLeft { throwable -> - CalculatingNewStateFailed(this, command, throwable) + suspend fun S?.computeNewStateWithEffect(command: C): Effect = + effect { + try { + computeNewState(command) + } catch (t: Throwable) { + shift(CalculatingNewStateFailed(this@computeNewStateWithEffect, command, t.nonFatalOrThrow())) + } } /** * Inner function - Fetch state - either version * * @receiver Command of type [C] - * @return [Either] [Error] or the State of type [S]? + * @return [Effect] (either [Error] or the State of type [S]?) */ - suspend fun C.eitherFetchStateOrFail(): Either, S?> = - catch { - fetchState() - }.mapLeft { throwable -> FetchingStateFailed(this, throwable) } + suspend fun C.fetchStateWithEffect(): Effect = + effect { + try { + fetchState() + } catch (t: Throwable) { + shift(FetchingStateFailed(this@fetchStateWithEffect, t.nonFatalOrThrow())) + } + } /** * Inner function - Save state - either version * * @receiver State of type [S] - * @return [Either] [Error] or the newly saved State of type [S] + * @return [Effect] (either [Error] or the newly saved State of type [S]) */ - suspend fun S.eitherSaveOrFail(): Either, S> = - catch { - save() - }.mapLeft { throwable -> StoringStateFailed(this, throwable) } + suspend fun S.saveWithEffect(): Effect = + effect { + try { + save() + } catch (t: Throwable) { + shift(StoringStateFailed(this@saveWithEffect, t.nonFatalOrThrow())) + } + } - // Arrow provides a Monad instance for Either. Except for the types signatures, our program remains unchanged when we compute over Either. All values on the left side assume to be Right biased and, whenever a Left value is found, the computation short-circuits, producing a result that is compatible with the function type signature. - return either { - command.eitherFetchStateOrFail().bind() - .eitherComputeNewStateOrFail(command).bind() - .eitherSaveOrFail().bind() + return effect { + command + .fetchStateWithEffect().bind() + .computeNewStateWithEffect(command).bind() + .saveWithEffect().bind() } } @@ -83,36 +92,36 @@ suspend fun StateStoredAggregate.handleEither(command: C): Ei * Extension function - Handles the [Flow] of command messages of type [C] * * @param commands [Flow] of Command messages of type [C] - * @return [Flow] of [Either] [Error] or State of type [S] + * @return [Flow] of [Effect] (either [Error] or State of type [S]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ @FlowPreview -fun StateStoredAggregate.handleEither(commands: Flow): Flow> = +fun StateStoredAggregate.handleWithEffect(commands: Flow): Flow> = commands - .map { handleEither(it) } - .catch { emit(Left(CommandPublishingFailed(it))) } + .map { handleWithEffect(it) } + .catch { emit(effect { shift(CommandPublishingFailed(it)) }) } /** * Extension function - Publishes the command of type [C] to the state stored aggregate of type [StateStoredAggregate]<[C], [S], *> * @receiver command of type [C] * @param aggregate of type [StateStoredAggregate]<[C], [S], *> - * @return [Either] [Error] or successfully stored State of type [S] + * @return [Effect] (either [Error] or successfully stored State of type [S]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ @FlowPreview -suspend fun C.publishEitherTo(aggregate: StateStoredAggregate): Either = - aggregate.handleEither(this) +suspend fun C.publishWithEffect(aggregate: StateStoredAggregate): Effect = + aggregate.handleWithEffect(this) /** * Extension function - Publishes the command of type [C] to the state stored aggregate of type [StateStoredAggregate]<[C], [S], *> * @receiver [Flow] of commands of type [C] * @param aggregate of type [StateStoredAggregate]<[C], [S], *> - * @return the [Flow] of [Either] [Error] or successfully stored State of type [S] + * @return the [Flow] of [Effect] (either [Error] or successfully stored State of type [S]) * * @author Иван Дугалић / Ivan Dugalic / @idugalic */ @FlowPreview -fun Flow.publishEitherTo(aggregate: StateStoredAggregate): Flow> = - aggregate.handleEither(this) +fun Flow.publishWithEffect(aggregate: StateStoredAggregate): Flow> = + aggregate.handleWithEffect(this) diff --git a/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/EventSourcedAggregateTest.kt b/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/EventSourcedAggregateTest.kt index 7e6135dd..dd2ebffb 100644 --- a/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/EventSourcedAggregateTest.kt +++ b/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/EventSourcedAggregateTest.kt @@ -3,6 +3,7 @@ package com.fraktalio.fmodel.application import arrow.core.Either import arrow.core.Either.Left import arrow.core.Either.Right +import arrow.core.continuations.Effect import com.fraktalio.fmodel.application.examples.numbers.NumberRepository import com.fraktalio.fmodel.application.examples.numbers.even.command.EvenNumberRepository import com.fraktalio.fmodel.application.examples.numbers.even.command.evenNumberRepository @@ -19,6 +20,7 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.collections.shouldContainExactly import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList /** @@ -28,11 +30,11 @@ import kotlinx.coroutines.flow.toList private fun IDecider.given( repository: EventRepository, command: () -> C -): Flow> = +): Flow> = eventSourcingAggregate( decider = this, eventRepository = repository - ).handleEither(command()) + ).handleWithEffect(command()) /** * DSL - When @@ -43,8 +45,8 @@ private fun IDecider.whenCommand(command: C): C = command /** * DSL - Then */ -private suspend infix fun Flow>.thenEvents(expected: Iterable>) = - toList() shouldContainExactly (expected) +private suspend infix fun Flow>.thenEvents(expected: Iterable>) = + map { it.toEither() }.toList() shouldContainExactly (expected) /** * Event sourced aggregate test diff --git a/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/MaterializedViewTest.kt b/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/MaterializedViewTest.kt index 84a0b069..eed319e6 100644 --- a/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/MaterializedViewTest.kt +++ b/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/MaterializedViewTest.kt @@ -27,7 +27,7 @@ private suspend fun IView.given(repository: ViewStateRepository IView.whenEvent(event: E): E = event * DSL - Then */ private suspend infix fun Effect.thenState(expected: S) { - val result = this.toEither() - val state = when (result) { + val state = when (val result = this.toEither()) { is Either.Right -> result.value is Either.Left -> throw AssertionError("Expected Either.Right, but found Either.Left with value ${result.value}") } return state shouldBe expected } -private fun Either.thenError() { - val error = when (this) { - is Either.Right -> throw AssertionError("Expected Either.Left, but found Either.Right with value ${this.value}") - is Either.Left -> value +private suspend fun Effect.thenError() { + val error = when (val result = this.toEither()) { + is Either.Right -> throw AssertionError("Expected Either.Left, but found Either.Right with value ${result.value}") + is Either.Left -> result.value } error.shouldBeInstanceOf() } diff --git a/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/StateStoredAggregateTest.kt b/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/StateStoredAggregateTest.kt index 3f5844f1..274f3c21 100644 --- a/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/StateStoredAggregateTest.kt +++ b/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/StateStoredAggregateTest.kt @@ -1,6 +1,7 @@ package com.fraktalio.fmodel.application import arrow.core.Either +import arrow.core.continuations.Effect import com.fraktalio.fmodel.application.examples.numbers.NumberStateRepository import com.fraktalio.fmodel.application.examples.numbers.even.command.EvenNumberStateRepository import com.fraktalio.fmodel.application.examples.numbers.even.command.evenNumberStateRepository @@ -26,11 +27,11 @@ import kotlinx.coroutines.FlowPreview private suspend fun IDecider.given( repository: StateRepository, command: () -> C -): Either = +): Effect = stateStoredAggregate( decider = this, stateRepository = repository - ).handleEither(command()) + ).handleWithEffect(command()) /** * DSL - When @@ -41,18 +42,18 @@ private fun IDecider.whenCommand(command: C): C = command /** * DSL - Then */ -private infix fun Either.thenState(expected: S) { - val state = when (this) { - is Either.Right -> value - is Either.Left -> throw AssertionError("Expected Either.Right, but found Either.Left with value ${this.value}") +private suspend infix fun Effect.thenState(expected: S) { + val state = when (val result = this.toEither()) { + is Either.Right -> result.value + is Either.Left -> throw AssertionError("Expected Either.Right, but found Either.Left with value ${result.value}") } return state shouldBe expected } -private fun Either.thenError() { - val error = when (this) { - is Either.Right -> throw AssertionError("Expected Either.Left, but found Either.Right with value ${this.value}") - is Either.Left -> value +private suspend fun Effect.thenError() { + val error = when (val result = this.toEither()) { + is Either.Right -> throw AssertionError("Expected Either.Left, but found Either.Right with value ${result.value}") + is Either.Left -> result.value } error.shouldBeInstanceOf() } From 2a3aeb8a77b8ba2a3e945a435cf405e6163a8eab Mon Sep 17 00:00:00 2001 From: Ivan Dugalic Date: Sat, 4 Jun 2022 00:36:21 +0200 Subject: [PATCH 4/5] Arrow - from `either` to `Effect` - source code reformat/clean --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 10a467ec..e4fa23ad 100644 --- a/README.md +++ b/README.md @@ -199,8 +199,10 @@ Notice that `Decider` implements an interface `IDecider` to communicate the cont - with identity element `Decider` -> A monoid is a type together with a binary operation (combine) over that type, satisfying associativity and having an identity/empty element. -> Associativity facilitates parallelization by giving us the freedom to break problems into chunks that can be computed in parallel. +> A monoid is a type together with a binary operation (combine) over that type, satisfying associativity and having an +> identity/empty element. +> Associativity facilitates parallelization by giving us the freedom to break problems into chunks that can be computed +> in parallel. We can now construct event-sourcing or/and state-storing aggregate by using the same `decider`. @@ -318,8 +320,10 @@ Notice that `View` implements an interface `IView` to communicate the contract. - `View.combine(y: View): View, Pair, E_SUPER>` - with identity element `View` -> A monoid is a type together with a binary operation (combine) over that type, satisfying associativity and having an identity/empty element. -> Associativity facilitates parallelization by giving us the freedom to break problems into chunks that can be computed in parallel. +> A monoid is a type together with a binary operation (combine) over that type, satisfying associativity and having an +> identity/empty element. +> Associativity facilitates parallelization by giving us the freedom to break problems into chunks that can be computed +> in parallel. We can now construct `materialized` view by using this `view`. @@ -446,7 +450,8 @@ private fun CoroutineScope.commandActor( } ``` -> [Actors](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html) are marked as @ObsoleteCoroutinesApi by Kotlin at the moment. +> [Actors](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/actor.html) +> are marked as @ObsoleteCoroutinesApi by Kotlin at the moment. ## Kotlin From 6969dd7bfdb04d12c894a00544aa22b93567e593 Mon Sep 17 00:00:00 2001 From: Ivan Dugalic Date: Sat, 4 Jun 2022 10:35:02 +0200 Subject: [PATCH 5/5] Arrow - from `either` to `Effect` - typo fixed --- .../fraktalio/fmodel/application/SagaManagerArrowExtension.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/SagaManagerArrowExtension.kt b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/SagaManagerArrowExtension.kt index 65d7bde3..04d2da6f 100644 --- a/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/SagaManagerArrowExtension.kt +++ b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/SagaManagerArrowExtension.kt @@ -42,7 +42,7 @@ fun SagaManager.handleWithEffect(actionResult: AR): Flow