Adventures in Elm: Services, Ports, and Filthy Side Effects
I wrote a little demo in order to learn how to actually wire things together and execute Tasks, as well as how I might create a Service that could be used through out an app. I learned a few things during this process.
Although Native implementations are attractive from a re-use stand point, they're really fragile since they depend on compiler details.
It would be better to simply have a JS portion of the app that subscribes to a port and reacts to it.
A second exploration, forthcoming, will delve into how to use this, how it differs from a more Task oriented set up, and integration thereinto.
Thereinto is an officially listed word, albeit archaic.
How to actually wire user events through Tasks, into the evaluation pipeline, and finally take the results back into updates to the model.
The first item isn't really all that important, more just something to consider when creating services your own app will make use of. Elm-http makes use of a Native module to take care of actually performing the side-effect of trying to make an HTTP connection.
The second wasn't originally in this, just something which surprised me.
The third, however, will take up the bulk of this.
Digging into actual examples
Since I'd wanted to have some sort of Service which reacts to User Input and couldn't make heads nor tails of how to connect a signal of user Actions to services and back into updates, I decided to really dig into the examples of such interaction that were provided and rewrite them until I could understand them.
One of the examples provided is a simple ZIP code searcher which takes a valid ZIP code the user enters and sends a search request to api.zippopotam.us. Looking just at the imports didn't show anything particularly interesting, not that I'd expected anything there, but this was a close-grain search so I had to check anyway. The rest of the app was split into View and Wiring.
The view itself didn't show much aside from using the more generic on rather than on{Event} thing, perhaps due to listening for input DOM events. Upon the event firing, a message would be sent to query.address, so looking at query (which presumably is a mailbox) would be a natural place to start in the following section, the Wiring.
The first part of Wiring is simply the main function, which of course has the usual type of Signal Html and although the use of Signal.map2 is odd, it's not too far out. The view, it should be noted, takes as arguments a String named value and a Result String (List String) named result, rather than the address and model seen in fuller App examples.
The second part is where we will start in, since that's actually the thing we were looking for: the query mailbox. We can see it's a mailbox of strings, which makes sense if it's receiving the values of the text input. We also see just below it the results mailbox, but we'll mostly ignore at the moment, merely noting it's there. The thing after that is the interesting part, since it's a port.
The port here is annotated port requests : Signal (Task x ()) and given the definition of port requests = Signal.map lookupZipCode query.signal |> Signal.map (\task -> Task.toResult task `andThen` Signal.send results.address)... Oy vey. Even rewriting the |> out of there doesn't clear anything up. What the hell are any of the types?
One thing that will save you some confusion is to note that Task.toResult has the type Task error value-> Task x (Result error value) and it's not some magical Task to Result converter, just a re-wrapper, thus allowing the use of andThen as-is rather than with any additional functions around its use.
At any rate, I'd tried dissecting that port line for awhile and couldn't make any headway, so decided to just start rewriting parts to make it clearer using what I knew to transform its current form into something more verbose, but more understandable, mostly by breaking the parts out into temporary values in a let in statement, resulting in this fatter port definition. This has the same end result, all of the same processing, but lacks the confusing all-in-one-ness of the original.
Now it's easier, not necessarily obvious but still easier, to see that we're starting out with the query.signal from earlier and mapping a function lookupZipCode across it, which thus becoming a Signal of Task String (List String) (or whatever lookupZipCode returns, it doesn't really matter to us here.) Then, the lookup zip code Tasks are turned into Results and sent off the results mailbox. While this is eventually apparent when looking back at the original, it's not initially clear to a newbie how it works, leading to this sort of rewriting becoming necessary.
And while we're on the topic of rewriting, let's rewrite the above description to be more "functional" in nature, because functional programming deals with "things" not "commands", or "nouns" and not "verbs".
We start with query.signal, a Signal of query Strings, then get a Signal of Tasks by getting a Task representing the zip-code lookup request for each query String, from there get a Result of that lookup Task, and finally get a Task representing the process of sending that result to the results mailbox.
And that's that. The exact definitions of lookupZipCode and its sub function places aren't that important, though the former does demonstrate the use of "immediate" tasks through the use of succeed and fail, allowing flow control within a series of Tasks, somewhat reminiscent of Promise chains in JS.