PurelyFunctional.tv Newsletter 372: Model change over time with state machines
Issue 372 - April 06, 2020 · Archives · Subscribe
Clojure Tip 💡
Model change over time with state machines
We write sequential programs. That means the "current" piece of code that is executing often holds implicit information. It's often beneficial to make the implicit info explicit. Here's an example from a GUI in ClojureScript:
(defn on-click [event]
(set-loading true)
(ajax/get "https://server.com/request/path"
(fn [data]
(set-loading false) ;; at this point in the program, we know we are no
longer loading
(save-data data))))
If we trace through the execution, step-by-step, we can see how this works. When the user clicks a button, this function is called. It starts by setting the loading state to true, which will display a loading spinner. We make an ajax request. Once the ajax request responds, our callback is called. We set the loading state to false so that we don't show the spinner any more.
This looks straightforward and will work most of the time. It relies on the sequential nature of our code. But it will fail in many cases. It makes too many assumptions that just don't hold in an async environment. For instance, what if the user clicks the button twice quickly?
The problem, as I see it, is that we are relying on the implicit state
of the execution---where in the code each statement is. For example, we
know that the (set-loading true)
is at the beginning of this workflow,
and the (set-loading false)
is after the request succeeds. But we
can't really query that. We can't know, outside of this piece of code,
what that current execution state is.
The solution is to reify the implicit state to make it explicit. (Reify is a latinate term meaning "to make real". It means to give it a tangible, concrete form.) We can do that with a piece of data that represents the current state of the piece of data we need to fetch. I think the best way to know what we need to model is to draw it out. Here's a diagram of the simple case of a successful request/response.
These are the same states we are handling implicitly in the code above.
Now to model it explicitly. From the client's perspective (because that's what we're working on), we can identify three different time periods. These can correspond to three different states we could be in.
- Before the request
- During the request (after request, before response)
- After the response
We just need to figure out how to model these three states. I suggest we use a Variant Entity.
{:request-state :before}
{:request-state :during}
{:request-state :after
:data {...}}
Now, the state we had before (in this simple situation) is completely modeled.
These examples pieces of data are the states in a state machine.
We could model more, but this is already getting long. Here are some things you could model:
- Request times out.
- Request fails for server reasons.
- We want to request a value again (re-loading).
Each of these situations would be a new state in the data model of our state machine.
The other part of the state machine is the transitions. They are operations on the state that answer the question: How do we move from one state to another?
In our situation, we have simple transitions. We move from :before
to
:during
as we make the request. We move from :during
to :after
as
we get the response. These are obvious and were implicitly encoded in
the oringal code. The nice thing about state machines is they tell you
what to do when you are in an unexpected state.
For example, what if the user clicks twice quickly?
Before, we would have made the same ajax request twice. There could be weird things happening like the state getting set to loading after the first request came back. Asynchrony is weird like that.
But if we explicitly capture it in a state machine, we can avoid that problem. We just do nothing if we're already loading. We can encode that like this:
clojure (defn fetch-data [current-state url] (if (= :loading (:request-state current-state)) current-state ;; state does not change (do (ajax/get url ...) (assoc current-state :request-state :after))))
If you're squeamish about calling the ajax request from inside the transition function (you want to make this a calculation), you can always return the action as a thunk, like so:
clojure (defn fetch-data [current-state url] (if (= :loading (:request-state current-state)) [current-state (fn [])] ;; state does not change [(assoc current-state :request-state :after) (fn [] (ajax/get url ...))]))
The click handler will know what to do with the state and the thunk.
Well, that's enough for this time. There are lots more states and transitions to model, so feel free to do that. Next time, I think I'll focus on more practical aspects of this, like how to store the state in an atom.
Clojure Newsletters 📬
There is some great activity in the Clojure Newsletter space!
The Repl by Daniel Compton is back after 7 months of hiatus.
Clojure Weekly is a new newsletter from @dotemacs
Upcoming presentation 📢
This Monday I'm going to be presenting the main ideas from my upcoming book on functional programming, Grokking Simplicity, online at the Clojure Mid-Cities meetup.
Quarantine update 😷
I know a lot of people are going through tougher times than I am. If you, for any reason, can't afford my courses, and you think the courses will help you, please hit reply and I will set you up. It's a small gesture I can make, but it might help.
I don't want to shame you or anybody that we should be using this time to work on our skills. The number one priority is your health and safety. I know I haven't been able to work very much, let al one learn some new skill. But if learning Clojure is important to you, and you can't afford it, just hit reply and I'll set you up. Keeping busy can keep us sane.
Stay healthy. Wash your hands. Stay at home. Wear a mask. Take care of loved ones.
Clojure Challenge 🤔
Last week's challenge
The challenge in Issue 371 was to invert Yoda grammar. You can see them here.
You can leave comments on these submissions in the gist itself. Please leave comments! You can also hit the Subscribe button to keep abreast of the comments. We're all here to learn.
This week's challenge
Subset sums
Write a function that takes a set of numbers and returns all subsets that sum to a given number. That's a mouthful. Here's an example.
;; subsets that sum to 6
(subset-sums #{1 2 3 4 5} 6) ;=> #{ #{1 5} #{2 4} }
;; subsets that sum to 7
(subset-sums #{1 2 3 5 6 7} 7) ;=> #{ #{1 6} #{2 5} #{7} }
;; subsets that sum to 0
(subset-sums #{0 1 -1} 0) ;=> #{ #{} #{0} #{1 -1} }
Thanks to this site for the challenge idea.
As usual, please reply to this email and let me know what you tried. I'll collect them up and share them in the next issue. If you don't want me to share your submission, let me know.
Rock on!
Eric Normand