Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mutable State and the Observer Pattern

I'm currently redeveloping an application in F# and, while the experience has been excellent, I find myself a little bewildered when it comes to controlling mutability.

Previously, the document model used by my C# program was highly mutable and implemented ObservableCollections and INotifyPropertyChanged that shared state between views wouldn't bug out. Clearly, this isn't an ideal, especially if I want a fully immutable approach to my designs.

With that in mind I created a non-observable, immutable, document model for my underlying application kernel but, because I want a UI subscriber to see changes I immediately found myself implementing event-driven patterns:

// Raw data.
type KernelData = { DocumentContent : List<string> }

// Commands that act on the data.
type KernelCommands = { AddString : string -> () }

// A command implementation. Performs a state change, echos the new state through the event.
let addStringCommand (kernelState : KernelData) (kernelChanged : Event<KernelData>) (newString : string) =
    kernelState with { DocumentContent=oldList |> List.add newString }
    |> kernelChanged.Trigger

// Time to wire this up.
do
    // Create some starting state.
    let kernelData = { DocumentContent=List.Empty }

    // Create a shared event that commands may use to inform observers (UI).
    let kernelChangedEvent = new Event<KernelData>()

    // Create the command, it uses the event to inform observers.
    let kernelCommands = { AddString=addString kernelData kernelChangedEvent }

    // Create a UI element that uses the commands to initialize data transformations. UI elements subscribed to the data use the event to listen.
    let myUI = new UiObject(kernelData, kernelChangedEvent.Publish, kernelCommands)
    myUI.Show()

So this has been my solution to passing new state to the relevant listeners. However, what would be more ideal is a "box" I can "hook" into with transform functions. When the box mutates, functions are called to deal with the new state and produce corresponding changed state in a UI component.

do
    // Lambda called whenever the box changes.
    idealBox >>= (fun newModel -> new UIComponent(newModel))

So I guess I'm asking if there is an observable pattern for dealing with these situations. Mutable state is normally handled using monads but I've only seen examples which involve performing the operation (e.g. piping console IO monads, loading files, etc.) and not actually dealing with persistently mutating state.

like image 537
Adam Kewley Avatar asked Oct 02 '22 23:10

Adam Kewley


1 Answers

My general solution for these scenarios is to build all business logic in a purely functional setting and then provide a thin service layer with the necessary functionality for synchronizing and propagating changes. Here's an example of a pure interface for your KernelData type:

type KernelData = { DocumentContent : List<string> }
let emptyKernelData = {DocumentContent = []}
let addDocument c kData = {kData with DocumentContent = c :: kData.DocumentContent}

I would then define a service layer interface wrapping the functionality for modifying and subscribing to changes:

type UpdateResult = 
    | Ok
    | Error of string

/// Service interface
type KernelService =
{
    /// Gets the current kernel state.
    Current : unit -> KernelData

    /// Subscribes to state changes.
    Subscribe : (KernelData -> unit) -> IDisposable

    /// Modifies the current kernel state.
    Modify : (KernelData -> KernelData) -> Async<UpdateResult>
}

The Async responses enable non-blocking updates. The UpdateResult type is used to signal whether update operations succeeded or not. In order to build a sound KernelService object it's important to realize that modification requests need to by synchronized to avoid data loss from parallel updates. For this purpose MailboxProcessors come in handy. Here's a buildKernelService function that constructs a service interface given an initial KernelData object.

// Builds a service given an initial kernel data value.
let builKernelService (def: KernelData) =

    // Keeps track of the current kernel data state.
    let current = ref def

    // Keeps track of update events.
    let changes = new Event<KernelData>()

    // Serves incoming requests for getting the current state.
    let currentProc :  MailboxProcessor<AsyncReplyChannel<KernelData>> =
        MailboxProcessor.Start <| fun inbox ->
            let rec loop () =
                async {
                    let! chn = inbox.Receive ()
                    chn.Reply current.Value
                    return! loop ()
                }
            loop ()

    // Serves incoming 'modify requests'.
    let modifyProc : MailboxProcessor<(KernelData -> KernelData) * AsyncReplyChannel<UpdateResult>> =
        MailboxProcessor.Start <| fun inbox ->
            let rec loop () =
                async {
                    let! f, chn = inbox.Receive ()
                    let v = current.Value
                    try
                        current := f v
                        changes.Trigger current.Value
                        chn.Reply UpdateResult.Ok
                    with
                    | e ->
                        chn.Reply (UpdateResult.Error e.Message)
                    return! loop ()
                }
            loop ()
    {
        Current = fun () -> currentProc.PostAndReply id
        Subscribe = changes.Publish.Subscribe
        Modify = fun f -> modifyProc.PostAndAsyncReply (fun chn -> f, chn)
    }

Note that there is nothing in the implementation above that is unique to KernelData so the service interface along with the build function can be generalized to arbitrary types of internal states.

Finally, some examples of programming with KernelService objects:

// Build service object.
let service = builKernelService emptyKernelData

// Print current value.
let curr = printfn "Current state: %A" service.Current

// Subscribe 
let dispose = service.Subscribe (printfn "New State: %A")


// Non blocking update adding a document
service.Modify <| addDocument "New Document 1"

// Non blocking update removing all existing documents.
service.Modify (fun _ -> emptyKernelData)

// Blocking update operation adding a document.
async {
    let! res = service.Modify (addDocument "New Document 2")
    printfn "Update Result: %A" res
    return ()
}
|> Async.RunSynchronously

// Blocking update operation eventually failing.
async {
    let! res = 
        service.Modify (fun kernelState ->
            System.Threading.Thread.Sleep 10000
            failwith "Something terrible happened"
        )
    printfn "Update Result: %A" res
    return ()
}
|> Async.RunSynchronously

Besides the more technical details, I believe the most important difference from your original solution is that special command functions are not needed. Using the service layer, any pure function operating on KernelData (e.g addDocument) can be lifted into a stateful computation using the Modify function.

like image 123
esevelos Avatar answered Oct 17 '22 00:10

esevelos