Mutated State and Observer Pattern

I am currently processing the application in F #, and although the experience was excellent, I am a little overwhelmed when it comes to controlling for variability.

Previously, the document model used by my C # program was very volatile and implemented by ObservableCollections and INotifyPropertyChanged, that the general state between views was not an error. It is clear that this is not ideal, especially if I want a completely unchanged approach to my projects.

With this in mind, I created an invisible, immutable document model for my application kernel, but since I want the user interface subscriber to see the changes, I immediately found that I was implementing event-driven templates:

// 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, it was my decision to convey the new state to the appropriate listeners. However, what would be more ideal is a β€œbox” with which I can β€œconnect” to the conversion functions. When a field mutates, functions are called to work with the new state and create the corresponding changed state in the user interface component.

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

So, I think, I ask if there is an observable picture to solve these situations. A Mutable state is usually handled using monads, but I have only seen examples that include performing an operation (such as IU I / O consoles, downloading files, etc.), rather than an actual action with a constantly changing state.

+4
source share
1 answer

My common solution for these scenarios is to build the entire business logic in a purely functional setting, and then provide a thin layer of service with the necessary functionality to synchronize and propagate the changes. Here is a clean interface example for your KernelData type:

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

Then I would define a service level interface that wraps the functionality for changing 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> } 

Async answers let you block updates. The UpdateResult type UpdateResult used to determine whether update operations succeeded or not. To create a KernelService sound object, it is important to understand that modification requests must be synchronized to avoid data loss from concurrent updates. MailboxProcessor in handy for this. Here is a buildKernelService function that builds the service interface specified by the 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 := fv 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) } 

Please note that in the implementation above there is nothing that is unique to KernelData , therefore the service interface together with the build function can be generalized to arbitrary types of internal states.

Finally, some programming examples 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 that the most important difference from your original solution is that special command functions are not needed. Using the service level, any pure function that works with KernelData (for example, addDocument) can be raised to state calculation using the Modify function.

+2
source

Source: https://habr.com/ru/post/957833/


All Articles