User Interfaces with Optics

Posted on August 23, 2015

User interfaces are tedious. They are so particularily tedious that dozens of frameworks have been developed to numb the pain mildy that arises from managing dozens of user interface elements and their interaction with the user. The classical solution comes from the object oriented camp where a user interface is tree of interacting mutable objects that all manage their own local state and instantiate a vast class hierarchy (Swing, I’m looking at you!). Since that is obviously unsatisfying, more declarative approaches have been developped: QML, WPF (and HTML, to some extent), try to ease construction of interfaces by using a markup language. Frameworks like AngularJS extend HTML with data binding capabilities. ReactJS generates the user interface anew every time something changes, using a clever diffing algorithm to regain performance. A multitude of libraries that claim to be functional reactive programming to some extent try to bring a more functional flavour into the mix. Despite this vast zoo of options, still I find myself not satisfied at all.

If all you have is a hammer everything looks like a nail. After all lenses in Haskell are quite nice a hammer, so in the last couple of weeks I learned to appreciate what they can do for us also in the field of functional user interfaces. When looking at a user interface, each component (e.g. text field, list view or button) has its own local state, synchronized with the state of other components in a suitable way and updated by event handlers. What if one did not keep that state local, but rather put it all in one vast state object and use lenses to zoom into that state for every particular component, associating to each component a region of the state space it can read and modify? What if a event handler in a component only sees the component’s local state, yet can be extended to work on the entire state object? This is what I want to explore in this article.

Optic UI and Embeddings

This is a literate Haskell file, so we need to state the imports first:

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
import Control.Lens
import Control.Applicative
import Control.Comonad.Representable.Store (pos, peek)
import Control.Lens.Internal.Context
import Control.Monad.Trans.Class  (MonadTrans, lift)
import Control.Monad.Trans.Reader (ReaderT, withReaderT, asks)

We will define a monad transformer UI s m a over an arbitrary monad m that represents UI components with local state s and view a. The view could be a lucid or blaze html tree, a virtual dom node, a react component or anything else that can be displayed to the user as an interface and in which event handlers can be stored.

The UI monad transformer is internally a reader monad that stores the current state s of the component, as well a continuation s -> m () to be called with the new state if a handler changes the state:

newtype UI s m a = UI { fromUI :: ReaderT (s, s -> m ()) m a }
  deriving (Functor, Applicative, Monad)

instance MonadTrans (UI s) where
  lift = UI . lift

We can’t make UI s m into a reader monad, since s appears both covariantly and contravariantly, preventing us to implement local. We can nevertheless get access to the current state:

viewUI :: Monad m => ALens' s t -> UI s m t
viewUI l = UI $ view (_1 . cloneLens l)

Now for the interesting part: Suppose we have a child component with state type t and want to use it into a larger component with state s. If we have a lens from s to t, we can view the t that lurks in the current state using that lens. Further, we can build a continuation t -> m () that updates the t in the current state and then calls the continuation s -> m () of the parent component:

embed :: Monad m => ALens' s t -> UI t m a -> UI s m a
embed l ui = UI $ withReaderT
  (\(s, fs) -> (s ^# l, fs . flip (storing l) s)) (fromUI ui)

If instead of a lens from s to t we have a traversal, we can embed copies of the child component for each target of the traversal. This allows us to display and edit collections of things or display some user interface element conditionally.

embedAll :: Monad m => ATraversal' s t -> UI t m a -> UI s m [a]
embedAll tr ui = UI $ view _1 >>= mapM go . holesOf (cloneTraversal tr) where
  go p = withReaderT (\(_, fs) -> (pos p, fs . flip peek p)) (fromUI ui)

Moving one step further, we can use an indexed traversal and and vary the child component depending on the index:

embedIxed
  :: Monad m
  => AnIndexedTraversal' i s t -> (i -> UI t m a) -> UI s m [a]
embedIxed tr ui = UI $ view _1 >>= mapM go . holesOf tr' where
  tr'     = cloneIndexedTraversal tr
  index p = getConst $ runPretext p (Indexed $ \i _ -> Const i)
  go p    = withReaderT
    (\(_, fs) -> (pos p, fs . flip peek p))
    (fromUI $ ui (index p))

A handler for an event e is a continuation function e -> m () that updates the UI when the event is triggered. From a function f :: e -> s -> m s that updates the state on an event e and the continuation s -> m () we can build a handler function e -> m (), by first calling f on the current state and then feeding the result into the continuation. Then, the resulting event handler can be put into the view, to be called if the view triggers the event.

handler :: Monad m => (e -> s -> m s) -> UI s m (e -> m ())
handler f = UI $ asks $ \(s, fs) a -> f a s >>= fs

Example 1: Registration Form

Since I’m currently fighting with getting GHCJS to do the things its supposed to do (e.g. to compile), I have not implemented any examples yet in Haskell. I have a working quick and dirty prototype in Purescript, but the lens library in Purescript is far from being as full-featured as the Haskell one, so some things aren’t possible there yet. So for the sake of discussion, suppose we have a type of views with the following API:

data View = {- ... -}                  -- ^ type of view
button    :: String -> IO () -> View   -- ^ button
label     :: String -> View            -- ^ text label
hcat      :: [View] -> View            -- ^ horizontal composition
vcat      :: [View] -> View            -- ^ vertical composition
textField :: UI String IO View         -- ^ text field
checkBox  :: UI Bool IO View           -- ^ check box

Here, the text field and the check box are components in the UI monad that update their state when the user types in the text field or checks/unchecks the box. This suffices to build a simple registration form:

data User = User
  { _userName     :: String -- ^ user name
  , _userEmail    :: String -- ^ email address
  , _userPassword :: String -- ^ password
  , _userTOS      :: Bool   -- ^ accepted terms of service
  } deriving (Eq, Show)

makeLenses ''User

form :: (User -> IO ()) -> UI User IO View
form go = do
  nameView     <- embed userName     textField
  emailView    <- embed userEmail    textField
  passwordView <- embed userPassword textField
  tosView      <- embed userTOS      checkBox
  handleSubmit <- handler (const $ liftA2 (>>) go return)
  return $ vcat
    [ hcat [ label "Name: ", nameView ]
    , hcat [ label "Email: ", emailView ]
    , hcat [ label "Password: ", passwordView ]
    , hcat [ label "I have read and accepted the terms of service: ", tosView]
    , button "Submit" (handleSubmit ())
    ]

Now we can use this form component together with embedAll and prisms to handle the registration process: We use as state Maybe User, where Nothing represents a finished registration, in which case we want just to display a message that the registration was successful, and Just u represents a yet unsubmitted form with content u.

storeUser :: User -> IO () -- ^ store a user in the database
storeUser = {- ... -}

register :: UI (Maybe User) IO View
register = do
  submit <- handler (\u _ -> storeUser u >> return Nothing)
  v <- embedAll _Nothing $ return $ label "Thank you for your registration!"
  w <- embedAll _Just    $ form submit
  return $ head (v ++ w)

Example 2: Todo Manager

Our second example is a todo manager that lets us add, remove and edit todo items of the following form:

data Todo = Todo
  { _todoName     :: String
  , _todoFinished :: Bool
  } deriving (Eq, Show)

makeLenses ''Todo

First, we define a component to display an individual todo item: There is a checkbox to mark a task as finished, a text field for the description and a delete button. The component is passed the delete action to execute when the button is clicked.

todoItem :: IO () -> UI Todo IO View
todoItem del = do
  nameView     <- embed todoName     textField
  finishedView <- embed todoFinished checkBox
  return $ hcat [ finishedView, nameView, button "Delete" del ]

Combining this and using the indexed embedding for getting the index for deleting, we can thus build a todo application with the list of tasks, a text field for the description of a new task and a button to add the new item.

todoApp :: UI (String, [Todo]) IO View
todoApp = do
  delH <- handler $ \i s -> return $ s
    & _2 %~ liftA2 (++) (take i) (drop (i + 1))
  addH <- handler $ \_ s -> return $ s
    & _1 .~ ""
    & _2 <>~ [Todo (s ^. _1) False]
  listView <- embedIxed (_2 . itraversed) (todoItem . delH)
  textView <- embed _1 textField
  return $ vcat
    [ vcat listView
    , hcat [ textView, button "Add" (addH ()) ]
    ]

Building an Application

So if you have a UI defined this way, how do you assemble this into an application? First, there needs to be a way to display the view; preferably using a diffing algorithm for performance when the view is updated. The event handlers attached to the various parts of the views should be registered to whatever display engine is used, and called on an event. The continuation passed into the UI should reevaluate the UI with the new state and update the view and the event handlers accordingly.

For more complex applications, one would want to have a model seperate from the UI state. The background model can be accessed through the monad the UI transformer wraps. The resulting architecture resembles MVVM a bit, the state of the UI being the view model. This could be particularily worthwhile for developing frontends for some web service.

Conclusion

Lenses and traversals together with a suitable monad provide a workable environment to build user interfaces in. There are a lot of things left to do now: