A service pattern

Ever want to have a top-level component in your app that provides services to multiple other sub-components in the app? Like a help service, or a MIDI messaging service. These are cases where there needs to be one service that has control over the domain… but you want multiple places in the app to use it.

What if you have several such services… and many nested components?

I finally worked out and wrote up a design pattern for doing this easily, and with linear code for each new service.

The docs are intended as a narrative for the techniques.

Interested to know what you think, and if you find it useful.

7 Likes

The one time I really wanted this was in my authentication module. I wanted any component in an application to be able to change the authentication state to Unauthed in the event of getting a 403 or 401 from a web service, and I also wanted components to be aware of changes to the current authentication state - say when a user logs in using the log-in widget. I also wanted an authentication module that could be published as a package and re-usable across many applications, rather than just being some boiler plate code I cut and pasted into each ones Main module.

I ended up implementing this using out messages, and defining a custom type describing the authentication states which is easy to extract from the overall authentication module state and pass into the update functions wherever it was needed.

Overall I felt this was better than trying to create some more general 2-way messaging construct - it was much simpler and more explicit in how things are routed together. But that was just for 1 such service and you are generalising to n services.

You can find the code for this authentication package here: the-sett/auth-elm

==

In Elm 0.18 I stole some kernel code from the original (and unrelated) elm-ui, gdotdesign/elm-ui. You can find the code I borrowed from that here: rupertlssmith/elmq.

This essentially does the same thing that your pattern does - it allows async messaging between child components. As it is kernel code, the runtime event queue is used instead of having to create explicit queues, as your code does.

A disadvantage of this elmq was that the messages had to be serializable, so either simple types or you have to write codecs for them to pass Values. This could be inconvenient, and also weakened the typing compared with how you are doing it.

==

I wonder - if message passing between components is a desirable pattern to build Elm applications on, then would it make sense for TEA (v2.0) to provide it as a built-in? Preferably with stronger typing on the messages than I was able to do with elmq.

It does feel like we are reaching for message passing between stateful components, that is, OO programming with this. Grokking that was partly why I abandoned it, although I must admit that kernel code restrictions in 0.19 were the real impetus for re-writing my authentication module - though I do feel it was greatly improved as a result.

Exactly: My pattern here is just a technique for doing what “out messages” do in a disciplined way so that you can have more than one or two such services.

That the two services shown here act as queues is not essential. Generalized message passing (ala actors) is not my aim. Indeed, I wanted to be able to do this in a functional and type safe way… and one that integrated with the existing Msg/update style.

The origin of this is that I built some code and port functions to handle WebMIDI. But this was a low level service. Then I had three different protocols over MIDI with a particular synthesizer. In turn, there were different parts of the main program that dealt with these different protocols. These all run at the same time, and need to have state at both the lowest (MIDI connection state) and mid layers (protocol state, and modeling of the synthesizer state.)

In OO systems, you’d have the components call the mid-tier protocol objects, which would in turn all talk with a low level MIDI object managing the connection, and that would talk to the OS services.

But in Elm, the threading with things that have state and make port calls makes this hard. If two different UI components needed to do things that eventually called WebMIDI - then all that low-level state (the WebMIDI Model) has to be passed all over the place all the time so it can be updated.

Also, the way a component would talk with the mid-tier looks very different than the way it talks with either port calls, other Cmd based services, or Html event handlers. What I wanted was a way to open up the common Elm pattern: return a thing with an embedded message function, see that message in update in the future (zero, one, or many times!).

I usually add module-boundaries for separation of concerns. I would argue that the complexity of communicating over these boundaries is analogous to getters/setters in OOP, where classes are used for boundaries.

If two or more modules need to communicate, I would consider:

  1. Should I extract some of their internal structure up, closer to the main module, to share it between submodules. I do this for when I need that logic between multiple submodules.
  2. Perhaps it makes sense to merge the modules if there’s only coupling between the two and the logic seems to cohesively fit together.

I think this approach results in nicely structured code with less accidental complexity compared to communicating across module boundaries. This is obviously just my perspective and experience, but I feel compelled to give this feedback on “component-like” approaches, as it isn’t AFAIK idiomatic to Elm.

Please correct me if I’m wrong :sunny:

You’re not wrong… In fact… your suggesting what this service pattern does:

Consider a normal Elm style system where a top component has two independent sub-components:

     top
   /     \
  a       b

Top holds the state of the sub-components, passing it to them in update, and then taking it back, along with low level operations (Cmd) to be performed.

Now, consider if both sub-components need to access a shared resource - say a MIDI connection, or perhaps an session with a back end:

     top
   /     \
  a       b
   \     /
   service

We could pass the service’s state to a and b each time they are invoked, and have them pass the service’s state to the service, and then pass the updated service state back to top, along with their own… but you can see how this gets messy quickly, especially if there are multiple services and multiple layers.

To make matters worse, if the service needs access to ports, this must be plumbed all the way to the top, and responses back down. Again doable, but the type signatures for update in a and b are starting to get quite unwieldy.

But the final stumbling block is that the service in that position can’t do things that a pair of in/out ports can: It can’t offer a module like a the ability to request something, passing a message function so that a msg will be sent back to a’s update function when done.


Now consider your first point: You’d look to moving the internal structure (service) upward into top. This is fine and solves some of the problems… but not when there are half a dozen services or several layerings… they’d all end up in top, which is unworkable.

… But your intuition is right: service shouldn’t be “under” the other modules, because in a functional pure system, it can’t easily be there due to the way state is handled.

Which is where the service pattern comes in: The service (or services) can be their own things without cluttering up top, but still offer services to sub components a and b. What’s more, the interaction follows “the Elm way”: a requests service by returning a Cmd like object from update… and gets a response later via a Msg.

We end up with:

     top
   /  |  \
  a   |   b
      |   
   service

Where communication is mediated by top, in a completely type safe way, and without top having to add code for every function in the service.

2 Likes

I came to the same conclusion, albeit for a simpler use-case.

What you have done is quite complicated but I don’t have a better suggestion.

===

If Elm had some kind of communicating Channel type built in, would that have been of use to you? Here is a little outline of what this might look like:

-- Async channel for passing `a`s.
type Channel a

-- Create a channel.
channel : () -> Channel a

-- Send an `a` over the channel.
send : a -> Channel a -> Cmd msg

-- Receive `a`s over the channel.
receive : (a -> msg) -> Channel a -> Sub msg

Then to use it:

type Midi = ...

midiChannel : Channel Midi
midiChannel = channel ()

Then give the midiChannel to the modules sending and receiving MIDI events.

This isn’t quite right because I had to declare that the midiChannel was of type Channel Midi, but I could have just let it be a Channel a and then passed stuff that isn’t Midi over it, giving the receiving end a nasty surprise. I can’t really see a simple way of fixing that, since I cannot pass a type as an argument to the channel constructor.

I less than perfect way of doing it might be to require an example message to be passed to the constructor in order to bind the type argument:

-- Create a channel.
channel : a -> Channel a

midiChannel : Channel Midi
midiChannel = channel MidiReset -- or whatever Midi type instance makes sense.

Variations:

  • publish/subscribe behaviour versus each message only being received by one susbcriber.
  • maybe want to distinguish between the sending and receiving ends?

Elm doesn’t have objects/components, but I think I know what you’re talking about. I don’t agree with your conclusions, but it was nice to get your perspective :sunny: Thanks!

I think I can see a way to implement Channel on top of Task.peform. Might give it a go and see how it turns out. Gotta complete my current rabbit-hole first though… :thinking:

I think channels (and that actor style of communication) is orthogonal to the structure problem I’m tackling. In particular, I would prefer not to conflate the semantics of queues and pub/sub with a more simple shared resource structure.

With respect to your channels, if one needed queued messages… The types you have can’t support the style of API Elm uses.

For example, I’m looking to implement this sort of API:

requestDeviceStatus : String -> (DeviceInfo -> msg) -> ???
requestDeviceStatus deviceId msgFn ...

The client wants to cause the request to happen, and later get one it’s own msg values with the DeviceInfo back. The client needs this to be a msg, not just a function, because in response it is going to want to change it’s own Model - and the only way it can do that is while responding in update. This is very similar to Html.Events and Task.

If we try to implement the service as a Channel, you get:

requestDeviceStatus : String -> (DeviceInfo -> msg) -> Channel MIDIServiceRequests -> Cmd msg
requestDeviceStatus deviceId msgFn chan = ...

But we immediately run into type issues:

First, the type MIDIServiceRequests is going to have to be parameterized on msg:

type MIDIServiceRequests msg
  = ProbeDevices (List String -> msg)
  | GetDeviceInfo String (DeviceInfo -> msg)
  | ...

Which means, in turn, that the channel would have to be parameterized:

requestDeviceStatus : String -> (DeviceInfo -> msg) -> Channel (MIDIServiceRequests msg) -> Cmd msg

We’d expect the channel to be created in the top and passed to this code. But unless the top of your code shares the same Msg type with all other modules, this can’t work.

I clearly haven’t stated the motivation for all this clearly - and I don’t think I’m aiming at something all that general, or all that different than Elm’s conventional ways of use (Msg, update, Model, and Cmd style…).

So, let’s review the problem:

  1. module A in the code wants to call some other module B, and later, when B finally does it’s work (async to the request), A wants to modify its state in response.

  2. This means, the response from B must come back to A wrapped in one of A’s Msg values, and be delivered via A’s update.

  3. B needs to keep state (remember, things are async, so state here can’t be avoided) - so B must receive the request via update or a similar function called by the holder of B’s Model. If B uses port functions or other Cmd and Sub functions, then it will need to interact with those via update and its own Msg type.

  4. If A holds B’s model, the types all work… but no other module can use B. If B is pure, then perhaps we don’t care if every module has its own B. But if B is managing a shared resource (internal or external to the application), then the common holder of B’s Model (top?) will need to pass it to, and get it back, from every module that uses B. This is what we do when we write ad hoc out messages. It gets unworkable once there are more than a few services.

  5. Since the common holder of B’s Model must be at the top (or at least on top of all things that use B), then this top component must be responsible for shuffling around the requests and responses.

This is what the pattern I posted is:

  • a type safe way to ensure that requests and messages are delivered between A and B
  • using each module’s own update and Msg types
  • and ensuring the type safety of the interface B chooses (A only imports B’s interface, not it’s Msg and Model types)
  • enabling B to use Cmd, Sub, and port in the standard way
  • with Elm’s standard callback style using message functions (passing ResultType -> Msg functions in requests)
2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.