Haskell
💖
Lua

Come together over FFI

Albert Krewinkel

What’s this about?

whoami

  • Scientific publishing
  • Pandoc contrib
  • HsLua

Language overview

Haskell

  • static typing
  • compiled
  • lazy evaluation

Lua

  • dynamic typing
  • interpreted
  • strict evaluation

Use-cases

Haskell

  • web gateway

    (PostgREST, Hasura)

  • window manager

    (XMonad)

Lua

  • web gateway

    (Kong)

  • window manager

    (Awesome)

Use-cases

Haskell

Complex systems

Lua

Scriptable systems

Example: pandoc

data Inline
  = Str Text           -- ^ Text (string)
  | Emph [Inline]      -- ^ Emphasized text
  | SmallCaps [Inline] -- ^ Small caps text
  -- ⋮
Markdown Pandoc AST
*Hello* Emph [Str "Hello"]
_Hello_ Emph [Str "Hello"]
_*Hello*_ Emph [Emph [Str "Hello"]]
? SmallCaps [Str "Hello"]]

Pandoc Lua filter

function Emph (em)
  local nested = em.content[1]
  if nested and nested.t == 'Emph' then
    return pandoc.SmallCaps(nested.content)
  end
end

In action

% echo '_*Hello*_' | pandoc --to=latex
⇒ \emph{\emph{Hello}}

% echo '_*Hello*_' | pandoc --to=latex --lua-filter=sc.lua
⇒ \textsc{Hello}

Language overview

Haskell Lua
typing static dynamic
programs compiled interpreted
FP
GC
C API

FFI

Foreign Function Interface

  • Connects programs written in different languages
  • Must support the relevant types
  • Builds bridges from the runtime system.

Function imports

C header

void (lua_pushboolean) (lua_State *L, int b);

Import

foreign import capi "lua.h lua_pushboolean"
  lua_pushboolean :: Ptr () -> CInt -> IO ()

Types

Simple in C

int (lua_setiuservalue) (lua_State *L, int idx, int n);

Expressive in Haskell

newtype LuaBool    = LuaBool CInt    deriving (Storable)
newtype StackIndex = StackIndex CInt deriving (Storable, Num)

foreign import capi "lua.h lua_setiuservalue"
  lua_setiuservalue :: Lua.State -> StackIndex -> CInt
                    -> IO LuaBool

package: lua

Basic bindings to Lua

-- | Retrieve content of field \"age\" from the registry.
getAge :: Lua.State -> IO Lua.Integer
getAge l = do
  withCString "age" $
    lua_getfield l LUA_REGISTRYINDEX
  result <- lua_tointegerx l (-1) nullPtr
  lua_pop l 1
  pure result

hslua: Familiar Haskell feeling

-- | Get name field from registry table.
getAge :: Lua (Maybe Integer)
getAge = do
  getfield registryindex "age"
  tointeger top <* pop 1

Reader monad

Lua state as first argument

lua_getfield    :: State -> ...
lua_pushboolean :: State -> ...
newtype Lua a = Lua { unLua :: ReaderT State IO a }
  deriving (Monad, MonadIO, MonadReader State)

main = run $ do
  age <- getAge
  ...

Data

Stack

        ,----------.
        |  arg 3   |
        +----------+
        |  arg 2   |
        +----------+
        |  arg 1   |
        +----------+                  ,----------.
        | function |    call 3 1      | result 1 |
        +----------+   ===========>   +----------+
        |          |                  |          |
        |  stack   |                  |  stack   |
        |          |                  |          |

Marshal

       <----???--- Emph [Str "a"] :: Inline
,----------.
|     5    |
+----------+
|   true   |
+----------+
| "banana" |

Lua tables

pushInline x = do
  newtable
  case x of
    Str txt -> pushText txt *> setfield (nth 2) "text"
    Emph xs -> pushList pushInline xs *>
               setfield (nth 2) "content"
    -- ...

Str "a" becomes

x = {text = 'a'}
print(x.text) -- prints: a

Metatables

Metatables define object behavior.

local mt = {
  __add   = function (a, b) return {a, b} end,
  __index = function (t, k) return 42 end,
  __gc    = function () print('collected') end,
}

local x = setmetatable({1}, mt)
local nope  = {1} + 1 -- ⇒ ERROR!
local tuple = x + 2   -- tuple == {x, 2}
print(x[1], x.hello)  -- prints: 1, 42

Table values

  • Straight-forward
  • Simple
  • Strict

Userdata

  • Wrapper for arbitrary data.
  • Metatables define behavior.
  • Frequently used with pointers.
  • But pointers don’t work well with GC.
,---------.
|  ??? ---|----> data
`---------'

Stable Pointer

  • Create stable pointer with Foreign.StablePtr
  • Not a pointer in the C sense
xptr <- newStablePtr x
-- ...
x' <- deRefStablePtr xptr
freeStablePtr xptr

Pointers in userdata

,------------.
| StablePtr -|----> Haskell value
`------------'
  xPtr <- newStablePtr x
  udPtr <- lua_newuserdata l (fromIntegral $ sizeOf xPtr)
  poke (castPtr udPtr) xPtr
  -- ??? freeStablePtr ???

Free pointer

Set __gc metamethod on userdata.

#include <HsFFI.h>

static int hslua_userdata_gc(lua_State *L) {
  HsStablePtr *userdata = lua_touserdata(L, 1);
  if (userdata)
    hs_free_stable_ptr(*userdata);
  return 0;
}

In action

Types

-- | A table row.
data Row = Row Attr [Cell]

typeRow = deftype "Row" []
  [ property "cells" "row cells"
      (pushList pushCell, \(Row _ cells) -> cells)
      (peekList peekCell, \(Row attr _) cells ->
                             Row attr cells)
  ]
-- print first cell in row
print(row.cells[1])

Functions

mkRow = defun "Row"
  ### liftPure2 Row -- lift a pure Haskell function to Lua
  <#> udparam typeAttr "attr" "cell attributes"
  <#> parameter (peekList peekCell) "{Cell,...}"
        "cells" "row cells"
  =#> udResult typeRow "new Row object"

registerDocumentedFunction mkRow

In Lua:

empty_row = Row(Attr(), {})

Tests

return {
  group 'examples'  {
    test('multiplication', function()
      assert.are_equal(6, 2 * 3)
    end),
    test('empty var is nil', function ()
      assert.is_nil(var)
    end)
  }
}

Tasty integration

main = do
  luaTest <- withCurrentDirectory "test" . run $ do
    translateResultsFromFile "example-tests.lua"
  defaultMain . testGroup "Haskell and Lua tests" $
    [ luaTest {- more tasty tests go here -} ]
  test/example-tests.lua
    constructor
      has type `userdata`:       OK
      accepts list of integers:  OK
    comparison
      equality:                  OK

Property tests

    test('property test with integers',
      forall(
        arbitrary.integer,
        function (i)
          return type(i) == 'number' and math.floor(i) == i
        end
      )
    ),

🧑‍💻🏢🧑‍💼

pandoc

Pandoc-related projects written in Lua on GitHub

quarto

Scientific and technical publishing system

End

Thanks

HsLua
hslua.org
Code
github.com/hslua/hslua
Languages
lua.org, haskell.org
pandoc Lua filters
pandoc.org/lua-filters
Quarto
quarto.org

Appendix

Safety

  • Callbacks into Haskell are allowed.
  • Requires some runtime investment.
  • Can be avoided by using unsafe.
-- Improves performance by a third
--                  vvvvvv
foreign import capi unsafe "lua.h lua_pushnumber"
  lua_pushnumber :: Lua.State -> Lua.Number -> IO ()

Speed

https://github.com/dyu/ffi-overhead

C Types

Free stable pointer

setjmp & longjmp