oHm Om with Haskell in the middle
Om is awesome. oHm is a hommage to Om in GHCJS using Haskell's pipes, mvc and pipes-concurrent libraries.
Introduction
Ohm at its core is the idea of building an application as a pure left fold over a stream of events. At a previous position we built a UI that captured this model in clojurescript and Om, this is a port of that architectural idea to Haskell.
Set up
Concepts
-
Models
Models are the state of your application. Here's the Model from the todo mvc example mentioned later:
-
State
data ToDo = ToDo { _items :: [Item] , _editText :: String , _filter :: Filter } deriving Show
In addition to the model you also need a updating function of type
mdlEvent -> model -> model
which is a left fold function that applies a to the Model resulting in the new Model. This function is one of the things you need to construct a . -
Here's the model function from our todo mvc example:
process :: Action -> ToDo -> ToDo process (NewItem str) todo = todo &~ do items %= (Item str False:) editText .= "" process (RemoveItem idx) todo = todo & items %~ deleteAt idx process (SetEditText str) todo = todo & editText .~ str process (SetCompleted idx c) todo = todo & items.element idx.completed .~ c process (SetFilter f) todo = todo & filter .~ f
Note that MVC, one of the libraries that oHm is built on has a concept of a model too. In MVC Model refers to the pure transformation that happens within a Pipe and applies an event to the state to produce a new state. In oHm construction of an MVC Model happens with the
appModel
function that therunComponent
function applies for you.
-
-
Model Events
Model Events represent events that happen in your domain to effect change to the state of the world. This is the
Action
type mentioned in the event -> model -> model function earlier:data Action = NewItem String | RemoveItem Index | SetEditText String | SetCompleted Index Bool | SetFilter Filter
-
UI Events
UI Events occur at the points of interaction between user and your app. These are the sorts of things that you'd attach callbacks to: changes, clicks, mouse moves etc. A
DOMEvent
type is provided to 1.1.2.5 for these events.For simpler apps, like our todo mvc example, the UI could emit events which are passed straight through to the model.
-
Processors
Processors consume events of one type, say UI Events and produce Events of another type, with the ability to perform actions in some Monad. These are used to process the UI Events that a Component emits into a form that that component's model can use to update its state.
In our simple todo mvc example, as we're using the same type for UI Events and Model Event, there's no processing and events are just passed straight through using the idProcessor.
-
Renderers
A Renderer is a function of type
DOMEvent a -> model -> HTML
where HTML is a virtual-dom representation of the UI.This is the top level
Renderer
from todo mvc:todoView :: DOMEvent Action -> ToDo -> HTML todoView chan todo@(ToDo itemList _txtEntry currentFilter) = with div (classes .= ["body"]) [ titleRender, itemsRender, renderFilters chan todo] where titleRender = with h1 (classes .= ["title"]) ["todos"] itemsRender = with ul (classes .= ["items"]) (newItem chan todo : (P.map (renderItem chan) $ zip [0..] filteredItems)) filteredItems = filterItems currentFilter itemList
In this example
renderFilters
,newItem
, andrenderItem
are allRenderers
that each render a sub part of the UIOne other point of interest is how
DOMEvents
work if we take the example of onInput:onInput :: MonadState HTML m => DOMEvent String -> m ()
In our
newItem
RenderernewItem :: DOMEvent Action -> ToDo -> HTML newItem chan todo = with li (classes .= ["newItem"]) [ into form [ with input (do attrs . at "placeholder" ?= "Create a new task" props . at "value" ?= value onInput $ contramap SetEditText chan) [] , with (btn click "Create") (attrs . at "hidden" ?= "true") ["Create"] ] ] where value = (todo ^. editText.to toJSString) click = (const $ (channel chan) $ NewItem (todo ^. editText))
we only have a
DOMEvent Action
available to accept UI Events, whereas onInput takes aDOMEvent String
so we need to adapt theDOMEvent
passed tonewItem
to be one that takes aString
for passing toonInput
.DOMEvent
happens to be an instance of theContravariant
class. You can thing of thecontramap
function being like anfmap
, but applying its function to the input of something rather than the content.f :: String -> Action f = SetEditText -- We have a DOMEvent Action -- We want a DOMEvent String -- fmap :: (a -> b) -> f a -> f b contramap :: (a -> b) -> f b -> f a contramap :: (String -> Action) -> DOMEvent Action -> DOMEvent String
-
Components
A component packages up the three things that you provide into something that the framework can run, with an extra environment in
ReaderT
that the processor can use within its actions to route events that have an external effect (for example REST requests or socket.io calls)modelComp :: Component () Action ToDo Action modelComp = Component process todoView idProcessor main :: IO () main = void $ runComponent initialToDo () modelComp
Examples
Todo MVC
http://todomvc.com/ The canonnical TODO MVC example demonstrates the basic moving parts of oHm
Socket.IO Chat
The socket.io example is a bit more involved and adds some new concepts illustrating nesting components by adapting the types of processors