Auto: A Todo GUI application with Auto (on GHCJS, etc.)
by Justin Le ♦
Source ♦ Markdown ♦ LaTeX ♦ Posted in Auto, Haskell, Tutorials ♦ Comments
Continuing along with All About Auto, let’s look at another exciting and useful application of the auto library: GUI’s. We’re going to look at the canonical “hello world” of GUI apps these days — the todo app. We’re going to be using the specs of todoMVC to build a todoMVC “candidate” that follows the specs…and along the way see what auto offers in its tools of managing isolated state components and modeling GUI logic. We’re really going to be focusing on application logic — “control” and “model” — and not looking too close on “views”, which auto doesn’t quite try to offer and where you can really pick your own view rendering system, making this adaptable to really any platform — javascript/web, desktop, command line, etc.
A live version of our end-product is hosted and online.
This post does assume some concepts from the tutorial…if not all, then at least those in the introductory post or the README. If you ever find yourself thinking that these concepts are completely new and crazy, you might want to try looking through the tutorial or docs to refresh your mind. As always, comments are welcome, and I’m also usually on #haskell-auto as jle`, and also on twitter
(Fair warning…this is not quite a “ghcjs tutorial”, if that’s what you’re looking for; it’s an auto tutorial that uses some rudimentary ghcjs. Hopefully you can learn from that too!)
Overall Layout
At the highest level, auto is a library that provides us tools to build and work with stream transformers on streams of values. Transform a stream of input values to a stream of output values. So, let’s try to phrase our Todo app problem in that perspective. What are our inputs, and what are our outputs?
For a Todo app, the outputs are probably going to be a todo list itself. If we’re building a GUI, then having the todo list itself is going to be enough to build our front-end display. The stream of inputs is a little less obvious, but, well, what does an app really take as inputs? Commands! Our stream of inputs will be commands sent by a GUI or by whatever front-end we choose. Our todo app then is a transformer of a stream of commands to a stream of todo lists…where the todo list we get changes as we process more commands.
So the “overall loop” will be:
- A front-end rendered by ghcjs-dom (or whatever) with event handlers that drop commands into a concurrent
Chan
queue. This just handles rendering. - Our
Auto
launched withrunOnChan
, which waits on theChan
queue, runs the inputs through theAuto
, and renders the result. This handles all of the logic.
We like types in Haskell, so let’s begin by laying out our types!
-- source: https://github.com/mstksg/inCode/tree/master/code-samples/auto/Todo.hs#L19-L46
import Control.Auto
import Control.Auto.Collection
import Control.Monad.Fix
import Data.IntMap (IntMap)
import Data.Serialize
import GHC.Generics
import Prelude hiding ((.), id)
import qualified Data.IntMap as IM
data TodoInp = IAdd String -- new task with description
| ITask TaskID TaskCmd -- send command to task by ID
| IAll TaskCmd -- send command to all tasks
deriving Show
data TaskCmd = CDelete -- delete
| CPrune -- delete if completed
| CComplete Bool -- set completed status
| CModify String -- modify description
| CNop -- do nothing
deriving Show
data Task = Task { taskDescr :: String
taskCompleted :: Bool
,deriving (Show, Generic)
}
instance Serialize Task -- from Data.Serialize, from the cereal library
We have a type to represent our inputs, TodoInp
, which can be an “add” command with a String
, a “task” command with a TaskId
(Int
) and a TaskCmd
, and an “all” command with a TaskCmd
that is supposed to represent sending that command to all tasks.
Our TaskCmd
represents commands we can send to individual tasks – we can delete, prune (delete if completed), set the “completed” flag, or modify the description.
We’re going to represent our task list, TaskMap
, as not a []
list, but as an IntMap
from containers, which associates an Int
to a Task
that we can look up with the IntMap
API. What would a TaskMap
store other than a bunch of Task
s, which we are defining as jus a tupling of a String
description and a Bool
completed/uncompleted status.
The Todo Auto
Time to go over the logic portion! The part that auto is meant for! We’re going to structure the logic of our app (also known as the “model”) by using principles of local statefulness to avoid ever working with a “global state”, and working in a declarative, high-level manner.
Tasks
It’s clear that the core of our entire thing is going to be the “task list” construct itself…something that can dynamically add or remove tasks.
In auto, there is a construct created just for this kind of situation: dynamic collections indexed by a key (a “task id”), where you can add or subtract Auto
s from dynamically — they are dynMap
and dynMapF
from Control.Auto.Collection. We’ll be using dynMapF
because it’s serializable, and we don’t need the extra power that dynMap
offers.
dynMapF :: (k -> Interval m a b) -- ^ function to initialize new `Auto`s
-> a -- ^ default inputs
-> Auto m ( IntMap a -- ^ input for each internal `Auto`, indexed by key
Blip [k] -- ^ blip stream to initialize new `Auto`s
,
)IntMap b) -- ^ `Auto` outputs, by key (
dynMapF
keeps a “dynamic collection” of Interval m a b
s, indexed by an Int
key, or an “ID”. It takes as input a stream of IntMap a
…basically a bunch of (Int, a)
pairs. dynMapF
routes each input to the Interval
at that ID/address (with a suitable “default” a
if none was sent in), and then outputs all of the results as an IntMap b
— a bunch of (Int, b)
pairs, each output with the address of the Auto
that made it.
For example, IM.singleton 5 True
would send True
to the Auto
stored at 5
. It’ll then output something that includes (5, "received True!")
— the output of the Auto
at slot 5.
Whenever an Interval
turns “off” (is Nothing
), it is removed from the collection. In this way we can have Auto
s “remove themselves”.
It also takes as input a blip stream of [k]
s. We use each emitted k
to “initialize a new Interval
” and throw it into the collection, creating a new unique key for it. Every time a new Auto
is initialized, dynMapF
creates a new key for it.
Read over the tutorial section on blip streams and Interval
s if you are still unfamiliar with them.
This pretty much fits exactly what we want for our task collection. If we imagined that we had our Task
as an Auto
:
initTask :: Monad m => String -> Interval m TaskCmd Task
initTask
takes a string (a starting description) and initializes an Interval
that takes in a stream of task commands, and has a stream of new, updated Task
s as its output stream. At every step, it processes the command and outputs the new appropriate Task
.
We can then use this as our “initializer” for dynMapF
…and now we have a dynamic collection of tasks!
-- source: https://github.com/mstksg/inCode/tree/master/code-samples/auto/Todo.hs#L48-L50
taskCollection :: Monad m
=> Auto m (IntMap TaskCmd, Blip [String]) (IntMap Task)
= dynMapF initTask CNop taskCollection
If we wanted to send in the command CModify "hey!"
to the task whose id/key/address is 12
, I’d feed in IM.singleton 12 (CModify "hey!")
. The output would then contain the output of feeding that CModify
to the Auto
at that slot 12, associated with slot 12 on the output IntMap
.
Writing initTask
and the task Auto
is straightforward with accum
, which is basically like foldl
on the inputs and a “current state”. (The current state is of course the Task
).
-- source: https://github.com/mstksg/inCode/tree/master/code-samples/auto/Todo.hs#L52-L62
initTask :: Monad m => String -> Interval m TaskCmd Task
= accum f (Just (Task descr False))
initTask descr where
Just t) tc = case tc of
f (CDelete -> Nothing
CPrune | taskCompleted t -> Nothing
| otherwise -> Just t
CComplete s -> Just t { taskCompleted = s }
CModify descr -> Just t { taskDescr = descr }
CNop -> Just t
Nothing _ = Nothing f
See that our Auto
“turns off” by outputting Nothing
. That’s interval semantics, and it’s what dynMapF
relies on for its internal Auto
s!
Routing the inputs
The only thing left, then, is just to route our input stream to send everything to the correct Auto
in taskCollection
.
Our input stream is going to be a stream of TodoInp
, which can be “add”, “send command to a single task”, or “send command to all tasks”. Really, though, you can think of it three separate streams all “jammed” into one stream.
This is a common pattern that we can use blip streams for. Instead of working with one big fatty stream, we can work with several blip streams that only emit when the input that we care about comes in.
Typically, we’d do this with emitJusts
:
emitJusts :: (a -> Maybe b) -> Auto m a (Blip b)
You can imagine emitJusts
is a “siphon” off of the input stream of a
s…and pulling out only the values that we care about, as a blip stream of b
’s.
We can build our “siphoners”:
-- source: https://github.com/mstksg/inCode/tree/master/code-samples/auto/Todo.hs#L95-L105
getAddEvts :: TodoInp -> Maybe [String]
IAdd descr) = Just [descr]
getAddEvts (= Nothing
getAddEvts _
getModEvts :: TodoInp -> Maybe (IntMap TaskCmd)
ITask n te) = Just $ IM.singleton n te
getModEvts (= Nothing
getModEvts _
getMassEvts :: ([TaskID], TodoInp) -> Maybe (IntMap TaskCmd)
IAll te) = Just $ IM.fromList (map (,te) allIds)
getMassEvts (allIds, = Nothing getMassEvts _
getAddEvts
, when used with emitJusts
, will siphon off all IAdd
commands as a blip stream of [String]
s, emitting descriptions of new tasks to add.
getModEvts
, when used with emitJusts
, will siphon off all ITask
commands as a blip stream of IntMap TaskCmd
, which will be fed into taskCollection
and dynMapF
.
getMassEvts
is pretty much the same thing…siphoning off all IAll
commands as a blip stream of IntMap TaskCmd
. It needs a list of all TaskID
s though, to do its job…because it needs to make an IntMap
targeting all of the current tasks.
Remember, we interace with tasks through an IntMap TaskCmd
…which is a map of task id-task command pairs. The TaskCmd
stored at key 1
will be the command we want to send to task id 1.
Let’s see it all work together!
-- source: https://github.com/mstksg/inCode/tree/master/code-samples/auto/Todo.hs#L64-L93
todoApp :: MonadFix m => Auto m TodoInp (IntMap Task)
= proc inpEvt -> do
todoApp
-- all id's currently alive
rec <- arrD IM.keys [] -< taskMap
allIds
-- "forking" `inpEvt` into three blip streams:
-- newTaskB :: Blip [String]
<- emitJusts getAddEvts -< inpEvt
newTaskB -- modTaskB :: Blip (IntMap TaskCmd)
<- emitJusts getModEvts -< inpEvt
modTaskB -- massTaskB :: Blip (IntMap TaskCmd)
<- emitJusts getMassEvts -< (allIds, inpEvt)
massTaskB
-- merge the two streams together to get "all" inputs, single and
-- mass.
let allInpB :: Blip (IntMap TaskCmd)
= modTaskB <> massTaskB
allInpB
-- from a blip stream to an `IntMap` stream that is empty when the
-- stream doesn't emit
-- taskCommands :: IntMap TaskCmd
<- fromBlips IM.empty -< allInpB
taskCommands
-- feed the commands and the new tasks to `taskMap`...the result is
-- the `IntMap` of tasks.
-- taskMap :: IntMap Task
<- taskCollection -< (taskCommands, newTaskB)
taskMap
id -< taskMap
To read the proc block, it does help to sort of see all of the lines as english statements of what things “are”.
allIds
is a list of keys (id’s) currently in the task maptaskMap
. All of the id’s of the tasks currently alive.Now, we fork into blip streams:
newTaskB
is a blip stream that emits with task descriptions wheneverinpEvt
calls for one.modTaskB
is a blip stream that emits with a command to a specific task wheneverinpEvt
calls for one.massTaskB
is a blip stream that emits commands to every single task inallIds
wheneverinpEvt
calls for it.allInpB
is a blip stream with addressed commands whenever eithermodTaskB
ormassTaskB
emits.
taskCommands
is a map of addressed commands for each task. It’s whateverallInpB
emits, when it does emit…or justIM.empty
(an empty map) when it doesn’t.taskMap
is the map of tasks that we get from ourtaskCollection
updater, which manages a collection of tasks.taskCollection
needs the commands for each task and the new tasks we want to do its job.
We state things as an interplay of streams. And in the end, the result is what we want — an indexed list of tasks.
Note that we needed the rec
block because we referred to taskMap
at the beginning (to get allIds
), but we don’t define taskMap
until the end.
Note that we use arrD
for allIds
. What we really “meant” was something like:
<- arr IM.keys -< taskMap allIds
But…this doesn’t really work out, because when the whole thing “starts”, we don’t know what taskMap
is. We need to know massTaskB
to know taskMap
, and we need to know allIds
to know massTaskB
, and…recursive dependency!
We can use arrD
to specify an “initial output” to “close the loop” (in technical terms). We want allIds
to initially be []
(we can assume we start with no task id’s), so instead of
<- arr IM.keys -< taskMap allIds
we have
<- arrD IM.keys [] -< taskMap allIds
Where []
is the “initial output”, so when we first try to do anything, we don’t need taskMap
— we just pop out []
!
This is just a small thing to worry about whenever you have recursive bindings. There is a small cognitive price to pay, but in return, you have something that really just looks like laying out relationships between different quantities :)
Interfacing with the world
Our application logic is done; let’s explore ways to interface with it!
Testing/command line
-- source: https://github.com/mstksg/inCode/tree/master/code-samples/auto/todo-cmd.hs#L25-L62
parseInp :: String -> Maybe TodoInp
= p . words
parseInp where
"A":xs) = Just (IAdd (unwords xs))
p ("D":n:_) = onId n CDelete
p ("C":n:_) = onId n (CComplete True)
p ("U":n:_) = onId n (CComplete False)
p ("P":n:_) = onId n CPrune
p ("M":n:xs) = onId n (CModify (unwords xs))
p (= Nothing
p _
onId :: String -> TaskCmd -> Maybe TodoInp
"*" te = Just (IAll te)
onId = (`ITask` te) <$> readMaybe n
onId n te
formatTodo :: IntMap Task -> String
= unlines . map format . IM.toList
formatTodo where
Task desc compl) = concat [ show n
format (n, ". ["
, if compl then "X" else " "
, "] "
,
, desc
]
main :: IO ()
= do
main putStrLn "Enter command! 'A descr' or '[D/C/U/P/M] [id/*]'"
. interactAuto $ -- interactAuto takes an Interval; `toOn` gives
void -- one that runs forever
toOn-- default output value on bad command
. fromBlips "Bad command!"
-- run `formatTodo <$> todoApp` on emitted commands
. perBlip (formatTodo <$> todoApp)
-- emit when input is parseable
. emitJusts parseInp
interactAuto
runs an Interval
by feeding it in strings from stdin printing the output to stdout, until the output is “off”/Nothing
— then stops. Here we use parseInp
to emit input events whenever there is a parse, run todoApp
(formatted) on the emitted events, and then condense it all with fromBlips
and wrap it in an “always on” toOn
.
$ cabal sandbox init
$ cabal install auto
$ cabal exec runghc todo-cmd.hs
Enter command! 'A descr' or '[D/C/U/P/M] [id/*]'
> A take out the trash
0. [ ] take out the trash
> A do the dishes
0. [ ] take out the trash
1. [ ] do the dishes
> C 1
0. [ ] take out the trash
1. [X] do the dishes
> U 1
0. [ ] take out the trash
1. [ ] do the dishes
> C 0
0. [X] take out the trash
1. [ ] do the dishes
> P *
1. [ ] do the dishes
You can download and run this yourself!
Looks like the logic works! Time to take it to GUI!
As a GUI
To build a GUI, we must build an Auto
that takes in inputs from events and output everything the front-end renderer needs to render the interface.
For a typical todomvc gui, we need to be able to filter and select things. So that means we need to be extend our input type with filtering and selecting events. And our output has to also indicate the current filter selected, and the current task selected, as well.
data GUIOpts = GUI { _currFilter :: Filter -- currently applied filter
_currSelected :: Maybe TaskID -- currently selected task
,
}
data GUIInp = GIFilter Filter
| GISelect (Maybe TaskID)
data Filter = All | Active | Completed
deriving (Show, Generic, Enum, Eq)
instance Serialize Filter
Instead of defining a new input mega-type with all input events and the todo map with the options, we can use good ol’ fashioned Either
and (,)
. So now, instead of:
todoApp :: Auto m TodoInp (IntMap Task)
We have:
todoAppGUI :: Auto m (Either TodoInp GUIInp) (IntMap Task, GUIOpts)
Now we take either TodoInp
or GUIInp
and then return both IntMap Task
and GUIOpts
.
todoAppGUI :: Auto' (Either TodoInp GUIInp) (IntMap Task, GUIOpts)
= proc inp -> do
todoAppGUI <- holdWith All . emitJusts filtInps -< inp
filt <- holdWith Nothing . emitJusts selcInps -< inp
selc <- holdWith mempty . perBlip todoApp . emitJusts todoInps -< inp
tasks
id -< (tasks, GUI filt selc)
where
todoInps :: Either TodoInp GUIInp -> Maybe TodoInp
Left ti) = Just ti
todoInps (= Nothing
todoInps _ filtInps :: Either TodoInp GUIInp -> Maybe Filter
Right (GIFilter filt)) = Just filt
filtInps (= Nothing
filtInps _ selcInps :: Either TodoInp GUIInp -> Maybe (Maybe TaskID)
Right (GISelect sec)) = Just selc
selcInps (= Nothing selcInps _
Here we have the same idea as before. One input stream of Either TodoInp GUIInp
comes through, and we fork it into three blip streams that each do what we want. holdWith x :: Auto m (Blip b) b
is always the value of the last emitted item…but starts off as x
first.
By the way, the above code is much more succinct if you are willing to use lens…
todoAppGUI :: Auto' (Either TodoInp GUIInp) (IntMap Task, GUIOpts)
= proc inp -> do
todoAppGUI <- holdWith All
filt . emitJusts (preview (_Right . _GIFilter)) -< inp
<- holdWith Nothing
selc . emitJusts (preview (_Right . _GISelect)) -< inp
<- holdWith mempty . perBlip todoApp
tasks . emitJusts (preview _Left) -< inp
id -< (tasks, GUI filt selc)
(assuming we defined the prisms for GUIInp
or used ''mkPrisms
)
Neat, right? In a way, you can say that emitJusts
and Prisms
/lens was a match made in heaven :)
Giving it life
The last step is to hook everything up together —
- Setting up events in our GUI to feed inputs to a queue
- Setting up the queue to wait on inputs, and output the task map/gui status on every one using
todoAppGUI
- Rendering the output into the GUI framework of your choice
The second step in particular can be handled with good ol’ [runOnChan][]
:
runOnChan :: (b -> IO Bool) -> Chan a -> Auto' a b -> IO (Auto' a b)
We know and love runOnChan
from when we used it to make our chatbot. It runs an Auto' a b
“on a Chan
” (concurrent queue). The first argument is an “output hander” — it handles the b
s that the Auto'
pops out. It decides whether to stop the whole thing or keep on listening based on the Bool
result of the handler. The second argument is the Chan a
to listen for inputs on. Whenever something is dropped into that Chan
, it runs the Auto'
with the a
and processes the output b
with the handler.
Our final runner is then just:
runOnChan renderGUI inputChan todoAppGUI
where
renderGUI :: (IntMap Task, GUIOpts) -> IO Bool
inputChan :: Chan (Either TodoInp GUIInp)
The rendering is done with renderGUI
…and it really depends on your framework here. That’s #3 from the list above.
All you need after that is just to have your GUI hook up event handlers to drop the appropriate Either TodoInp GUIInp
into inputChan
…and you’re golden!
Seeing it in action
We’ve reached the end of our tutorial — the parts about auto
. It is my hope that whatever GUI front-end you want to work with, it’ll be simple enough to “plug in” our Auto
logic.
A live demo is online too; you can see the source of the front-end bindings
This is a bare-bons ghcjs implementation using ghcjs-dom, which uses direct dom manipulation.
User eryx67 has been kind enough to provide an implementation in ghcjs with the virtual-dom library (side-by-side comparison), so there is a slightly less uglier implementation with abstraction :)
As always, feel free to ask questions in the comments, hop over to #haskell-game or #haskell-auto on freenode, or send me a tweet! And look forward to more tutorials as the All About Auto series progresses!