How to create a plug-in system in a functional style?

Denial of responsibility:
Although I accept the gospel of an unchanging state and functions of a higher order, my real experience is still 95% object oriented. I would like to change that, but whatchagonnado. Therefore, my brain is very connected to OO.

Question:
I have this situation very often: part of the business functionality is implemented as a small “core” plus a lot of “plugins” working together to present the user with a seemingly solid surface. I have found that this microkernel architecture works very well in many circumstances. In addition, it is very convenient, it goes well with the DI container, which can be used to detect plugins.

So how to do it functionally ?

I do not think that the main idea in this method is inherently object-oriented, because I just described it without using any terms or concepts of OO. However, I cannot completely wrap my head around the functional way around this. Of course, I can present plugins as functions (or buckets of functions), but the difficult part arises when plugins must have their own data as part of a large image, and the data form is a form plugin for the plugin.

Below is a small fragment of F #, which is a more or less literal translation of C # code that I would write when implementing this template from scratch.
Note the weaknesses : the loss of type information in CreateData needs to be increased in PersistData .
Every time I flinch when throwing (up or down), but I learned to take them as a necessary evil in C #. However, my past experience suggests that a functional approach often offers unexpected, but beautiful and elegant solutions to such problems. It is such a decision that I follow him.

 type IDataFragment = interface end type PersistedData = string // Some format used to store data in persistent storage type PluginID = string // Some form of identity for plugins that would survive app restart/rebuild/upgrade type IPlugin = interface abstract member UniqueID: PluginID abstract member CreateData: unit -> IDataFragment // NOTE: Persistence is conflated with primary function for simplicity. // Regularly, persistence would be handled by a separate component. abstract member PersistData: IDataFragment -> PersistedData option abstract member LoadData: PersistedData -> IDataFragment end type DataFragment = { Provider: IPlugin; Fragment: IDataFragment } type WholeData = DataFragment list // persist: WholeData -> PersistedData let persist wholeData = let persistFragmt { Provider = provider; Fragment = fmt } = Option.map (sprintf "%s: %s" provider.UniqueID) (provider.PersistData fmt) let fragments = wholeData |> Seq.map persistFragmt |> Seq.filter Option.isSome |> Seq.map Option.get String.concat "\n" fragments // Not a real serialization format, simplified for example // load: PersistedData -> WholeData let load persistedData = // Discover plugins and parse the above format, omitted // Reference implementation of a plugin module OnePlugin = type private MyData( d: string ) = interface IDataFragment member x.ActualData = d let create() = {new IPlugin with member x.UniqueID = "one plugin" member x.CreateData() = MyData( "whatever" ) :> _ member x.LoadData d = MyData( d ) :> _ member x.PersistData d = match d with | :? MyData as typedD -> Some typedD.ActualData | _ -> None } 




Some updates and clarifications

  • I do not need to get an education in functional programming "in general" (or at least what I like to think :-). I really understand how interfaces relate to functions, I know what functions of a higher order are, and how function composition works. I even understand that monads are warm fluffy things (as well as some other mumbo-jumbo from category theory).
  • I understand that I do not need use interfaces in F # because functions are generally better. But both interfaces in my example are actually justified: IPlugin serves to combine UniqueID and CreateData ; if not an interface, I would use a similar form entry. And IDataFragment serves to limit the types of data fragments, otherwise I would have to use obj for them, which would give me even less type safety. (and I can’t even imagine how I will do this in Haskell without using Dynamic)
+6
source share
2 answers

You do not need to define interfaces to make the architecture pluggable in F #. Functions are already compiled.

You can write your Outside-In system, starting with the desired, general behavior of your system. For example, here a function that I recently wrote translates a “Consumer Survey” from a state in which a message was not received to a new state:

 let idle shouldSleep sleep (nm : NoMessageData) : PollingConsumer = if shouldSleep nm then sleep () |> Untimed.withResult nm.Result |> ReadyState else StoppedState () 

This is a higher order function . While I was writing this, I found that it depends on the helper functions shouldSleep and sleep , so I added them to the argument list. Then the compiler automatically passes this, for example. shouldSleep must be of type NoMessageData -> bool . This function is dependent. The same goes for the sleep function.

As a second step, it turns out that a reasonable implementation of the shouldSleep function is as follows:

 let shouldSleep idleTime stopBefore (nm : NoMessageData) = nm.Stopped + idleTime < stopBefore 

Do not pay attention if you do not know what all this does. This is the composition of the functions that matter here. In this case, we learned that this special shouldSleep function is of type TimeSpan -> DateTimeOffset -> NoMessageData -> bool , which is not quite the same as NoMessageData -> bool .

This is pretty close, and you can use the partial function app to go the rest of the distance:

 let now' = DateTimeOffset.Now let stopBefore' = now' + TimeSpan.FromSeconds 20. let idleTime' = TimeSpan.FromSeconds 5. let shouldSleep' = shouldSleep idleTime' stopBefore' 

The shouldSleep' function is a partial application of the shouldSleep function and has the desired type NoMessageData -> bool . You can compose this function in the idle function along with the implementation of its sleep dependencies.

Since the lower order function is of the correct type (the correct signature of the function), it simply clicks into place ; no casting is required to achieve this.

The idle , shouldSleep and shouldSleep' can be defined in different modules in different libraries, and you can assemble them all together using a process similar to Pure DI .

If you want to see a more complete example of compiling an entire application from separate functions, I will give an example in my functional architecture with F # The course of pluralism.

+4
source

I can only sympathize with your statements. While functional programming in a small language has been discussed with death, there are very few tips on how to perform functional programming in general. I think that for F # in particular, most solutions will tend toward a more object-oriented (or at least interface-oriented) style as your system grows. I don’t think this is necessarily bad, but if there is a convincing FP solution, I would also like to see it.

One pattern that I saw in a similar scenario was to have a pair of interfaces, typed and atypical, and a reflection-based mechanism should go between them. So in your scenario you will have something like this:

 type IPlugin = abstract member UniqueID: PluginID abstract member DataType: System.Type abstract member CreateData: unit -> IDataFragment type IPlugin<'data> = inherit IPlugin abstract member CreateData: unit -> 'data abstract member PersistData: 'data -> PersistedData option abstract member LoadData: PersistedData -> 'data 

and the implementation will look like this:

 let create() = let createData () = MyData( "whatever" ) { new IPlugin with member x.UniqueID = "one plugin" member x.DataType = typeof<MyData> member x.CreateData() = upcast createData() interface IPlugin<MyData> with member x.LoadData d = MyData( d ) member x.PersistData (d:MyData) = Some d.ActualData member x.CreateData() = createData() } 

Note that CreateData is part of both interfaces - it is just there to illustrate that there is a balance between the amount of duplication between the typed and untyped interfaces and how often you need to jump over hoops to convert between them. Ideally, CreateData should not be in IPlugin , but if it saves you time, I would not look back twice.

To go from IPlugin to IPlugin<'a> you will need a reflection-based helper function, but at least you clearly know the type argument, since it is part of the IPlugin interface. And although this is not very good, at least the type conversion code is contained in one part of the code, and not sprinkled with all the plugins.

+4
source

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


All Articles