Haskell to JavaScript

2016-07-04  

David Lettier  

Preview

Below you see the stylized push button we will create with Haskell.

Notice how the button class and its inner contents change with every mouse click.

Setup

In order to establish our build pipeline, we will need to setup our environment with a few tools, directories, and project files.

Stack

Stack will handle all of our Haskell environment needs from installing the compiler to managing our Haskell dependencies. The instructions contain all of the installation information you will need.

For a general Linux installation you could do:

cd Downloads
wget https://www.stackage.org/stack/linux-x86_64
tar xvzf stack-1.1.2-linux-x86_64.tar.gz
cd ~
echo 'export PATH="$HOME/Downloads/stack-1.1.2-linux-x86_64/:$PATH"' >> .bashrc
source .bashrc
# Or if you use ZSH.
echo 'export PATH="$HOME/Downloads/stack-1.1.2-linux-x86_64/:$PATH"' >> .zshrc
source .zshrc
stack

Project Directory

With Stack installed, we will need to create our project directory.

cd ~
mkdir -p onOffButton

We will also need to define some additional directories for our project.

cd ~/onOffButton
mkdir -p app dist src static/scss

With the directories in place, create the empty project files.

cd ~/onOffButton
touch app/Main.hs src/Lib.hs static/scss/style.scss

YAML

To use Stack, you will need to define a stack.yaml file in the root of the directory.

cd ~/onOffButton
touch stack.yaml

Go ahead and open this file with your favorite text editor and copy this information into the stack.yaml file.

resolver: lts-6.4
packages:
- '.'
extra-deps:
  - haskell-src-exts-1.16.0.1
  - ref-tf-0.4
  - reflex-0.4.0
  - reflex-dom-0.3
  - hlint-1.9.35
flags: {}
extra-package-dbs: []
compiler: ghcjs-0.2.0.20160414_ghc-7.10.3
compiler-check: match-exact
setup-info:
  ghcjs:
    source:
      ghcjs-0.2.0.20160414_ghc-7.10.3:
        url: https://s3.amazonaws.com/ghcjs/ghcjs-0.2.0.20160414_ghc-7.10.3.tar.gz
        sha1: 6d6f307503be9e94e0c96ef1308c7cf224d06be3

Cabal

We will also need an onOffButton.cabal file which Stack will use to build our project. Make sure this file is located in the root of the project.

cd ~/onOffButton
touch onOffButton.cabal

Go ahead and open this file with your favorite text editor and copy this information into the onOffButton.cabal file.

name:                onOffButton
author:              David Lettier
version:             0.0.0.1
build-type:          Simple
cabal-version:       >=1.10

library
  hs-source-dirs:      src
  exposed-modules:     Lib
  build-depends:       base
                     , haskell-src-exts >= 1.16 && < 1.18
                     , reflex == 0.4.0
                     , reflex-dom
                     , containers == 0.5.*
                     , safe == 0.3.*
  default-language:    Haskell2010

executable onOffButton-exe
  hs-source-dirs:      app
  main-is:             Main.hs
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  build-depends:       base
                     , onOffButton
                     , haskell-src-exts >= 1.16 && < 1.18
                     , reflex == 0.4.0
                     , reflex-dom
                     , containers == 0.5.*
                     , safe == 0.3.*
  default-language:    Haskell2010

GHCJS

GHCJS will be our main dependency. This is the dependency that will help transpile our Haskell source code to JavaScript.

At this point we can finish the Stack setup.

cd ~/onOffButton
stack setup # This will take awhile.

NVM

To make building the project easier, we will use some NPM scripts but first we need to install Node.js using NVM.

cd ~/Downloads
wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.31.2/install.sh | bash
cd ~/onOffButton
echo 'v4.0.0' > .nvmrc
nvm use

NPM Package

Our build scripts will be defined in a NPM package.json file.

cd ~/onOffButton
touch package.json

Open up your favorite text editor and copy this into the package.json file.

{
  "name": "onOffButton",
  "author": "David Lettier",
  "scripts": {
    "scss": "mkdir -p dist/ && node-sass --output-style compressed static/scss/style.scss dist/css/style.css",
    "autoprefixer": "postcss -u autoprefixer -r dist/css/style.css dist/css/style.css",
    "uglify": "mkdir -p dist/ && uglifyjs dist/lib.js -o dist/lib.js -c -m && uglifyjs dist/rts.js -o dist/rts.js -c -m && uglifyjs dist/out.js -o dist/out.js -c -m",
    "copy:stackbuild": "mkdir -p dist/ && cp -R ./.stack-work/install/x86_64-linux/lts-6.4/ghcjs-0.2.0_ghc-7.10.3/bin/onOffButton-exe.jsexe/. dist",
    "clean:haskell:full": "stack clean --full",
    "clean:haskell": "stack clean",
    "clean:dist": "rm -rf dist && mkdir -p dist/",
    "clean:all": "npm run clean:haskell && npm run clean:dist",
    "build:js": "stack build && npm run copy:stackbuild && npm run uglify",
    "build:css": "npm run scss && npm run autoprefixer",
    "build:dist": "mkdir -p dist/ && npm run build:js && npm run build:css",
    "serve": "browser-sync start --server 'dist/' --files 'dist/css/*.css, dist/*.js, **/*.html, !node_modules/**/*.html'",
    "watch:index": "onchange 'app/Main.hs' -- npm run build:js",
    "watch:css": "onchange 'static/scss/*.scss' -- npm run build:css",
    "watch:all": "npm-run-all -p serve watch:index watch:css"
  },
  "devDependencies": {
    "autoprefixer": "^6.3.1",
    "browser-sync": "^2.11.1",
    "node-sass": "^3.4.2",
    "npm-run-all": "^1.5.1",
    "onchange": "^2.0.0",
    "postcss-cli": "^2.4.0",
    "uglify-js": "^2.6.1"
  }
}

This will allows us to perform commands like npm run build:js and have our transpiled JavaScript files located in dist/.

Before we can run any NPM scripts, we must install all of our Node.js build dependencies.

cd ~/onOffButton
npm install

Source Code

Now that our development environment is setup, we can begin coding our project.

Haskell

The majority of the source code will be written in Haskell. If you have never programmed in Haskell before do not worry as we will cover each part line by line.

Setup

The first file we will create is the simplest.

cd ~/onOffButton
touch Setup.hs

The Setup.hs file is located below.

import Distribution.Simple
main = defaultMain

Here we import the distribution simple package and define the main entry point as defaultMain.

This is the command line front end to the Simple build system. When given the parsed command-line args and package information, is able to perform basic commands like configure, build, install, register, etc.

Main

The next file we need to write is the app/Main.hs file which is the starting point of our Haskell to JavaScript project.

cd ~/onOffButton
touch app/Main.hs
{-# LANGUAGE OverloadedStrings #-}

This language extension allows us to take a string literal such as "Some text." and use it for arguments that require type ByteString, Text, or String/[Char] (a ['l','i','s','t'] or array of characters).

import Lib
import Reflex
import Reflex.Dom
import qualified Data.Map as Map

The first import is our own project library that we will define in src/Lib.hs later on. The two Reflex and Reflex.Dom imports are related to the Reflex-FRP project. These two packages together will help us write Haskell code that will translate to JavaScript and HTML. The last import–Map–allows us to define hash tables with keys and values.

main :: IO ()
main = mainWidgetWithHead headElement bodyElement

This is the entry or starting point of our application. The first line defines its output as an empty IO context or input-output monad unit. Think of () as void. The second line equates the main function to mainWidgetWithHead which takes two arguments. The first argument is the HTML head section <head><!--...--></head> while the second argument defines the HTML body section <body><!--...--></body>.

headElement :: MonadWidget t m => m ()
headElement = do

Here we begin the headElement function definition. Its type declaration says that, given that the MonadWidget interface (type-class) is defined for some type m, it returns a m empty context or monad unit. In the second line we begin the do block.

  el "title" (text "On/Off Button")

This function takes a HTML tag name and some text that will go inside the HTML tag. After we build the project, this line will translate to <title>On/Off Button</title>. Note that el is short for element.

  styleSheet "css/style.css"
  styleSheet "https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css"

styleSheet is a function we define below. It takes a single argument–the location of the style sheet. We will be using some icons so we will need Font Awesome.

  where
    styleSheet link = elAttr "link" (Map.fromList [
          ("rel", "stylesheet")
        , ("type", "text/css")
        , ("href", link)
      ]) $ return ()

Our style sheet function takes a string (the href value) and returns an element with three attributes defined. This will translate to <link rel="stylesheet" type="text/css" href=link />. Notice how we use the Map package here–defining three keys and values which become the element’s attributes.

bodyElement :: MonadWidget t m => m ()
bodyElement = el "div" $ onOffButton

Our bodyElement function takes no arguments and creates a parent div element which holds whatever element(s) is/are returned by the onOffButton function defined in src/Lib.hs.

<!--...-->
  <div>
    <!-- onOffButton output -->
  </div>
<!--...->

Here is app/Main.hs in its entirety.

{-
  David Lettier (C) 2016.
  http://www.lettier.com/
-}

{-# LANGUAGE OverloadedStrings #-}

import Lib
import Reflex
import Reflex.Dom
import qualified Data.Map as Map

main :: IO ()
main = mainWidgetWithHead headElement bodyElement

headElement :: MonadWidget t m => m ()
headElement = do
  el "title" (text "On/Off Button")
  styleSheet "css/style.css"
  styleSheet "https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css"
  where
    styleSheet link = elAttr "link" (Map.fromList [
          ("rel", "stylesheet")
        , ("type", "text/css")
        , ("href", link)
      ]) $ return ()

bodyElement :: MonadWidget t m => m ()
bodyElement = el "div" $ onOffButton

Lib

{-# LANGUAGE RecursiveDo #-}

This language extension allows us to do something like this:

someFuncZero = do
  rec
    a <- someFuncOne b
    b <- someFuncTwo a
  return ()

Notice how a depends on b and b depends on a and a depends on b and … recursively.

module Lib (
  onOffButton
) where

Our main application file, app/Main.hs, relies on the onOffButton function. In order to access it, we export it from our library src/Lib.hs.

import Reflex
import Reflex.Dom
import qualified Data.Map as Map

These imports are the same as the ones in app/Main.hs.

onOffButton :: MonadWidget t m => m ()
onOffButton = do

The function onOffButton takes no arguments and produces a push button on the generated HTML page.

  rec
    let onButtonClass  = Map.singleton "class" "onButton"
    let offButtonClass = Map.singleton "class" "offButton"

Here we define two hashes which only contain a single key and value each. These will specify the class="..." attribute for the <button></button> element we generate below.

    (buttonElement, _) <- elDynAttr' "button" cssClassDynamic $ elDynHtml' "span" textDynamic

The main element on the page will be our button. Its CSS class is dynamic (will change). The CSS class is different based on the button’s state. The button has two states: down or up.

elDynAttr' generates an element that has dynamic attributes and returns a tuple containing the element created and a monad. We will only use the returned element buttonElement. The function elDynHtml' generates an element that has dynamic inner HTML. The $ function, in this context, will set the element generated by elDynHtml' as the child of the element generated by elDynAttr'.

This line will roughly translate to:

<!--...-->
  <button class="...">
    <span><!--...--></span>
  </button>
<!--...-->
    buttonElementClickEvent <- return $ domEvent Click buttonElement
    toggleDynamic <- toggle False buttonElementClickEvent

Here we setup a toggling dynamic that starts off with a False value. Every time the button is clicked, the toggleDynamic will flip to !toggleDynamic (false, true, false, true, …).

    cssClassDynamic <- mapDyn (
        \ x -> if x then (
          onButtonClass :: Map.Map String String
        ) else (
          offButtonClass :: Map.Map String String
        )
      ) toggleDynamic

Using the x value extracted from the toggle dynamic, we return either the “on” CSS class (if the toggle is true) or we return the “off” CSS class (if the toggle is false). The Map.Map String String parts are type annotations which say that on/OffButtonClass has a type where the hash key and value are type String. The ( \ x -> ... ) syntax is a lambda or anonymous function.

    textDynamic <- mapDyn (
        \ x -> if x then (
          "<i class='fa fa-circle-o-notch' aria-hidden='true'></i>" :: String
        ) else (
          "<i class='fa fa-power-off' aria-hidden='true'></i>" :: String
      )) toggleDynamic
  return ()

Similar to the cssClassDynamic, we define the dynamic inner HTML based on the x value extracted from toggleDynamic. True evaluates to the Font Awesome icon fa-circle-o-notch while False evaluates to the Font Awesome icon fa-power-off. These icons will show up on the face of the button and will swap with each button press.

Here is src/Lib.hs in its entirety.

{-
  David Lettier (C) 2016.
  http://www.lettier.com/
-}

{-# LANGUAGE RecursiveDo #-}

module Lib (
  onOffButton
) where

import Reflex
import Reflex.Dom
import qualified Data.Map as Map

onOffButton :: MonadWidget t m => m ()
onOffButton = do
  rec
    let onButtonClass  = Map.singleton "class" "onButton"
    let offButtonClass = Map.singleton "class" "offButton"
    (buttonElement, _) <- elDynAttr' "button" cssClassDynamic $ elDynHtml' "span" textDynamic
    buttonElementClickEvent <- return $ domEvent Click buttonElement
    toggleDynamic <- toggle False buttonElementClickEvent
    cssClassDynamic <- mapDyn (
        \ x -> if x then (
          onButtonClass :: Map.Map String String
        ) else (
          offButtonClass :: Map.Map String String
        )
      ) toggleDynamic
    textDynamic <- mapDyn (
        \ x -> if x then (
          "<i class='fa fa-circle-o-notch' aria-hidden='true'></i>" :: String
        ) else (
          "<i class='fa fa-power-off' aria-hidden='true'></i>" :: String
      )) toggleDynamic
  return ()

SCSS

To make the button more visually appealing we will style it using Sass.

cd ~/onOffButton
touch static/scss/style.scss

Here is the static/scss/style.scss file in its entirety.

body {
  margin:  0px;
  padding: 0px;
  background-color: #252b3a;
}
div {
  text-align: center;
}

$buttonSize: 300px;

button {
  width:  $buttonSize;
  height: $buttonSize;
  border-radius: 100%;
  border-width: 20px;
  border-color: #546170;
  font-size: 50px;
  color: white;
  text-shadow: 0px 0px 40px whitesmoke;
  position: absolute;
  top: 40%;
  outline: none;
  cursor: pointer;
  margin-left: $buttonSize / 2 * -1;
}
.offButton {
  background-color: #db5461;
  border-style: outset;
}
.onButton {
  background-color: #54d367;
  border-style: inset;
}

Notice the .offButton and .onButton CSS classes.

Build

With our source code files in place, we can build our project and view it in the browser.

cd ~/onOffButton
nvm use
npm run build:dist
npm run watch:all

The final project structure is shown below.

onOffButton/
  app/
    Main.hs
  dist/
    css/
      style.css
    all.js
    lib.js
    out.js
    rtl.js
    runmain.js
    index.html
  node_modules/
    ...
  src/
    Lib.hs
  static/
    scsss/
      style.scss
  onOffButton.cabal
  package.json
  Setup.hs
  stack.yaml
  .nvmrc

Output

After opening dist/index.html in your browser, the following output will be produced.

<html>
  <head>
    <title>On/Off Button</title>
    <link href="css/style.css" rel="stylesheet" type="text/css">
    <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" type="text/css">
  </head>
  <body>
    <div>
      <button class="offButton">
        <span>
          <i class="fa fa-power-off" aria-hidden="true"></i>
        </span>
      </button>
    </div>
  </body>
</html>

Wrap-up

Using an array of tools and the Haskell programming language, we built a push button interface for a web browser. All of our Haskell source code was transpiled to HTML and JavaScript.

If you enjoyed programming in Haskell, be sure to read Building a Haskell Web API.