Расширение синтаксиса Lua: лямбды

image-loader.svg

Использованные библиотеки:

Предположим, мы хотим написать функцию, которая выводит наглядное представление значения переменной. Наши лямбды должны делать следующие две строки эквивалентными:

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-кода в рантайме замедляют скорость выполнения кода, поэтому я рекомендую пользоваться этим решением с осторожностью.

© Habrahabr.ru