{ "cells": [ { "cell_type": "markdown", "metadata": { "hidden": false }, "source": [ "Recently, Jeffrey Rosenbluth published (and showcased [on Reddit](http://www.reddit.com/r/haskell/comments/2vpf0t/announcing_staticcanvas_write_html5_canvas_in/)) a pretty cool Haskell package called [static-canvas](https://hackage.haskell.org/package/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](https://github.com/jeffreyrosenbluth/static-canvas)." ] }, { "cell_type": "markdown", "metadata": { "hidden": false }, "source": [ "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](http://www.github.com/gibiansky/IHaskell), an interactive notebook-based environment for Haskell.\n", "\n", "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.\n", "\n", "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." ] }, { "cell_type": "markdown", "metadata": { "hidden": false }, "source": [ "## The Tools\n", "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.\n", "\n", "IHaskell internally uses a data type called `Display` to represent possible outputs. The `Display` data types looks like this:\n", "\n", "```haskell\n", "-- In IHaskell.Display\n", "data Display = Display [DisplayData] -- Display just one thing.\n", " | ManyDisplay [Display] -- Display several things.\n", "```\n", "In turn, the `DisplayData` data type from the `ipython-kernel` package specifies how to actually display the object in the browser:\n", "```haskell\n", "-- In IHaskell.IPython.Display\n", "data DisplayData = DisplayData MimeType Text\n", "\n", "-- All the possible ways to display things.\n", "data MimeType = PlainText\n", " | MimeHtml\n", " | MimePng Width Height -- Base64 encoded.\n", " | MimeJpg Width Height -- Base64 encoded.\n", " | MimeSvg\n", " | MimeLatex\n", " | MimeJavascript\n", "```\n", "\n", "For example, to output the string \"Hello\" in red in the browser, you might construct a value like this:\n", "```haskell\n", "redStr :: Display\n", "redStr = Display [textDisplay, htmlDisplay]\n", "\n", "textDisplay :: DisplayData\n", "textDisplay = DisplayData PlainText \"Hello\"\n", "\n", "htmlDisplay :: DisplayData\n", "htmlDisplay = DisplayData MimeHtml \"Hello\"\n", "```\n", "\n", "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.\n", "\n", "Instead of always using the data types, `IHaskell.Display` exports the following convenience functions:\n", "```haskell\n", "-- Construct displays from raw strings of different types.\n", "plain :: String -> DisplayData\n", "html :: String -> DisplayData\n", "svg :: String -> DisplayData\n", "latex :: String -> DisplayData\n", "javascript :: String -> DisplayData\n", "\n", "-- Encode into base 64.\n", "encode64 :: String -> Base64\n", "decode64 :: ByteString -> Base64\n", "\n", "-- Display images.\n", "png :: Int -> Int -> Base64 -> DisplayData\n", "jpg :: Int -> Int -> Base64 -> DisplayData\n", "\n", "-- Create final Displays.\n", "Display :: [DisplayData] -> Display\n", "many :: [Display] -> Display\n", "```\n" ] }, { "cell_type": "markdown", "metadata": { "hidden": false }, "source": [ "## Creating a Display\n", "\n", "In order to create a display for some data type, we must first import the main IHaskell display module:\n", "```haskell\n", "import IHaskell.Display\n", "```\n", "This package contains the following typeclass:\n", "```haskell\n", "class IHaskellDisplay a where\n", " display :: a -> IO Display\n", "```\n", "\n", "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. \n", "\n", "Let's go ahead and do this for `CanvasFree a` from the `static-canvas` package." ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": false, "hidden": false }, "outputs": [], "source": [ "-- Start with necessary imports.\n", "import IHaskell.Display -- From the 'ihaskell' package.\n", "import IHaskell.IPython.Types(MimeType(..))\n", "import Graphics.Static -- From the 'static-canvas' package.\n", "\n", "-- Text conversion functions.\n", "import Data.Text.Lazy.Builder(toLazyText)\n", "import Data.Text.Lazy(toStrict)" ] }, { "cell_type": "markdown", "metadata": { "hidden": false }, "source": [ "Now that we have the imports out of the way, we can define the core instance necessary:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "collapsed": false, "hidden": false }, "outputs": [], "source": [ "-- Since CanvasFree is a type synonym, we need a language pragma.\n", "{-# LANGUAGE TypeSynonymInstances #-}\n", "\n", "instance IHaskellDisplay (CanvasFree ()) where\n", " -- display :: CanvasFree () -> IO Display\n", " display canvas = return $\n", " let src = toStrict $ toLazyText $ buildScript width height canvas\n", " in Display [DisplayData MimeHtml src]\n", " \n", " where (height, width) = (200, 600)" ] }, { "cell_type": "markdown", "metadata": { "hidden": false }, "source": [ "We can now copy and paste the examples from the `static-canvas` Github page, and see them appear right in the notebook!" ] }, { "cell_type": "code", "execution_count": 34, "metadata": { "collapsed": false, "hidden": false }, "outputs": [ { "data": { "text/html": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "{-# LANGUAGE OverloadedStrings #-}\n", "import Graphics.Static.ColorNames\n", "\n", "text :: CanvasFree ()\n", "text = do\n", " font \"italic 60pt Calibri\"\n", " lineWidth 6\n", " strokeStyle blue\n", " fillStyle goldenrod\n", " textBaseline TextBaselineMiddle\n", " strokeText \"Hello\" 150 100 \n", " fillText \"Hello World!\" 150 100\n", " \n", "text" ] }, { "cell_type": "markdown", "metadata": { "hidden": false }, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "collapsed": false, "hidden": false }, "outputs": [], "source": [ "data Canvas = Canvas {\n", " width :: Int,\n", " height :: Int,\n", " canvas :: CanvasFree ()\n", " }" ] }, { "cell_type": "markdown", "metadata": { "hidden": false }, "source": [ "Then we could define an `IHaskellDisplay` that respects this width and height:" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "collapsed": false, "hidden": false }, "outputs": [], "source": [ "{-# LANGUAGE TypeSynonymInstances #-}\n", "instance IHaskellDisplay Canvas where\n", " -- display :: Canvas -> IO Display\n", " display cnv = return $\n", " let src = toStrict $ toLazyText $ buildScript (width cnv) (height cnv) (canvas cnv)\n", " in Display [DisplayData MimeHtml src]" ] }, { "cell_type": "markdown", "metadata": { "hidden": false }, "source": [ "Then when we use this we can specify how to display our canvases:\n", "```haskell\n", "Canvas 200 600 $ do\n", " font \"italic 60pt Calibri\"\n", " lineWidth 6\n", " strokeStyle blue\n", " fillStyle goldenrod\n", " textBaseline TextBaselineMiddle\n", " strokeText \"Hello\" 150 100 \n", " fillText \"Hello World!\" 150 100\n", "```\n", "\n", "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!" ] }, { "cell_type": "markdown", "metadata": { "hidden": false }, "source": [ "## Packaging IHaskell Display Addons\n", "\n", "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!\n", "\n", "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`. \n", "\n", "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!)\n", "\n", "If you have any opinions about this, or suggestions for how to improve this process, please let me know!\n", "\n", "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!" ] } ], "metadata": { "kernelspec": { "display_name": "Haskell", "language": "haskell", "name": "haskell" }, "language_info": { "name": "haskell", "version": "7.8.3" } }, "nbformat": 4, "nbformat_minor": 0 }