August 11, 2016

Let's build a little Haskell interpreter that we can use as a safe sandbox on the Web.

We'll use Scotty for our Web binding, so we'll want to enable OverloadedStrings and import some functions:

{-# LANGUAGE OverloadedStrings #-}

import Web.Scotty (get, param, request, scotty, ScottyM, text)
import Data.Text.Lazy (pack)
import Control.Monad.IO.Class (liftIO)
import System.Environment (getEnv)

We'll use hint to interpret and run client code:

import qualified Language.Haskell.Interpreter as I

We need functions to take arbitrary source code as a string, try to evaluate it, and handle compilation errors:

eval :: String -> IO String
eval src = fmap
  (either show id)
  (I.runInterpreter $ I.setImports ["Prelude"] >> I.eval src)

We also need a way to expose it to the Web:

service :: ScottyM ()
service = do
  get "/" $ do
    req <- request
    src <- param "src"
    res <- liftIO $ eval src
    text $ pack res

Finally, we need a way to launch it:

main :: IO ()
main = do
  port <- read <$> getEnv "HASKEVAL_PORT"
  scotty port service

With our main function defined, we're ready to fire it up. We can use Nix to import the hint and scotty packages for us:

$ nix-shell -p 'haskellPackages.ghcWithPackages (pkgs: with pkgs; [hint scotty])'

From our Nix shell, we can use Codedown to extract the Haskell code above, and runhaskell to launch it:

$ curl -sL earldouglas.com/posts/haskeval.md |
  codedown haskell |
  HASKEVAL_PORT=3000 runhaskell
Setting phasers to stun... (port 3000) (ctrl-c to quit)

Let's try it out! Enter a line of Haskell (with the type Show a => a) below, and hit Run.