Haskell + Lua = дружба!
date = fromGregorian 2015 oct 21
category = "Пакеты"
tags = ["Haskell", "Lua", "scripting"]
Обратите внимание на то, что эта статья является исходником программы - она написана на Literate Haskell! Запустить эту программу можно с помощью stack командой:
stack --resolver lts-3.2 runghc --package hslua --package bytestring имя_файла.lhs
Использование Lua-скриптов в программе на Haskell
Иногда бывает очень полезно иметь возможность расширения/изменения поведения программы без её перекомпиляции. Для решения этой задачи часто используют скриптовые языки, встраивая их интерпретаторы прямо в основную программу. Один из наиболее популярных встраиваемых скриптовых языков, это Lua. Это динамически типизированный процедурный язык программирования, очень экономно расходующий ресурсы процессора и памяти. Эта статья не рассматривает сам Lua, а демонстрирует лишь типичный сценарий его использования, поэтому тем, кто желает познакомиться с этим языком поближе, автор рекомендует почитать официальное руководство по оному.
Итак, приступим. Начнем, как обычно, с импортов:
import Control.Monad (when, void)
import qualified Scripting.Lua as Lua
import qualified Data.ByteString.Char8 as BSS
“Скриптом” нам послужит такая строка:
script :: String
= unlines
script "function pine(height) "
[ " for i = 1, height do "
, " write( replicate(height - i, \" \")) "
, " writeLn(replicate(1 + (i - 1) * 2, \"*\")) "
, " end "
, "end " ] ,
Автору этот код видится достаточно очевидным для любого читателя,
знакомого с каким-либо процедурным языком. Делает же функция pine
следующее: при вызове для некоего числа, скажем, 5
, она всего лишь
выводит на экран (печатает в stdout
) “ёлочку” с высотой, равной этому
числу:
*
***
*****
*******
*********
Заметьте, что скрипт не обязан быть строкой, более того, чаще всего скрипты расширения именно из файлов и читаются. Это, собственно, и позволяет модифицировать их после того, как основная программа скомпилирована. В данном же случае скрипт хранится в строке только лишь для того, чтобы literate-исходник оставался самодостаточным.
Теперь напишем основную программу:
main :: IO ()
= do main
Для начала инициализируем интерпретатор и загрузим стандартные библиотеки:
<- Lua.newstate
lua Lua.openlibs lua
Теперь стоит снова обратить свой взор на код скрипта, а точнее, на процедуры
write
и writeLn
и функцию replicate
. Дело в том, что это не встроенные
в язык элементы - функция и процедуры инжектируются в пространство имен скрипта
нашей основной программой! Т.е. мы способны не только вызывать скрипт снаружи,
мы можем расширять “лексикон” самого скриптового языка!
Регистрируются функции довольно просто:
"write" $ putStr . BSS.unpack
Lua.registerhsfunction lua "writeLn" $ putStrLn . BSS.unpack
Lua.registerhsfunction lua "replicate" $ \n s ->
Lua.registerhsfunction lua return $ BSS.concat $ replicate n s) :: IO BSS.ByteString (
Последняя функция выглядит чуть сложнее первых двух, но так же довольно проста.
Регистрируемые функции должны принимать аргументы нескольких простых типов,
таких как числа (Int
) и байтовые строки (ByteString
), и возвращать столь
же простые значения, но уже в контексте IO
. Так, первые две
зарегистрированные функции - настоящие процедуры, возвращающие IO()
,
т.е. “ничего”.
Окружение готово, теперь нужно загрузить скрипт:
<- Lua.loadstring lua script "script" res
Строка "script"
здесь - “имя” скрипта, которое интерпретатор использует
в сообщениях об ошибках. Из файла скрипт бы загружался так:
-- res <- Lua.loadfile lua "replua.lua"
Скрипт может и не загрузиться и это стоит проверить:
/= 0) $ error "Can't load script!" when (res
Загруженный скрипт можно выполнить (с начала первой строки, 0 0
):
0 0 Lua.call lua
Скрипт загружен, можно что-нибудь из него повызывать:
$ Lua.callproc lua "pine" (10 :: Int) void
И, конечно же, корректно завершить работу с интерпретатором:
Lua.close lua
Вот и всё! Тем Lua и хорош (не только этим, конечно же), что легко встраивается, в т.ч. и в Haskell :)