I have a problem I am tring to create some sort of mediator based on singelton in f#. Main reason for it is I need to register some action in one part of my application and execute it in another similar to c# that looks like:
public class Mediator
{
private static readonly object syncRoot = new object();
private static Mediator instance;
public static Mediator Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
{
instance = new Mediator();
}
}
}
return instance;
}
}
public event Action<string> InputLogUpdate;
public void InvokeInputLogUpdate(string msg)
{
InputLogUpdate?.Invoke(msg);
}
}
Invoke looks like:
Mediator.Instance.InvokeInputLogUpdate(msg);
Register looks like:
Mediator.Instance.InputLogUpdate += msg => this.DebugSection += $"{msg} \n";
But as for now no luck, all I have got is:
vnamespace Operation.Mediator
module Mediator =
open System
type UdpMediator private () =
static let instance = UdpMediator()
let mutable UpdateDebigLog : Action<string> = null
static member Instance = instance
member this.InvokeUpdateLog (msg:string) = UpdateDebigLog.Invoke(msg)
And it does not work very well, any one know how to do this?
First, that pattern of a lock sandwiched between two null checks is so antiquated, I don't even remember the last time I saw one. The modern .NET has Lazy<T> for such on-demand initialization. And, as luck would have it, F# actually has special syntax for it:
type UpdMediator private () =
static member val private _instance = lazy UpdMediator()
static member Instance = UpdMediator._instance.Value
Second, an event Action<_> in C# is not equivalent to mutable Action<_> in F#. An event declaration in C# actually creates two accessor methods - add and remove, - just like get and set with properties.
However, F# actually has a better mechanism - Event<_>. You make an instance of that type private, and then expose it via its .Publish property, which has type IEvent<_>. The consumers can use that IEvent value to add and remove handlers, or to subscribe to the events observable-style (because IEvent inherits from IObservable):
type UpdMediator private () =
...
member val private _updDebugLog = Event<string>()
member this.UpdDebugLog = this._updDebugLog.Publish
member this.InvokeUpdDebugLog() = this._updDebugLog.Trigger "foo"
// usage:
UpdMediator.Instance.UpdDebugLog.AddHandler (fun sender str -> printfn "%s" str)
// Or observable-style:
UpdMediator.Instance.UpdDebugLog.Subscribe (printfn "%s")
(note that .Subscribe returns an IDisposable, which can be used later to cancel the subscription)
Third, attempting to translate C# practices into F# word for word usually does not end well. F# usually has better facilities than C#, so a word-for-word translation usually ends up kinda awkward, as your case illustrates.
Trying to use C#, OOP-style design patterns in F# usually results in WAY overcomplicating things compared to the natural way of expressing things in idiomatic F#. What you have said you need is a way to have one part of your code register an action, and another part of your code execute the registered action if it has been registered. Since you're using events, I assume you want to be able to register multiple actions and have all of them fire. So here's some code that would do just that:
module ActionRegistry
let mutable private actions : (string -> unit) list = []
let register f =
actions <- f :: actions
let invokeRegisteredActions() =
actions
|> List.iter (fun action -> action())
Now isn't that a LOT simpler to understand than a singleton Mediator class? OOP-style design patterns always overcomplicate things.
P.S. You haven't mentioned unregistering actions, so I left the design simple and didn't include an unregistration function. If you do need to unregister actions, I'd do something like keep a list of (int, action) pairs, with a simple incrementing counter ensuring that the int part of the pair keeps going up. Then the register function would return an int, and the unregister function would take an int and filter out the pair with that int in the first position.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With