What if I told you that a spreadsheet could be a library instead of an application? What would that even mean? How do we distill the logic behind spreadsheets into a reusable abstraction? My mvc-updates
library answers this question by bringing spreadsheet-like programming to Haskell using an intuitive Applicative
interface.
The central abstraction is an Applicative
Updatable
value, which is just a Fold
sitting in front of a Managed
Controller
:
data Updatable a =
forall e . On (Fold e a) (Managed (Controller e))
The Managed
Controller
originates from my mvc
library and represents a resource-managed, concurrent source of values of type e
. Using Monoid
operations, you can interleave these concurrent resources together while simultaneously merging their resource management logic.
The Fold
type originates from my foldl
library and represents a reified left fold. Using Applicative
operations, you can combine multiple folds together in such a way that they still pass over the data set just once without leaking space.
To build an Updatable
value, just pair up a Fold
with a Managed
Controller
:
import Control.Foldl (last, length)
import MVC
import MVC.Updates
import MVC.Prelude (stdinLines, tick)
import Prelude hiding (last, length)
-- Store the last line from standard input
lastLine :: Updatable (Maybe String)
lastLine = On last stdinLines
-- Increment every second
seconds :: Updatable Int
seconds = On length (tick 1.0)
What's amazing is that when you stick a Fold
in front of a Controller
, you get a new Applicative
. This Applicative
instance lets you combine multiple Updatable
values into new derived Updatable
values. For example, we can combine lastLine
and seconds
into a single data type that tracks updates to both values:
import Control.Applicative ((<$>), (<*>))
data Example = Example (Maybe String) Int deriving (Show)
example :: Updatable Example
example = Example <$> lastLine <*> seconds
example
will update every time lastLine
or seconds
updates, caching and reusing portions that do not update. For example, if lastLine
updates then only the first field of Example
will change. Similarly, if seconds
updates then only the second field of Example
will change.
When we're done combining Updatable
values we can plug them into mvc
using the updates
function:
updates :: Buffer a -> Updatable a -> Managed (Controller a)
This gives us back a Managed
Controller
we can feed into our mvc
application:
viewController :: Managed (View Example, Controller Example)
viewController = do
controller <- updates Unbounded example
return (asSink print, controller)
model :: Model () Example Example
model = asPipe $
Pipes.takeWhile (\(Example str _) -> str /= Just "quit")
main :: IO ()
main = runMVC () model viewController
This program updates in response to two concurrent inputs: time and standard input.
$ ./example
Example Nothing 0
Test<Enter>
Example (Just "Test") 0 <-- Update: New input
Example (Just "Test") 1 <-- Update: 1 second passed
Example (Just "Test") 2 <-- Update: 1 second passed
ABC<Enter>
Example (Just "ABC") 2 <-- Update: New input
Example (Just "ABC") 3 <-- Update: 1 second
quit<Enter>
$
Spreadsheets
The previous example was a bit contrived, so let's step it up a notch. What better way to demonstrate spreadsheet-like programming than .. a spreadsheet!
I'll use Haskell's gtk
library to set up the initial API, which consists of a single exported function:
module Spreadsheet (spreadsheet) where
spreadsheet
:: Managed -- GTK setup
( Updatable Double -- Create input cell
, Managed (View Double) -- Create output cell
, IO () -- Start spreadsheet
)
spreadsheet = ???
You can find the full source code here.
Using spreadsheet
, I can now easily build my own spread sheet application:
{-# LANGUAGE TemplateHaskell #-}
import Control.Applicative (Applicative, (<$>), (<*>))
import Lens.Family.TH (makeLenses)
import MVC
import MVC.Updates (updates)
import Spreadsheet (spreadsheet)
-- Spreadsheet input (4 cells)
data In = I
{ _i1 :: Double
, _i2 :: Double
, _i3 :: Double
, _i4 :: Double
}
-- Spreadsheet output (4 cells)
data Out = O
{ _o1 :: Double
, _o2 :: Double
, _o3 :: Double
, _o4 :: Double
}
makeLenses ''Out
-- Spreadsheet logic that converts input to output
model :: Model () In Out
model = asPipe $ loop $ \(I i1 i2 i3 i4) -> do
return $ O (i1 + i2) (i2 * i3) (i3 - i4) (max i4 i1)
main :: IO ()
main = runMVC () model $ do
(inCell, outCell, go) <- spreadsheet
-- Assemble the four input cells
c <- updates Unbounded $
I <$> inCell <*> inCell <*> inCell <*> inCell
-- Assemble the four output cells
v <- fmap (handles o1) outCell
<> fmap (handles o2) outCell
<> fmap (handles o3) outCell
<> fmap (handles o4) outCell
-- Run the spread sheet
liftIO go
return (v, c)
You can install this program yourself by building my mvc-updates-examples
package on Github:
$ git clone https://github.com/Gabriel439/Haskell-MVC-Updates-Examples-Library.git
$ cd Haskell-MVC-Updates-Examples-Library
$ cabal install
$ ~/.cabal/bin/mvc-spreadsheet-example
Or you can watch this video of the spreadsheet in action:
The key feature I want to emphasize is how concise this spreadsheet API is. We provide our user an Applicative
input cell builder and a Monoid
output cell builder, and we're done. We don't have to explain to the user how to acquire resources, manage threads, or combine updates. The Applicative
instance for Updatable
handles all of those trivial details for them. Adding extra inputs or outputs is as simple as chaining additional inCell
and outCell
invocations.
Reactive animations
We don't have to limit ourselves to spread sheets, though. We can program Updatable
graphical scenes using these same principles. For example, let's animate a cloud that orbits around the user's mouse using the sdl
library. Just like before, we will begin from a concise interface:
-- Animation frames for the cloud
data Frame = Frame0 | Frame1 | Frame2 | Frame3
-- To draw a cloud we specify the frame and the coordinates
data Cloud = Cloud Frame Int Int
-- mouse coordinates
data Mouse = Mouse Int Int
sdl :: Managed -- SDL setup
( View Cloud -- Draw a cloud
, Updatable Mouse -- Updatable mouse coordinates
)
The full source is located here.
In this case, I want to combine the Updatable
mouse coordinates with an Updatable
time value:
main :: IO ()
main = runMVC () (asPipe cat) $ do
(cloudOut, mouse) <- sdl
let seconds = On length (tick (1 / 60))
toFrame n = case (n `div` 15) `rem` 4 of
0 -> Frame0
1 -> Frame1
2 -> Frame2
_ -> Frame3
cloudOrbit t (Mouse x y) = Cloud (toFrame t) x' y'
where
x' = x + truncate (100 * cos (fromIntegral t / 10))
y' = y + truncate (100 * sin (fromIntegral t / 10))
cloudIn <- updates Unbounded (cloudOrbit <$> seconds <*> mouse)
return (cloudOut, cloudIn)
cloudOrbit
is defined as a pure function from the current time and mouse coordinates to a Cloud
. With the power of Applicative
s we can lift this pure function over two Updatable
values (mouse
and seconds
) to create a new Updatable
Cloud
that we pass intact to our program's View
.
Like before, you can either run this program yourself:
$ git clone https://github.com/Gabriel439/Haskell-MVC-Updates-Examples-Library.git
$ cd Haskell-MVC-Updates-Examples-Library
$ cabal install
$ ~/.cabal/bin/mvc-spreadsheet-example
... or you can watch the video:
Under the hood
mvc-updates
distinguishes itself from similar libraries in other languages by not relying on a semantics for concurrency. The Applicative
instance for Updatable
uses no concurrent operations, whatsoever:
instance Applicative Updatable where
pure a = On (pure a) mempty
(On foldL mControllerL) <*> (On foldR mControllerR)
= On foldT mControllerT
where
foldT = onLeft foldL <*> onRight foldR
mControllerT = fmap (fmap Left ) mControllerL
<> fmap (fmap Right) mControllerR
onLeft (Fold step begin done) =
Fold step' begin done
where
step' x (Left a) = step x a
step' x _ = x
onRight (Fold step begin done) =
Fold step' begin done
where
step' x (Right a) = step x a
step' x _ = x
In fact, this Applicative
instance only assumes that the Controller
type is a Monoid
, so this trick generalizes to any source that forms a Monoid
.
This not only simplifies the proof of the Applicative
laws, but it also greatly improves efficiency. This Applicative
instance introduces no new threads or buffers. The only thread or buffer you will incur is in the final call to the updates
function, but expert users can eliminate even that overhead by inlining the logic of the updates
function directly into their mvc
program.
Lightweight
The mvc-updates
library is incredibly small. Here's the entire API:
data Updatable = forall e . On (Fold e a) (Controller e)
instance Functor Updatable
instance Applicative Updatable
updates :: Buffer a -> Updatable a -> Managed (Controller a)
The library is very straightforward to use:
- Build
Updatable
values - Combine them using their
Applicative
instance - Convert them back to a
Managed
Controller
when you're done
That's it!
The small size of the library is no accident. The Updatable
abstraction is an example of a scalable program architecture. When we combine Updatable
values together, the end result is a new Updatable
value. This keeps the API small since we always end up back where we started and we never need to introduce additional abstractions.
There is no need to distinguish between "primitive" Updatable
values or "derived" Updatable
values or "sheets" of Updatable
values. The Applicative
interface lets us unify these three concepts into a single uniform concept. Moreover, the Applicative
interface is one of Haskell's widely used type classes inspired by category theory, so we can reuse people's pre-existing intuition for how Applicative
s work. This is a common theme in Haskell where once you learn the core set of mathematical type classes they go a very, very long way.
Conclusion
Hopefully this post will get you excited about the power of Applicative
programming. If you would like to learn more about Applicative
s, I highly recommend the "Applicative Programming with Effects" paper by Conor McBride and Ross Paterson.
I would like to conclude by saying that there many classes of problems that the mvc-updates
library does not solve well, such as:
- build systems,
- programs with computationally expensive
View
s, and: Updatable
values that share state.
However, mvc-updates
excels at:
- data visualizations,
- control panels, and:
- spread sheets (of course).
You can find the mvc-updates
library up on Hackage or on Github.