Is there any way to map a Behavior from one type to another

210 views Asked by At

I have a scenario where I need to provide a Behavior of a specific type. This Behavior also needs to handle events that are published on the Event Stream. So say the specific type is:

case class DoSomething(i: Int)

and I then need to implement a function to return a Behavior to handle this type of message:

def foo(): Behavior[DoSomething]

I then also need to handle the following message on the event stream:

case class PublishedEvent(str: String)

The only solution I came up with was to spawn another actor from within my DoSomething behavior and then forward messages to it:

sealed trait Command
case class Command1(str: String) extends Command
case class Command2(str: String) extends Command

def foo(): Behavior[DoSomething] = Behaviors.setup { context =>
    val actor = context.spawnAnonymous[Command](Behaviors.setup { context =>
        context.system.eventStream ! EventStream.Subscribe(context.messageAdapter {
            case PublishedEvent(str) => Command2(str)
        })
        Behaviors.receiveMessage {
            case Command1(str) =>
                println("Received Command1: " + str)
                Behaviors.same
            case Command2(str) =>
                println("Received Command1: " + str)
                Behaviors.same
        }
    })
    Behaviors.receiveMessage {
        case DoSomething(i) =>
            actor ! Command1(i.toString)
            Behaviors.same
    }
}

My question is is there any means of avoiding spawning a new actor and doing it all from within the same actor? i.e. Is there a way I can map a Behavior[Command] to a Behavior[DoSomething]?

2

There are 2 answers

0
Levi Ramsey On

The docs (as well as the signature) indicate that transformMessages on Behavior would work, provided that the externally sent messages map 1:1 to internal messages:

def transformMessages[Outer: ClassTag](matcher: PartialFunction[Outer, T]): Behavior[Outer]

i.e. an actor materialized with Behavior[T].transformMessages[Outer] { ??? } will yield an ActorRef[Outer]

sealed trait External

case class DoSomething(i: Int) extends External

private sealed trait Internal

private case class Command1(str: String) extends Internal
private case class Command2(str: String) extends Internal

private def internalBehavior: Behavior[Internal] = Behaviors.setup { context =>
  // do stuff with context
  Behaviors.receiveMessage {
    case Command1(s) => ???
    case Command2(s) => ???
  }
}

import akka.actor.typed.Behavior.BehaviorDecorators
def externalBehavior: Behavior[External] =
  internalBehavior.transformMessages {
    case DoSomething(i) => Command1(i.toString)
  }

In this case, the internal commands are invisible outside of their scope. The main potential drawback I see (beyond the slight ickiness of bringing a PartialFunction into typed) is that within an actor with Behavior[Internal], you can't get the ActorRef for actor's external "personality", unless you have something crazy like

 case class DoSomething(i: Int, recipient: ActorRef[External]) extends External

 private case class Command1(str: String, externalPersonality: ActorRef[External]) extends Internal

It's worth noting that in the case where the internal messages are under your control, the 1:1 restriction can be worked around.

0
lex82 On

I also have this problem regularly and the best solution I found is to use the narrow[...] method on a common "super Behavior" to narrow down on a message type a client is supposed to send to the respective actor. It is possible to combine the handling of both message types in one Behavior by defining a common interface (in scala of course a trait):

sealed trait CommandOrDoSomething // #TODO find better name

final case class DoSomething(i:Int) extends CommandOrDoSomething

sealed trait Command extends CommandOrDoSomething
case class Command1(str: String) extends Command
case class Command2(str: String) extends Command

def getBehavior(): Behavior[CommandOrDoSomething] = Behaviors.setup { context =>
    Behaviors.receiveMessage {
        // handle all kinds of messages here
    }
}

Now, if you want to pass a Behavior[DoSomething] to a client, you can simply obtain it like this:

def foo(): Behavior[DoSomething] = getBehavior().narrow[DoSomething]

You can keep the Command trait private to your current context, but I think you will have to expose at least the common super trait to the outside world. With union types as they will be available in Scala 3 this can be avoided and you won't need the artificial super trait.