Расширение синтаксиса Lua: лямбды
Использованные библиотеки:
Предположим, мы хотим написать функцию, которая выводит наглядное представление значения переменной. Наши лямбды должны делать следующие две строки эквивалентными:
local show = f[[x -> print(inspect(x))]] -- lambdas
local show = function(x) print(inspect(x)) end -- anonymous functions
В целом выглядит не так уж впечатляюще, но лямбды дадут значительный выигрыш в читаемости при написании библиотеки с method chaining:
-- lambdas
local var = {3, 12, 14, 42, 44, 51, "a"}
/ filter[[type(it) == "number"]]
/ map[[it ^ 2 - 1]]
/ filter[[it % 24 == 0]]
/ map[[it .. " probably can be prime"]]
-- anonymous functions
local var = {3, 12, 14, 42, 44, 51, "a"}
/ filter(function(it) return type(it) == "number" end)
/ map(function(it) return it ^ 2 - 1 end)
/ filter(function(it) return it % 24 == 0 end)
/ map(function(it) return it .. " probably can be prime")
Поскольку вызов filter[[text]] это просто альтернативная форма записи для filter («text»), с помощью простой конкатенации строк внутри filter и map мы сможем научить их работать с котлин-лямбдами.
Приступим к разработке. В целом функция создания лямбды f должна производить примитивную подстановку:
-- f[[ -> ]] ===> function() return end
local function f(text)
local a, b = text:find(" %-> ")
local args = text:sub(0, a - 1)
local result = text:sub(b + 1)
local function_text = "function(%s) return %s end" % {args, result}
local loading_function = load("return " .. function_text)
if loading_function == nil then -- вывод ошибки если не получилось загрузить
error("Incorrect lambda " .. inspect(function_text))
end
return loading_function()
end
Всю работу производит функция load, которая динамически интерпретирует строку с кодом. Она возвращает функцию без аргументов, подгружающую содержание строки, либо nil если что-то пошло не так.
local show = f[[x -> print(inspect(x))]]
show{message = "Hello, world!"}
--[[ ВЫВОД:
{
message = "Hello, world!
}
]]
Функция show сработает, если вы импортировали модуль inspect в глобальную переменную, и выдаст ошибку, если в локальную. Дело в том, что load использует окружение функции f, а модуль inspect существует в окружении на уровень выше. Для решения этой проблемы можно написать функцию, которая добавляет в текущее окружение локальные переменные на уровень выше.
local function push_environment(env, delta)
local i = 1
while true do
local name, value = debug.getlocal(3 + (delta or 0), i)
if not name then return end
env[name] = value
i = i + 1
end
end
debug.getlocal (n, m) позволяет получить доступ к локальной переменной с порядковым номером m на n уровней выше по стеку. n = 1 соответствует push_environment, n = 2 — f, n = 3 — функции, внутри которой f вызвана, что нам и нужно. Аргумент delta добавлен на всякий случай для расширяемости.
Добавляем push_environment (_ENV) в начало функции f и тадам! — всё работает.
Если вы хотите реализовать method chaining как в начале статьи, то это может выглядеть примерно следующим образом:
fnl.filter = fnl.pipe() .. function(self, predicate)
if type(predicate) == "string" then
predicate = f("ix, it -> " .. predicate)
end
local result = {}
for i, v in ipairs(self) do
if predicate(i, v) then
table.insert(result, v)
end
end
return result
end
В данном случае fnl.pipe () инкапсулирует перегрузку операторов, необходимую для method chaining.
Стоит заметить, что использование функций таблицы debug и интерпретация lua-кода в рантайме замедляют скорость выполнения кода, поэтому я рекомендую пользоваться этим решением с осторожностью.