9 September 2017
I was nearly finished building a relatively simple webapp to simulate stock trading, when a problem I had set aside from the beginning of the work came looming back: storing the data.
The app gives you some money to play with and allows you to buy and sell stocks, but the portfolio data was purely ephemeral, vanishing with a refreshed page. That doesn’t make much sense for a simulation that would most likely be used over a number of days.
But I had set the issue aside in favor of building out the actual features, knowing that eventually I would have to get my hands dirty in the world of Elm “ports”, a feature that allows non-functional JS code to interop with the clean and pure Elm code.
A little research turned up some useful information, including a lovely tutorial on building Elm apps that included detailed instructions on how to setup ports for localStorage.
Hurrah!
There wasn’t much to implement, and it mostly made sense.
port declarations in ElmModel -> ( Model, Cmd Msg ) that needed to be inserted
into the update function, capturing certain model updates
to create the side effect of the JS interopThe last one was the weirdest to wrap my head around, but the entire effort was promisingly easy.
As is typical of making changes to an Elm program, compiling resulted in a friendly error message.
Port `setStorage` is trying to communicate an unsupported type.
352| port setStorage : Model -> Cmd msg
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The specific unsupported type is:
Dict.Dict String { name : String, shares : Int, price : Float }
The types of values that can flow through in and out of Elm include:
Ints, Floats, Bools, Strings, Maybes, Lists, Arrays, Tuples, Json.Values,
and concrete records.
This was a problem. My data model did use a Dict to
store stock positions in a user’s portfolio, but apparently Elm wasn’t
having that.
I poked around to see if I could modify the code I’d only
half-understood to send/recieve something other than the whole model.
The good news: yes. The bad news: the data I actually cared about
storing was that Dict.
After a few half starts and many more friendly error messages, I
started to put together a solution. The Dict library
helpfully included the pair of functions toList and
fromList, which according to the compiler were
eligible for sending via ports.
On examination, the only data that needed to be kept was the cash balance and the list of positions, so the concrete data type that ended up being sent/recieved was:
(Float, List (String, Position))
Position is a concrete record type with a couple of pieces of relevant data. Probably there’s still some useful refactoring to be done here by aliasing the above tuple to a named type, since it shows up in several places.
Only a smattering more work remained after this insight. My
init function needed to rebuild the floating balance and
the dictionary, leaving it with this ugly type signature:
init : Maybe ( Float, List ( String, Position ) ) -> ( Model, Cmd Msg )
It’s all I can do to not go create a SavedPortfolio type alias right now.
I also updated the JS to provide a better key name to the saved data: “portfolio” rather than “model”. That led me on a crazy bughunt that only ended when I realized I had changed the key value only in the function calls to save and remove… but not to initialize the Elm app. Put that in the “literals should be defined as constants” file.
The actual code for stock trading is not very interesting, but delving into using localStorage and ports ended up forcing me to learn a bunch. I consider myself fortunate that my use case was just different enough from the tutorial code as to not work at all. If my model had been “sendable” in the first place, the code would have worked, I would have been satisfied, but I would have walked away without much actual knowledge of how it worked.
tl;dr: Failing is learning.