From 54da96844b9581224a353c1012ec8c2c85810e44 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Sat, 4 May 2019 15:38:34 +0200 Subject: feat: Improve DSL for building Behaviors This change adds methods to improve the composability of multiple Behaviors, such as joining together two behaviors. Furthermore, this change adds methods for juggling with Behavior message types. --- .../src/main/kotlin/com/atlarge/odcsim/ActorRef.kt | 2 +- .../src/main/kotlin/com/atlarge/odcsim/Behavior.kt | 38 +++++++-- .../main/kotlin/com/atlarge/odcsim/Behaviors.kt | 96 +++++++++++++++++++--- 3 files changed, 118 insertions(+), 18 deletions(-) (limited to 'odcsim-core/src/main') diff --git a/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorRef.kt b/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorRef.kt index 2fe97d59..251d0669 100644 --- a/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorRef.kt +++ b/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorRef.kt @@ -44,7 +44,7 @@ interface ActorRef : Comparable>, Serializable { /** * Unsafe helper method for widening the type accepted by this [ActorRef]. */ -fun ActorRef.upcast(): ActorRef { +fun ActorRef.unsafeUpcast(): ActorRef { @Suppress("UNCHECKED_CAST") return this as ActorRef } diff --git a/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behavior.kt b/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behavior.kt index 411915ef..8298ed28 100644 --- a/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behavior.kt +++ b/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behavior.kt @@ -43,18 +43,36 @@ sealed class Behavior { * This is almost always a safe operation, but might cause [ClassCastException] in case a narrowed behavior sends * messages of a different type to itself and is chained via [Behavior.orElse]. */ - fun narrow(): Behavior { - @Suppress("UNCHECKED_CAST") - return this as Behavior + fun narrow(): Behavior = unsafeCast() + + /** + * Widen the type of this behavior by placing a funnel in front of it. + * + * @param transform The mapping from the widened type to the original type, returning `null` in-case the message + * should not be handled. + */ + fun widen(transform: (U) -> T?): Behavior { + return wrap(this) { interpreter -> + receive { ctx, msg -> + val res = transform(msg) + @Suppress("UNCHECKED_CAST") + if (res == null || interpreter.interpretMessage(ctx as ActorContext, res)) + unhandled() + else + interpreter.behavior.unsafeCast() + }.unsafeCast() + }.unsafeCast() } /** * Compose this [Behavior] with a fallback [Behavior] which is used in case this [Behavior] does not handle the * incoming message or signal. + * + * @param that The fallback behavior. */ - fun orElse(behavior: Behavior): Behavior = + fun orElse(that: Behavior): Behavior = wrap(this) { left -> - wrap(behavior) { right -> + wrap(that) { right -> object : ReceivingBehavior() { override fun receive(ctx: ActorContext, msg: T): Behavior { if (left.interpretMessage(ctx, msg)) { @@ -78,6 +96,16 @@ sealed class Behavior { } } } + + /** + * Unsafe utility method for changing the type accepted by this [Behavior]. + * Be aware that changing the type might result in [ClassCastException], when sending a message to the resulting + * behavior. + */ + fun unsafeCast(): Behavior { + @Suppress("UNCHECKED_CAST") + return this as Behavior + } } /** diff --git a/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behaviors.kt b/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behaviors.kt index 4dea1007..f2736437 100644 --- a/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behaviors.kt +++ b/odcsim-core/src/main/kotlin/com/atlarge/odcsim/Behaviors.kt @@ -32,27 +32,18 @@ import com.atlarge.odcsim.internal.IgnoreBehavior * This [Behavior] is used to signal that this actor shall terminate voluntarily. If this actor has created child actors * then these will be stopped as part of the shutdown procedure. */ -fun stopped(): Behavior { - @Suppress("UNCHECKED_CAST") - return StoppedBehavior as Behavior -} +fun stopped(): Behavior = StoppedBehavior.unsafeCast() /** * This [Behavior] is used to signal that this actor wants to reuse its previous behavior. */ -fun same(): Behavior { - @Suppress("UNCHECKED_CAST") - return SameBehavior as Behavior -} +fun same(): Behavior = SameBehavior.unsafeCast() /** * This [Behavior] is used to signal to the system that the last message or signal went unhandled. This will * reuse the previous behavior. */ -fun unhandled(): Behavior { - @Suppress("UNCHECKED_CAST") - return UnhandledBehavior as Behavior -} +fun unhandled(): Behavior = UnhandledBehavior.unsafeCast() /** * A factory for [Behavior]. Creation of the behavior instance is deferred until the actor is started. @@ -83,6 +74,21 @@ fun receive(handler: (ActorContext, T) -> Behavior): Behavior } } +/** + * Construct a [Behavior] that reacts to incoming messages of type [U], provides access to the [ActorContext] and + * returns the actor's next behavior. Other messages will be unhandled. + */ +inline fun receiveOf(crossinline handler: (ActorContext, U) -> Behavior): Behavior { + return object : ReceivingBehavior() { + override fun receive(ctx: ActorContext, msg: T): Behavior { + return if (msg is U) + handler(ctx, msg) + else + unhandled() + } + } +} + /** * Construct a [Behavior] that reacts to incoming messages and returns the actor's next behavior. */ @@ -109,3 +115,69 @@ fun receiveSignal(handler: (ActorContext, Signal) -> Behavior): fun wrap(behavior: Behavior, wrap: (BehaviorInterpreter) -> Behavior): Behavior { return setup { ctx -> wrap(BehaviorInterpreter(behavior, ctx)) } } + +/** + * Join together both [Behavior] with another [Behavior], essentially running them side-by-side, only directly + * propagating stopped behavior. + * + * @param that The behavior to join with. + */ +fun Behavior.join(that: Behavior): Behavior = + wrap(this) { left -> + wrap(that) { right -> + object : ReceivingBehavior() { + override fun receive(ctx: ActorContext, msg: T): Behavior { + if (left.interpretMessage(ctx, msg)) { + return left.propagate(this) // Propagate stopped behavior + } else if (right.interpretMessage(ctx, msg)) { + return right.propagate(this) + } + + return unhandled() + } + + override fun receiveSignal(ctx: ActorContext, signal: Signal): Behavior { + if (left.interpretSignal(ctx, signal)) { + return left.propagate(this) + } else if (right.interpretSignal(ctx, signal)) { + return right.propagate(this) + } + + return unhandled() + } + } + } + } + +/** + * Widen the type of messages the [Behavior] by marking all other messages as unhandled. + */ +inline fun Behavior.widen(): Behavior = widen { + if (it is T) + it + else + null +} + +/** + * Keep the specified [Behavior] alive if it returns the stopped behavior. + */ +fun Behavior.keepAlive(): Behavior = + wrap(this) { interpreter -> + object : ReceivingBehavior() { + override fun receive(ctx: ActorContext, msg: T): Behavior { + if (interpreter.interpretMessage(ctx, msg)) { + return this + } + return empty() + } + + override fun receiveSignal(ctx: ActorContext, signal: Signal): Behavior { + if (interpreter.interpretSignal(ctx, signal)) { + return this + } + + return empty() + } + } + } -- cgit v1.2.3