Recently, Jeffrey Rosenbluth published (and showcased on Reddit) a pretty cool Haskell package called static-canvas. This package uses the free monad DSL pattern to make a DSL for programming for HTML5 canvas, restricted to fairly simple static use cases. While you can't use this to make user interfaces, it's still potentially a pretty cool tool, and there's a few very clear examples on the GitHub readme.

As with most things involving pretty graphics or pictures, I think this would be a whole ton of fun to experiment with interactively, making it a great fit for IHaskell, an interactive notebook-based environment for Haskell.

IHaskell allows the creation of "addon" packages to specify how to display various data types in its browser-based UI. These addons can render data types as text, as images, or even as HTML mixed with Javascript; they can even render them as interactive Javascript widgets that can evaluate Haskell code at will. All of this is done without GHCJS or similar Haskell-to-Javascript compilation tools.

However, these display packages have mostly been written by only a few people, those fairly closely involved with IHaskell development. As the creator of IHaskell, I'd love to have more of these packages, but I obviously can't create display instances for all existing packages, and certainly can't anticipate what people might want for their own packages or new ones. Thus, I'd love to use this very neat library as a showcase and tutorial for how to make IHaskell display packages.

The Tools

In this section, I'll very briefly introduce you to the tools IHaskell provides for creating IHaskell display packages. If you'd like to get to the real meat of this tutorial, skip this, read the next section, and maybe come back here if you need to.

IHaskell internally uses a data type called Display to represent possible outputs. The Display data types looks like this:

-- In IHaskell.Display
data Display = Display [DisplayData] -- Display just one thing.
             | ManyDisplay [Display] -- Display several things.

In turn, the DisplayData data type from the ipython-kernel package specifies how to actually display the object in the browser:

-- In IHaskell.IPython.Display
data DisplayData = DisplayData MimeType Text

-- All the possible ways to display things.
data MimeType = PlainText
              | MimeHtml
              | MimePng Width Height -- Base64 encoded.
              | MimeJpg Width Height -- Base64 encoded.
              | MimeSvg
              | MimeLatex
              | MimeJavascript

For example, to output the string "Hello" in red in the browser, you might construct a value like this:

redStr :: Display
redStr = Display [textDisplay, htmlDisplay]

textDisplay :: DisplayData
textDisplay = DisplayData PlainText "Hello"

htmlDisplay :: DisplayData
htmlDisplay = DisplayData MimeHtml "<span style=\"color: red;\">Hello</span>"

You may note that Display takes a list of DisplayData values; this allows IHaskell to choose the proper display mechanism for the frontend. The frontend can be a console or the in-browser notebook, and the in-browser notebook may have different preferences for displays, so by providing different ways to render output, the best possible rendering can be chosen for each interface.

Instead of always using the data types, IHaskell.Display exports the following convenience functions:

-- Construct displays from raw strings of different types.
plain :: String -> DisplayData
html :: String -> DisplayData
svg :: String -> DisplayData
latex :: String -> DisplayData
javascript :: String -> DisplayData

-- Encode into base 64.
encode64 :: String -> Base64
decode64 :: ByteString -> Base64

-- Display images.
png :: Int -> Int -> Base64 -> DisplayData
jpg :: Int -> Int -> Base64 -> DisplayData

-- Create final Displays.
Display :: [DisplayData] -> Display
many :: [Display] -> Display

Creating a Display

In order to create a display for some data type, we must first import the main IHaskell display module:

import IHaskell.Display

This package contains the following typeclass:

class IHaskellDisplay a where
  display :: a -> IO Display

In order to display a data type, create an instance of IHaskellDisplay for your data type – then, any expression that results in your data type will generate a corresponding display.

Let's go ahead and do this for CanvasFree a from the static-canvas package.

In [12]:
-- Start with necessary imports.
import IHaskell.Display -- From the 'ihaskell' package.
import IHaskell.IPython.Types(MimeType(..))
import Graphics.Static  -- From the 'static-canvas' package.

-- Text conversion functions.
import Data.Text.Lazy.Builder(toLazyText)
import Data.Text.Lazy(toStrict)

Now that we have the imports out of the way, we can define the core instance necessary:

In [24]:
-- Since CanvasFree is a type synonym, we need a language pragma.
{-# LANGUAGE TypeSynonymInstances #-}

instance IHaskellDisplay (CanvasFree ()) where
  -- display :: CanvasFree () -> IO Display
  display canvas = return $
    let src = toStrict $ toLazyText $ buildScript width height canvas
    in Display [DisplayData MimeHtml src]
    where (height, width) = (200, 600)

We can now copy and paste the examples from the static-canvas Github page, and see them appear right in the notebook!

In [34]:
{-# LANGUAGE OverloadedStrings #-}
import Graphics.Static.ColorNames

text :: CanvasFree ()
text = do
  font "italic 60pt Calibri"
  lineWidth 6
  strokeStyle blue
  fillStyle goldenrod
  textBaseline TextBaselineMiddle
  strokeText "Hello" 150 100 
  fillText "Hello World!" 150 100

As we play with this a little more, we see that this is a little bit unsatisfactory. Specifically, the width and the height of the resulting canvas are fixed in the IHaskellDisplay instance! I would solve this by creating a custom Canvas data type that stores these:

In [26]:
data Canvas = Canvas {
    width :: Int,
    height :: Int,
    canvas :: CanvasFree ()

Then we could define an IHaskellDisplay that respects this width and height:

In [27]:
{-# LANGUAGE TypeSynonymInstances #-}
instance IHaskellDisplay Canvas where
  -- display :: Canvas -> IO Display
  display cnv = return $
    let src = toStrict $ toLazyText $ buildScript (width cnv) (height cnv) (canvas cnv)
    in Display [DisplayData MimeHtml src]

Then when we use this we can specify how to display our canvases:

Canvas 200 600 $ do
  font "italic 60pt Calibri"
  lineWidth 6
  strokeStyle blue
  fillStyle goldenrod
  textBaseline TextBaselineMiddle
  strokeText "Hello" 150 100 
  fillText "Hello World!" 150 100

Sadly, it seems that the static-canvas library currently only supports having one generated canvas on the page – if you try to add another one, it simply modifies the pre-existing one. This is probably a bug that should be fixed, though!

Packaging IHaskell Display Addons

Once you've made an IHaskell display instance, you can easily package it up and stick it on Hackage. Specifically, for a package named package-name, you should take everything before the -. Then, prepend ihaskell- to the package name. Finally, make sure there exists a module IHaskell.Display.Package, where Package is the first word in package-name capitalized. If this is done, then IHaskell will happily load your package and instance upon startup, making it very easy for your users to install the display addon!

For example, the hatex library is exposed as an addon through the ihaskell-hatex display package and the IHaskell.Display.Hatex module in that package. The juicypixels library has an addon package called ihaskell-juicypixels with a module IHaskell.Display.Juicypixels.

As I write this now, I realize that this protocol is a little bit weird. Specifically, I think that perhaps the rule that you take the first thing before the - is not too great, but rather that perhaps the - should be a word separator, and thus package-name would get translated to ihaskell-package-name and IHaskell.Display.PackageName. (We do need some standard!)

If you have any opinions about this, or suggestions for how to improve this process, please let me know!

Anyway, I hope that this brief tutorial / guide can show someone how to write small IHaskell addons. Perhaps someone will find this useful, and please get in touch if you have any questions, comments, or suggestions!