layout | author | title | date | categories |
---|---|---|---|---|
default |
Albert Krewinkel |
Santa's Little Lua Scripts |
2020-12-06 |
example |
Santa sighted deeply as worry and uncertainty gave way, leaving a feeling of relieve and accomplishment. The year was one of the worst he'd seen so far. Large numbers of his helpers were moving from the North Pole to Antarctica to satisfy their ambient temperature preferences. There would be many telecommuting Elves this year, and each helper enjoyed additional autonomy. Tying everything together was a challenge. But he had succeeded: the wishes processing program was finished, and the elves would be able to help Santa from the comfort of their new homes.
The part of the wishes system that Santa had been working on was focused on classic toys: wooden bricks, dolls, and train sets.
data Toy = Bricks | TrainSet | Doll deriving Show
The system also kept track of basic data about the children:
data Behavior = Nice | Naughty deriving (Eq, Show)
data Child = Child
{ childName :: Text
, childBehavior :: Behavior
} deriving (Show)
Children and toys were tied together in a wish.
data Wish = Wish
{ wishingChild :: Child
, wishedToy :: Toy
} deriving (Show)
It was most elegant. The problem for Santa was that the Elves, being independent and autonomous workers, needed to access and process the data in very custom ways. Unfortunately for him, very few Elves had a Haskell build environment installed, so he had to distribute the binary. Writing a completely custom processing language seemed like an enormous rabbit hole.
Fortunately, Santa had a better idea: Lua, an embeddable scripting language. He had been using it for some projects1 and also made use of it in pandoc, which he used to answer his mails. Santa would just need to expose the relevant parts of the Haskell system, so the Elves could access and script it as their hearts desired. He looked for a library, found HsLua, and got to work.
Lua has a simple, yet powerful, stack-based API. The first step towards exposing Haskell data was to push them to the Lua stack. Keeping things simple, Santa chose strings to represent toys:
pushToy :: Toy -> Lua ()
pushToy = pushString . show
Lua offers only a single construct to structure data: tables. So that's what Child and Wish were represented with.
pushChild :: Child -> Lua ()
pushChild (Child name behavior) = do
-- create new Lua table on the stack
newtable
-- push string to stack
pushText name
-- table now in position 2; assign string to field in table
setfield (nthFromTop 2) "name"
-- push boolean to stack
pushBool (behavior == Nice)
setfield (nthFromTop 2) "nice"
pushWish :: Wish -> Lua ()
pushWish (Wish child toy) = do
newtable
pushChild child
setfield (nthFromTop 2) "child"
pushToy toy
setfield (nthFromTop 2) "toy"
Santa's goal for now was to allow his Elves to filter the list of wishes so each finds the ones relevant to them. For example, if an Elf only cares about wishes for train sets from children who were nice, then they should be able to use a script to filter those wishes out.
return function (wish)
return wish.child.nice and
wish.toy == 'TrainSet'
end
The script returns a (lambda) function that serves as a predicate
for wishes. The function can be thought of having the type Wish -> IO Bool
. Santa needed to turn the Lua lambda function into an
actual Haskell function runPredicate :: Wish -> Lua Bool
. If
Santa assumed that the lambda function was at the top of the Lua
stack, then he could push a Wish
value to the Lua stack, call
the function, and retrieve the result value from the stack.
runPredicate :: Wish -> Lua Bool
runPredicate wish = do
-- Assume filter function is at the top of the stack;
-- create a copy so we can re-use it.
pushvalue stackTop
pushWish wish
-- Call the function. There is one argument on the stack,
-- and we expect one result to be returned.
call (NumArgs 1) (NumResults 1)
toboolean stackTop <* pop 1
What remained was loading the Elves' script files. Santa did this
with dofile
of type FilePath -> Lua Status
. The predicate
then ends up on the top of the Lua stack, and can be called
through runPredicate
, e.g. to select a subset of wishes via
filterM
.
main :: IO ()
main = do
filterFile <- fmap (!! 0) getArgs -- get first argument
result <- run $ do
_status <- dofile filterFile
filterM runPredicate wishes
print result
Santa tested his creation on a short list of wishes
wishes :: [Wish]
wishes =
[ Wish (Child "Theodor" Nice) Bricks
, Wish (Child "Philine" Nice) TrainSet
, Wish (Child "Steve" Naughty) Doll
]
by running runhaskell wish-filter predicate.lua
. To his
uttermost satisfaction, the terminal echoed the right information
back to him.
[Wish {wishingChild = Child {childName = "Philine", childBehavior = Nice}, wishedToy = TrainSet}]
He reclined in his chair, shut down his device, and enjoyed a double chocolate chip cookie of which he felt very deserving now.
This post was written by Albert Krewinkel. Shoot him a mail or come say hi on Twitter!
Santa's full code, as presented here, is available as part of the examples at https://github.com/hslua/hslua-examples.
If you like reading brief articles on coding, checkout Advent of Haskell (@AdventOfHaskell); Thanks to the great people behind that project, there are, at the time of writing, five other articles with lovely tidbits on programming in Haskell; 18 more to follow!
Also, don't miss the Modernes Publizieren Advent calendar (in German), with many interesting bits and pieces on the use of open resources in academic publishing!
<style> hr { margin: 4em 2em; } </style>