Search code examples
f#immutabilityactorakka.net

How to avoid mutable reference to Actor instance


I'm trying to gain some experience with Akka.NET actors in F#. I have the scenario when one actor needs to spin up another actor as its child. The first actor would convert each message and then send the result to the other actor. I use actorOf2 function to spawn actors. Here is my code:

let actor1 work1 work2 =
  let mutable actor2Ref = null
  let imp (mailbox : Actor<'a>) msg =
    let result = work1 msg
    if actor2Ref = null then 
      actor2Ref <- spawn mailbox.Context "decide-actor" (actorOf2 <| work2)
    actor2Ref <! result
  imp

let actor1Ref = actor1 work1' work2'
  |> actorOf2 
  |> spawn system "my-actor"

What I don't like is the mutable actor reference. But I had to make it mutable because I need mailbox.Context to spawn a child actor, and I don't have any context before the first call. I saw this question but I don't know how to apply it to my function.

In a more advanced scenario I need a collection of child actors which is partitioned by a key. I'm using a Dictionary of actor refs in this scenario. Is there a better (more F#-ish) way?


Solution

  • In order to keep your "state" across iterations, you need to make the iterations explicit. That way, you can pass the current "state" as tail call argument. Just as in the question you linked:

    let actor1 work1 work2 (mailbox : Actor<'a>) =
      let rec imp actor2 =
        actor {
          let! msg = mailbox.Receive()
          let result = work1 msg
    
          let actor2 =
            match actor2 with
            | Some a -> a // Already spawned on a previous iteration
            | None -> spawn mailbox.Context "decide-actor" (actorOf2 <| work2)
    
          actor2 <! result
          return! imp (Some actor2)
        }
    
      imp None
    

    And now, you don't need to use actorOf2 or actorOf for spawning this actor, because it already has the right signature:

    let actor1Ref = 
      actor1 work1' work2'
      |> spawn system "my-actor"
    

    .
    EDIT
    If you're concerned about the extra boilerplate, nothing prevents you from packing the boilerplate away as a function (after all, actorOf2 does something similar):

    let actorOfWithState (f: Actor<'msg> -> 'state -> 'msg -> 'state) (initialState: 'state) mailbox =
      let rec imp state =
        actor {
          let! msg = mailbox.Receive()
          let newState = f mailbox state msg
          return! imp newState
        }
    
      imp initialState
    

    And then:

    let actor1 work1 work2 (mailbox : Actor<'a>) actor2 msg =
      let result = work1 msg
      let actor2 =
        match actor2 with 
        | Some a -> a
        | None -> spawn mailbox.Context "decide-actor" (actorOf2 work2)
    
      actor2 <! result
      actor2
    
    let actor1Ref = 
      actor1 work1' work2'
      |> actorOfWithState
      |> spawn system "my-actor"