Haskell in the Browser With Haste

Lars Kuhtz

2014-05-17

Preparation

Get the code:

git clone https://github.com/alephcloud/bayhac2014
cd bayhac2014

Build Haste:

cabal sandbox init
echo 'constraints: scientific<0.3' >> cabal.config
cabal install haste-compiler -j
export PATH=./.cabal-sandbox/bin:$PATH

Setup Haste:

echo 'solver: topdown' >> ~/.cabal/config
haste-boot
sed -i '/solver: topdown/d' ~/.cabal/config

Topics

  1. Setup and usage of Haste

  2. Development of portable libraries for web applications

Part 1: Setup and Usage

Outline

  1. Installation and Setup
  2. Compiling and Deploying an Application
  3. FFI
  4. Marshaling of data and callbacks

Setup Haste

cabal install haste-compiler
haste-boot

well ...

echo 'constraints: scientific<0.3' >> cabal.config
cabal install haste-compiler
echo 'solver: topdown' >> ~/.cabal/config
haste-boot
sed -i '/solver: topdown/d' ~/.cabal/config

Hello World with Haste

contrib/hello-bayhac.hs:

module Main
( main
) where

import Haste

main :: IO ()
main = alert "hello bayhac"

Compile:

hastec hello-bayhac.hs

contrib/hello-bayhac.html:

<!DOCTYPE html>
<html>
    <head> <script type="text/javascript" src="./hello-bayhac.js"></script></head>
    <body/>
</html>

Example

Haste Runtime

Thunks:

function T(f) {
    this.f = new F(f);
}

function F(f) {
    this.f = f;
}

Evaluate to head normal form:

function E(t) { /* ... */ }

Partial application:

function A(f, args) { /* ... */ }

Haste Runtime

  • Arithmetic
  • Strings: toJSStr, fromJSStr
  • jsAlert, jsPrompt, jsLog, jsEval, etc.
  • jsSetTimeout
  • DOM manipulation
  • Arrays and Pointers
  • etc.

Compiled Code

contrib/hello-bayhac.hs:

module Main
( main
) where

import Haste

main :: IO ()
main = alert "hello bayhac"

Result:

var _0=0,
    _1=unCStr("hello bayhac"),
    _2=function(_){var _3=jsAlert(toJSStr(E(_1)));return _0;},
    _4=function(_){return _2(_);};
var hasteMain = function() {A(_4, [0]);};
window.onload = hasteMain;

Marshaling

[<Constructor ID>, <Constructor Args>, ...]

Examples:

Haskell Javascript
5 :: Int [0,5]
Nothing :: Maybe () [0,[0]]
Just () :: Maybe () [1,[0]]
[1,2] :: [Int] [1,[0,1],[1,[0,2],[0]]]

Marshaling

Example code from the Haste runtime:

function arr2lst(arr, elem) {
    if(elem >= arr.length) {
        return [0];
    }
    return [1, toHS(arr[elem]), new T(function() {return arr2lst(arr,elem+1);})]
}

function lst2arr(xs) {
    var arr = [];
    for(; xs[0]; xs = E(xs[2])) {
        arr.push(E(xs[1]));
    }
    return arr;
}

FFI

Static:

foreign import ccall "alert" alert2 :: JSString -> IO ()

Dynamic:

alert3 :: JSString -> IO ()
alert3 = ffi "(function(x) { alert(x); })"

FFI Example

module Main
( main
) where

import Haste
import Haste.Prim
import Haste.Foreign

foreign import ccall "alert" alert2 :: JSString -> IO ()

alert3 :: JSString -> IO Int
alert3 = ffi "(function (x) { alert(x); return 0; })"

foreign import ccall "setTimeout" timeout :: JSFun (IO ()) -> Int -> IO ()

main :: IO ()
main = do
    alert2 $ toJSStr "hello bayhac 2"
    alert3 $ toJSStr "hello bayhac 3"
    flip timeout 2000 . mkCallback . alert2 . toJSStr $ "hello bayhac 4"

Example

Part 2: Portable Libraries with Haste

Outline

  1. Example
  2. Sharing code with native builds
  3. Exporting an API

Example Application

  • A very simple encryption library.

  • A very simple web email encryption application.

Code:

git clone https://github.com/alephcloud/bayhac2014

Native build

cabal install --enable-tests
cabal test

Result:

bayhac2014-cryptmail

Haskell Library API

newtype Password = Password { unPassword  B.ByteString }
    deriving (Eq, Code64)

newtype CipherText = CipherText { unCipherText  B.ByteString }
    deriving (Eq, Code64)

newtype PlainText = PlainText { unPlainText  B.ByteString }
    deriving (Eq, Code64)

encryptWithPwd  Password  PlainText  IO CipherText
decryptWithPwd  Password  CipherText  Either String PlainText

Service API

class (FromJSON α, ToJSON (Response α))  Request α where
    type Response α  *
    answerRequest  α  IO (Either String (Response α))

data EncryptWithPwd = EncryptWithPwd
    { ePassword  !Password
    , ePlainText  !PlainText
    }

instance FromJSON EncryptWithPwd where
    parseJSON = withObject "EncryptWithPwd" $ \o  EncryptWithPwd
        <$> o .: "password"
        <*> o .: "plain_text"

data EncryptWithPwdR = EncryptWithPwdR
    { erCipherText  !CipherText
    }

instance ToJSON EncryptWithPwdR where
    toJSON EncryptWithPwdR{..} = object
        [ "cipher_text" .= erCipherText
        ]

instance Request EncryptWithPwd where
    type Response EncryptWithPwd = EncryptWithPwdR
    answerRequest EncryptWithPwd{..} =
        RightEncryptWithPwdR <$> encryptWithPwd ePassword ePlainText

Haste build

rm -rf dist
haste-inst install

well ...

rm -rf dist
mv cabal.sandbox.config cabal.sandbox.config.disabled
haste-inst install -fhaste --dependencies-only
haste-inst configure -fhaste
haste-inst build

bayhac2014-cryptmail.cabal:

...
ghc-options: -Wall --start=asap --with-js=lib/sjcl.js,lib/bayhac2014-cryptmail.js
...

Result: bayhac2014-cryptmail-app

Javascript API

var logg = function(str) { console.log(str); return; }
var api = new API();

var req = { "password" : btoa(pwd), "plain_text" : btoa(msg) };
api.encrypt_with_password(req, logg, logg, function (r) {
    console.log(r.cipher_text);
}

var req = { "password" : btoa(pwd), "cipher_text" : msg };
api.decrypt_with_password(, logg, logg, function (r) {
   console.log(r.plain_text);
}

The Javascript API exposes the service API

  • Supports usage of web-workers
  • Switching between server and client API
  • Easy compatibility testing

Sharing Code with Native Builds

Examples:

  • Aeson instances
  • Cryptographic protocols
  • Special arithmetic
  • Business logic

Low-level dependencies:

  • Javascript big integers
  • ByteStrings
  • Text
  • Cryptographic primitives

Dispatch Options:

  • Package level abstraction
  • Module level abstraction
  • Code level abstraction with CPP

Sharing Code with Native Builds

Flag haste
    description: build with haste-compiler
    default: False

Library
    exposed-modules:
        BayHac2014.Cryptmail.Text
        BayHac2014.Cryptmail.ByteString
        BayHac2014.Cryptmail.PasswordEncryption
        BayHac2014.Cryptmail.ServiceApi
        BayHac2014.Cryptmail.Json
    if flag(haste)
        build-depends: ...
    else
        build-depends: ...

Executable cryptmail-client
    if ! flag(haste)
        buildable: False

Executable cryptmail-server
    if flag(haste)
        buildable: False

Abstraction of Low-Level Modules

newtype ByteString = ByteString JSAny

foreign import ccall "bsEmpty" j_bytesEmpty ∷ IO ByteString
foreign import ccall "bsConcat" j_bytesConcat ∷ ByteStringByteStringIO ByteString
foreign import ccall "bsEqual" j_bytesEqual ∷ ByteStringByteStringBool
foreign import ccall "bsLength" j_bytesLength ∷ ByteStringInt

instance Monoid ByteString where
    mempty = unsafePerformIO $ j_bytesEmpty
    mappend a b = unsafePerformIO $ j_bytesConcat a b

    {-# INLINE mempty #-}
    {-# INLINE mappend #-}

instance Eq ByteString where
    (==) a b = j_bytesEqual a b

    {-# INLINE (==) #-}

length  ByteString  Int
length = j_bytesLength

Export of Javascript API

main  IO ()
main = do
    register "encrypt_with_password" (asyncRequest  ApiMethod EncryptWithPwd)
    register "decrypt_with_password" (asyncRequest  ApiMethod DecryptWithPwd)

-- -------------------------------------------------------------------------- --

type ApiMethod α
    = α                    -- ^ argument
     (JSString  IO ())   -- ^ log callback
     (JSString  IO ())   -- ^ failure callback
     (Response α  IO ()) -- ^ success callback
     IO ()

register  (FromJSON α, ToJSON (Response α))  JSString  ApiMethod α  IO ()

asyncRequest  Request α  ApiMethod α

type FCallback
    = Ptr Value                -- ^ argument
     Ptr (JSString  IO ())   -- ^ log method
     Ptr (JSString  IO ())   -- ^ failure continuation
     Ptr (Ptr Value  IO ())  -- ^ success continuation
     IO ()

foreign import ccall "addApiMethod" js_add_api_method ∷ JSStringJSFun FCallbackIO ()

Export of Javascript API

function API () {
    return this;
}

API.prototype.addMethod = function (name, method) {
    API.prototype[name] = function (arg, log, fail, succ) {
        var successCB = function (hsResult) {
            /* convert result from the Haste JSON representation into a Javascript Object */
        }
        /* parse the argument object into the Haste JSON representation */
        /* call the method callback */
        var mapply = function () { return A(method,[[0,hsArg],[0,log],[0,fail],[0,successCB],0]); };
        setTimeout(mapply,0);
    }
    return 0;
}

function addApiMethod(name, method) {
    API.prototype.addMethod(name, method);
}

function calls(f,s) { f(E(s)[1]); }
function callv(f,v) { f(v[1]); }

Misc Topics

  • JSON serialization and marshaling of JSON Value
  • Calling Javascript callbacks from Haskell
  • Asynchronous callback execution
  • Concurrency
  • Integration with haste-ffi-parser
  • Managing Javascript dependencies for linked libraries
  • QuickCheck testing of Javascript builds
  • Exception handling