
Если вы хоть раз встраивали Lua в свой проект — будь то игровой движок, высоконагруженный веб-сервер на OpenResty или конфигуратор сложного сетевого оборудования — вы знаете, за что мы его любим:)
А любим мы его — за компактность, быстроту, встраиваемость и предсказуемость. Не любим — за аскетичный синтаксис, отсутствие привычных конструкций и постоянное «изобретение велосипеда».
Эта статья — обзор диалекта Lattelua: зачем он нужен, чем отличается от других диалектов, и почему его особенно удобно использовать в уже существующих проектах, где Lua — встраиваемый язык.
LatteLua: Кофеиновый апгрейд
Lua считается языком конфигов, описания данных и встраиваемых систем, но я смотрю на него немного по-другому, он больше похож на библиотеку или фреймворк над Си, для его усиления или расширения, как бы странно это ни звучало.
Стековая машина в Lua API — это действительно «низкоуровневый» протокол связи, чем-то напоминает assembler, и такой, на первый взгляд, простой подход на практике дает нехилый boost в интерпретации, что повышает эффективность исполнения.
Но давайте честно: когда логика разрастается, синтаксис Lua начинает напоминать попытку собрать вертолёт с помощью одной отвёртки и такой-то матери. Бесконечные then/end/local, отсутствие нормальных классов и врожденная «немногословность» превращают поддержку кода в квест.
Да и чего уж греха таить, хочется, очень хочется, обложиться всякими синтаксически-сахарными плюшками, к примеру, как в том же Python. И именно из-за этой потребности и появляются MetaLua, Fennel и MoonScript, каждый с копированием в сторону симпатизируемого языка.
Lattelua же, напротив, он как CoffeeScript, другой диалект того же Lua.
Lattelua - это попытка собрать все лучшее из best-practice, взяв за основу парсер MoonScript, своего рода дружелюбный Lua. Но почему не MoonScript в оригинале, зачем изобретать новый диалект если все уже придумано до нас?
Основная претензия к MoonScript — это его «питоноподобность»:
В MoonScript, как и в Python отступы — это закон, малейшая ошибка в отступах (особенно при смешивании пробелов и табуляции) приводит к синтаксическим ошибкам, которые крайне тяжело отловить, так как компилятор может интерпретировать блок кода как часть другой логической ветки. В Lua четкая блочная система, в MoonScript — мы гадаем по пустому пространству.
Сложность работы с анонимными функциями, MoonScript пытается это решить, но вложенные блоки внутри аргументов функций там выглядят как «лесенка», в которой легко запутаться.
API и минификация, код на Python/Moonscript невозможно минифицировать без потери смысла, так как пробелы — это и есть синтаксис. Любое искажение форматирования при пересылке «убивает» программу.
Метапрограммирование и кодогенерация, если нужно генерировать код на лету, то генерировать корректные отступы — это лишняя и сложная головная боль. Намного проще просто выплевывать токены и ставить
endв нужных местах.
Да чё я всё это перечисляю, все кому надо уже и так все знают. Я не хочу сказать что python-style — это плохо, плохая идея модифицировать в него язык с блочной разметкой. Куда проще сократить синтаксическую неуклюжесть (then/end, do/end, function/end) до блочного стиля, повысив «многословность» новыми синтаксическими конструкциями.
Синтаксис: меньше шума, больше смысла
Lattelua Language Reference
Базовый синтаксис
Блоки кода и разделители
В Lattelua отступы не имеют значения. Группировка выражений происходит с помощью { и }. Символ ; используется как разделитель инструкций, что позволяет писать код в одну строку:
-- Обычная запись if true { print("Hello") } -- Однострочная запись if true { print("Hello") }
Комментарии
Комментарии игнорируются компилятором. Символ ; внутри комментариев и строк не обрабатывается препроцессором:
-- Это однострочный комментарий --[[ Это многострочный комментарий. Он работает точно так же, как в Lua. В MoonScript такой тип комментариев не поддерживается! --]]
Переменные и присваивание
По умолчанию все переменные являются локальными (local):
a = 1 -- local a = 1 str = "hello" -- local str = "hello" x, y = 10, 20 -- local x, y = 10, 20
Обновление значений
Доступны операторы быстрого обновления значений: +=, -=, *=, /=, %=, ..=:
count = 0 count += 1 -- count = count + 1 name = "Lattelua" name ..= " Lang" -- Конкатенация
Глобальные переменные
Чтобы создать глобальную переменную или экспортировать её из модуля, используется ключевое слово export:
export VERSION = "1.0"
Это особенно полезно при объявлении того, что будет видно извне в модуле:
-- some module.llua export some_print add = (x, y) -> { x + y } some_print = (x, y) -> {print "Addition is: ", add x, y} -- some script.llua require "some_module" some_module.some_print 5, 10 -- 15 print some_module.add 5, 10 -- errors, `add` not visible
Экспорт не будет иметь эффекта, если в области видимости уже есть локальная переменная с таким же именем.
В контексте переменных, часто требуется перенести некоторые значения из таблицы/модуля в текущую область как локальные переменные по их имени.
Для этого используется ключевое слово import:
import insert from table -- local insert = table.insert
Можно указать несколько имен, каждое через запятую:
import C, Ct, Cmt from lpeg -- local C, Ct, Cmt = lpeg.C, lpeg.Ct, lpeg.Cmt
Иногда требуется, чтобы таблица была передана в качестве self-аргумента. Для сокращения можно добавить префикс :: к имени, чтобы связать функцию с этой таблицей:
t = { val: 100 add: (value) => { self.val + value } } import ::add from t print add 22 -- equivalent to add(t, 22) or t::add(22)
Типы данных и таблицы
Литералы
num = 123 float = 1.5 str_double = "Text" str_single = 'Text' str_multi = [[ multi line text ]] bool = true nothing = nil
Строковая интерполяция
Допускается смешивать выражения со строковыми литералами, используя #{} синтаксис:
print "This is #{math.random() * 100}% work, I'm sure" -- print("This is " .. tostring(math.random() * 100) .. "% work, I'm sure")
Интерполяция строк доступна только в строках, заключенных в двойные кавычки.
Таблицы
Как и в Lua, таблицы заключаются в фигурные скобки:
array = { 1, 2, 3, 4 }
В отличие от Lua, присвоение значения ключу в таблице выполняется с помощью : (вместо =):
config = { port: 8080, host: "localhost", list: { 1, 2, 3 }, ["key with spaces"]: "some value" }
Перевод строки можно использовать для разделения значений вместо запятой (или и то, и другое):
config = { port: 8080 host: "localhost" list: { 1, 2, 3 } ["key with spaces"]: "some value" }
Ключи таблицы могут быть ключевыми словами языка без экранирования:
t = { do: "do" end: "end" }
Если создается таблица из переменных и требуется, чтобы ключи совпадали с именами переменных, можно использовать префиксный оператор ::
gender = "male" age = 25 person = { :gender -- gender: gender :age -- age: age key: "value" -- key: "value" } print :gender, :age -- {gender: gender, age: age}
Если требуется, чтобы ключ был результатом выражения, можно обернуть его в [], как и в Lua. Также возможно использовать строковый литерал непосредственно в качестве ключа, исключая квадратные скобки. Это полезно, если ключ содержит специальные символы:
t = { [1 + 2]: "three", ["some value"]: true, "another some value": false }
Деструктуризация
Деструктуризация - это способ быстрого извлечения значений из таблицы по их имени или положению в таблицах на основе массива.
vec = { x: 10, y: 20, z: 30 } { :x, :y } = vec print(x, y) -- 10 20 arr = {1, 2, 3} {f,_,t} = arr print f, t -- 1 3
Это также работает с вложенными структурами данных:
obj = { array: {1, 2, 3, 4} properties: { align: "center" vec: { x: 10, y: 20, z: 30 } } } { array: { first, second } properties: { :align vec: { :x, :y } } } = obj -- first, second, align, x, y = obj.array[1], obj.array[2], obj.properties.align, obj.properties.vec.x, obj.properties.vec.y
Обычно значения из таблицы извлекаются и присваиваются локальным переменным, имеющим то же имя, что и ключ. Чтобы избежать повторения, возможно использовать префиксный оператор ::
{:concat, :insert} = table -- local concat, insert = table.concat, table.insert
По сути, это то же самое, что и import, но мы можем переименовать поля, которые хотим извлечь:
{:mix, :max, random: rand } = math -- local mix, max, rand = math.mix, math.max, math.random
Деструктуризация также может проявляться в тех местах, где неявно выполняется присваивание:
array = { {1, 2, 3, 4} {5, 6, 7, 8} } for {first, second} in *array { print first, second -- 1 2 & 5 6 }
Генераторы коллекций
Генераторы предоставляют удобный синтаксис для создания новой таблицы путем итерации по некоторому существующему объекту и применения выражения к его значениям.
Существует два типа генераторов: генератор списка и генератор таблицы.
Они оба создают таблицы Lua.
Генераторы списков накапливают значения в таблицу, подобную массиву, а генераторы таблиц позволяют устанавливать как ключ, так и значение на каждой итерации.
Генераторы списков
Следующий пример создаёт копию таблицы элементов, с удвоенными значениями:
array = { 1, 2, 3, 4 } doubled = [item * 2 for i, item in ipairs array] -- doubled = { 2, 4, 6, 8 }
Элементы, включенные в новую таблицу, можно ограничить с помощью when выражения:
iter = ipairs array slice = [item for i, item in iter when i > 1 and i < 3] -- slice = { 2 }
Операторы for и when возможно объединять в цепочки сколько угодно. Единственное требование, чтобы в выражении был хотя бы один оператор for.
Использование нескольких операторов for аналогично использованию вложенных циклов:
x = {4, 5, 6, 7} y = {9, 2, 3} points = [{x,y} for x in ipairs x for y in ipairs y]
Генераторы таблиц
Синтаксис генератора таблиц очень похож, отличается только использованием {} и получением двух значений на каждой итерации:
t ={ gender: "male", age: 25 } copy = {k,v for k,v in pairs t}
Генераторы таблиц, как и генераторы списков, также поддерживают несколько операторов for и when:
copy = {k,v for k,v in pairs t when k != "gender"}
Управляющие конструкции
If/Else/Unless
if x > 10 { print("Big") } elseif x == 10 { print("Equal") } else { print("Small") } -- Unless (если НЕ) unless ready { init() } -- Тернарный оператор / Однострочный if val = if check { true; } else { false; }
Условные выражения также можно использовать в операторах возврата и присваиваниях:
test = (c)->{ if c { true } else { false } } out = if test true { "is true" } else { "is false" } print out -- "is true"
Оператор Switch
Использует ключевое слово case для веток и else для значения по умолчанию:
value = 2 switch value { case 1 print("One") case 2 print("Two") case 1,2,3 print "One..Three" else print("Other") }
switch также можно использовать в качестве выражения, тем самым присвоить результат switch переменной:
out = switch value { case 1 "One" case 2 "Two" case 1,2,3 "One..Three" else "Other" } print out -- "Two"
Циклы
For (Числовой)
-- Без шага for i = 1, 10 { print(i) -- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } -- С шагом for i = 0, 10, 2 { print(i) -- 0, 2, 4, 6, 8, 10 }
For In (Итератор)
t = { a: 1, b: 2 } for k, v in pairs(t) { print(k, v) }
Цикл for также можно использовать в качестве выражения. Последний оператор тела цикла преобразуется в выражение и добавляется в таблицу накопительного массива.
Удвоение каждого четного числа:
doubled = for i=1,20 { if i % 2 == 0 { i * 2 } else { i } } print i for _,i in ipairs doubled -- 4, 8, 12, 16, 20, 24, 28, 32, 36, 40
Также возможно фильтровать значения, комбинируя выражения цикла for с оператором continue.
Циклы for в конце тела функции не накапливаются в таблице для возвращаемого значения (вместо этого функция вернет nil).
Возможно использовать явный оператор возврата, либо цикл можно преобразовать в генератор списка.
funca = -> {for i=1,10 {i}} funcb = -> {return [i for i=1,10] } print funca() -- prints nil print funcb() -- prints table object
Это сделано для того, чтобы избежать ненужного создания таблиц для функций, которым не нужно возвращать результаты цикла.
While
Цикл while также существует в двух вариантах:
i = 10 while i > 0 { print i i -= 3 } while running == true {some_func()}
Как и в случае с for, в цикле while также можно использовать выражение. Кроме того, чтобы функция возвращала накопленное значение цикла while, оператор должен быть возвращен явно.
Управление циклом
Break
Оператор break прерывает цикл (while или for), в теле которого встречается. В результате выполнения оператора break управление передаётся первой инструкции, следующей непосредственно за оператором цикла.
i = 0 while i < 10 { break if i > 5 print i i += 1 }
Continue
Оператор continue можно использовать для пропуска текущей итерации в цикле.
i = 0 while i < 10 { continue if i % 2 == 0 print i i += 1 }
Также continue можно использовать с выражениями цикла, чтобы предотвратить накопление этой итерации в результате.
В этом примере массив фильтруется только по четным числам:
array = {1,2,3,4,5,6} odds = for x in ipairs array { continue if x % 2 == 1 x }
Функции
Все функции создаются с использованием функционального выражения. Простая функция обозначается стрелкой: ->
some_func = -> some_func() -- call that empty function
Тело функции может представлять собой либо один оператор, либо серию, помещенных непосредственно в блок фигурных скобок, сразу после стрелки:
funca = -> {print "hello world"} funcb = -> { message = "world" print "hello #{message}" }
Если функция не имеет аргументов, ее можно вызвать с помощью оператора !, вместо пустых круглых скобок. ! вызов - предпочтительный способ вызова функций без аргументов.
funca! funcb()
Функции с аргументами можно создать, указав перед стрелкой список имен аргументов в круглых скобках:
sum = (a, b) -> { return a + b }
Для аргументов функции можно указать значения по умолчанию. Аргумент считается пустым, если его значение равно нулю. Любые нулевые аргументы, имеющие значение по умолчанию, будут заменены перед запуском тела функции.
greet = (name = "World") -> { print("Hello " .. name) }
Значения аргументов по умолчанию вычисляется в теле функции в порядке объявления аргументов. Именно по этой причине значения по умолчанию имеют доступ к ранее объявленным аргументам.
(x=100, y=x+1000) -> { print x + y }
Функции можно вызывать, перечисляя аргументы после имени выражения, результатом которого является функция. При объединении вызовов функций аргументы применяются к ближайшей функции слева.
sum 10, 20 -- sum(10, 20) print sum 10, 20 -- print(sum(10, 20)) a b c "a", "b", "c" -- a(b(c("a", "b", "c")))
Чтобы избежать двусмысленности при вызове функций, аргументы также можно заключать в круглые скобки. Это необходимо в примере ниже, чтобы гарантировать, что правильные аргументы будут отправлены в правильные функции.
print "x:", sum(10, 20), "y:", sum(30, 40) -- print("x:", sum(10, 20), "y:", sum(30, 40))
Между открывающей скобкой и функцией(sum) не должно быть пробела.
Как и в Lua, функции могут возвращать несколько значений. Последний оператор должен представлять собой список значений, разделенных запятыми:
some_func = (x, y) -> {x + y, x - y} a, b = some_func 10, 20
Self-контекст
Для создания функций предусмотрен специальный синтаксис =>, который автоматически включает аргумент self.
obj = { val: 10 update: (num) => { self.val = num -- self передается автоматически } } obj::update(13) print obj.val -- val = 13
Линейные декораторы
Для удобства операторы цикла for и if можно применять к отдельным операторам в конце строки:
print "hello world" if 1 == 1
И с базовыми циклами:
print "value: #{v}" for _, v in ipairs {1,2,3,4,5,6}
Объектно-ориентированное программирование
Классы
Класс объявляется с помощью оператора class, за которым следует табличное объявление, в котором перечислены все методы и свойства.
class Animal { new: (name) => { self.name = name } speak: => { print(self.name) } }
Объявление класса также можно использовать как выражение, которое можно присвоить переменной или вернуть явно.
Метод new, если определён, становится конструктором.
Создание экземпляра класса осуществляется путем вызова имени класса в качестве функции.
dog = Animal "woof woof"
Все свойства класса являются общими для всех экземпляров. Это нормально для методов, но для других типов объектов могут возникнуть нежелательные результаты:
class Animal { speech: {} new: (speech) => { table.insert self.speech, speech } speak: (who) => { print "#{who} say: #{speech}" for _, speech in ipairs self.speech } } dog = Animal "woof" cat = Animal "meow" dog::speak("dog") -- will print both `woof` and `meow` cat::speak("dog") -- will print both `woof` and `meow`
свойство speech является общим для всех экземпляров, поэтому изменения, внесенные в него в одном экземпляре, будут отображаться в другом.
Правильный способ избежать такого поведения - создать изменяемое состояние объекта в конструкторе:
class Animal { new: (speech) => { self.speech = {} -- private property for instance table.insert self.speech, speech } speak: (who) => { print "#{who} say: #{speech}" for _, speech in ipairs self.speech } }
Наследование
Ключевое слово extends можно использовать в объявлении класса для наследования свойств и методов другого класса.
class Dog extends Animal { new: (speech) => { super(speech) } speak: (who)=> { print("#{who} say: WOOF") } }
Если в подклассе не определен конструктор, то при создании нового экземпляра вызывается конструктор родительского класса.
Если же конструктор определен, то для вызова конструктора родительского класса можно использовать метод super.
super - это специальное ключевое слово, которое можно использовать двумя способами: как объект или как функцию. super обладает функциональностью только внутри класса.
При вызове в качестве функции super вызовет функцию с тем же именем в родительском классе. В качестве первого аргумента автоматически будет передан текущий объект self.
При использовании super в качестве обычного значения, это ссылка на объект родительского класса.
К super можно обращаться как к любому объекту для получения значений в родительском классе.
При наследовании классом наследника, он отправляет сообщение родительскому классу, вызывая метод __inherited родительского класса, если он существует. Метод принимает два аргумента: наследуемый класс и дочерний класс:
class Animal { __inherited: (child) => { print "#{self.__name} was inherited by #{child.__name}" } } class Dog extends Animal{}
With оператор
Блок with позволяет сократить код при множественных обращениях к одному объекту. Внутри блока, свойства начинающиеся с . или методы с ::, относятся к целевому объекту.
user = { name: "John", age: 30 } user.show = => { print self.name } with user { .name ..= " Doe" -- user.name = "John Doe" ::show() -- user:show() print(.age) -- print(user.age) }
Оператор with также можно использовать как выражение, возвращающее значение, к которому он предоставил доступ:
name = with user { .name = 'Jane Smith' } name::show() -- Jane Smith
Выражение в операторе with также может быть присвоением, если требуется дать выражению имя:
name = with n = setmetatable{name: user.name},{__index: user} { .name = 'John Doe' } name::show() -- John Doe user::show() -- Jane Smith
Do оператор
Использование оператора do работает так же, как и в Lua.
do { msg = "world" print "hello #{msg}" }
Оператор do также может использоваться как выражение. Результатом выражения do является последнее выражение в блоке.
print do { msg = "world" "hello #{msg}" }
Обработка ошибок (Try-Catch)
Блок try используется для обработки исключений. Это позволяет тестировать блок кода на наличие ошибок и корректно обрабатывать их, предотвращая неожиданный сбой программы.
try { -- Код, который вызывает ошибку error("Boom\!") } catch { -- Обработка ошибки (self содержит текст ошибки) print("Error caught: " .. self) } finally { -- Выполняется всегда, если присутствует print("Cleanup") }
Оператор try также может использоваться как выражение. Результатом выражения try является последнее выражение в блоках try/catch соответственно.
Основные отличия от MoonScript:
Блочная структура: Использование
{и}вместо отступов.Свободное форматирование: Игнорирование переносов строк и пробелов.
Разделители: Использование
;для разделения инструкций (препроцессор заменяет их на перевод строки).Синтаксис методов: Оператор
::для вызова методов экземпляра.switch: Ключевое слово
caseвместоwhen.Обработка ошибок: Встроенная конструкция
try/catch/finally.Множественное наследование, через встраивание: Концепция ООП в MoonScript и в Lua, в частности, не позволяет множественного наследования, точнее в «ванильном» Lua с метатаблицами и рекурсией головного мозга, возможно закостылить хоть какую глубину наследования. AST-шаблон такого глубокого колодца реализовать трудно, не невозможно — но трудно. Куда проще использовать паттерн «встраивания», как в незамысловатом GO: просто, дёшево, надёжно.
-
Встроенные документы: Выполнение Lattelua кода в режиме встроенного документа, в пространстве lua-кода. К примеру, если уже есть тысячи строк кода на Lua, и нет возможности просто всё переписать, можно воспользоваться встроенными документами:
local __latte = require "lattelua" local mt = { age = 25 } local msg = "hello world" local test = function() local copy = {} for k,v in pairs(_G) do copy[k] = v end return copy end local RESULT = __latte[[ getupenv(3) -- захватываем вышестоящее окружение, если нужно print "#{msg}" -- hello world print "#{mt.age}" -- 25 mt.age = 50 test! ]] for k,v in pairs(RESULT) do print(("\t%s => %s"):format(tostring(k), tostring(v))) end print(mt.age) -- 50 Автономный интерпретатор:
lluaс возможностью листинга компиляции. За основу взят lua 5.1 интерпретатор, совместимый с версиями Lua 5.1–5.4.REPL-режим на основе{и}поддерживается.
Важно понимать: Lattelua не добавляет новую модель исполнения, он компилируется в Lua используя те же таблицы и те же функции.
На выходе — обычный Lua‑код, который можно отладить, можно профилировать или оптимизировать руками.
Для разработки это огромная разница: маленькие скрипты, меньше визуального шума, и что самое главное, быстрый MVP:) Для больших проектов: меньше глобального состояния, меньше копипасты, как следствие проще рефакторить.
Для встраиваемых систем: не меняется ABI, не появляется второй VM и Lua остаётся главным.
Что тут думать, прыгать надо
Что ж, настало время показать все прелести Lattelua, так сказать на личном примере, никуда без велосипедостроения:)
Будем писать библиотеку, которая разукрасит Lattelua под golang-практики и как водится с псевдосервером для наглядности.
Используемый стек: Lanes, Linda и Cqueues, результатом будет библиотека упрощённой многопоточности, с её помощью можно насоздавать воркеров, затащить в них кооперативную многозадачность, прокинуть между ними каналы и обмениваться сообщениями без всяких там мьютексов (которые там, впрочем, есть).
Встроенный рантайм сам разрулит всё это дело, упрощая работу программиста такими конструкциями, как неблокирующий select, атомарные операции над данными и т.д. и т.п. Погнали ...
Архитектура spawn:
Диспетчер: Основной поток Lua, он не блокируется. При вызове
spawnон просто сериализует функцию и аргументы и кладет их в очередь задач.Очередь задач: Центральная шина, через которую задачи передаются воркерам.
У объекта
spawnодна центральная Lindabus.Воркеры: Набор системных потоков Lanes, которые крутятся в бесконечном цикле. Они конкурируют за задачи из
tasks. Как только воркер освобождается, он забирает следующую задачу.
spawn
lanes = require "lanes" -- определение task -- определение channel spawn = class { bus = {} wait = ->{} generator = {} new: (config)=>{ lanes.configure(config['core'] or {}) if lanes.configure bus = lanes.linda! wait = (n)->{ true, bus::receive(n, "wait/#{os.clock!}/#{math.random!}") } self.workers = {} self.config = config self.idle = "idle/#{os.clock!}/#{math.random()}" self.lock = "lock/#{os.clock!}/#{math.random()}" self.tasks = "tasks/#{os.clock!}/#{math.random()}" self.active = "active/#{os.clock!}/#{math.random()}" bus::limit(self.lock, 1) bus::set(self.active, 0) generator = lanes.gen("*", { required: {'lattelua'} }, task) for i = 1, config.workers { self::expand! } } channel: (capacity)->{ channel(bus, capacity) } expand: =>{ bus::send(nil, self.lock, true) current = bus::get(self.active) or 0 if self.config.limit > 0 and current >= self.config.limit { bus::receive(0, self.lock) return false } bus::set(self.active, current + 1) table.insert(self.workers, generator(bus, { idle: self.idle, lock: self.lock, tasks: self.tasks, active: self.active, config: self.config })) bus::receive(0, self.lock) current + 1 } sleep: (n)->{ return (wait(n)) } atomic: (init)->{ key = "atomic/#{os.clock!}/#{math.random()}" bus::limit key, 1 bus::send 0, key, init or 0 return setmetatable { key: key, add: (v=0)=>{ local key, value key, value = bus::receive nil, self.key if type(value) == 'number' { value += tonumber(v) or 0 } bus::send 0, self.key, value } }, {__call: (v)=>{ local key, value key, value = bus::receive nil, self.key if v { bus::send 0, self.key, v } else { bus::send 0, self.key, value } value } } } select: (cases)->{ default = nil if cases.default { default, cases.default = cases.default, nil } while wait(0.001) { done = false for desc, callback in pairs(cases) { assert(type(desc) == 'table', 'wrong channel description') assert(type(callback) == 'function', 'callback is not a function') ch = desc.chan switch desc.op { case "read" key, val = ch::check! if val != nil { ch::syn! if ch.cap == 0 if type(callback) == 'function' { done = { pcall(callback, val) } break } } case "write" payload = (#desc.args > 1) and desc.args or desc.args[1] sent = ch::push(payload) if sent { if ch.cap == 0 { ch::syn(0.020) } if type(callback) == 'function' { done = { pcall(callback) } break } } } } if type(done) == 'table' { return select(2, unpack(done)) } if type(default) == 'function' { ok = { pcall(default) } return select(2, unpack(ok)) } } } __call: (func)=>{ return (...)->{ alive = {} for _, w in ipairs(self.workers) { switch w.status { case "pending", "running", "waiting" table.insert(alive, w) } } self.workers = alive k, is_idle = bus::receive(0, self.idle) self::expand! unless is_idle bus::send(self.tasks, { fn: func, args: {...} }) } } } return { init: (config)->{ config = config or {} config.limit = ((tonumber(config.limit) or 0) > 0) and config.limit or 0 config.workers = ((tonumber(config.workers) or 0) > 0) and config.workers or 0 config.idle_timeout = ((tonumber(config.idle_timeout) or 0) > 0) and config.idle_timeout or 5 return spawn(config) } }
task это воркер который будет выполняться внутри каждого изолированного потока.
task
task = (bus, obj)->{ key = obj.tasks idle = obj.idle lock = obj.lock active = obj.active workers = obj.config.workers timeout = obj.config.idle_timeout or 5 while true { k, tsk = bus::receive(0, key) if not tsk { bus::send(0, idle, true) k, tsk = bus::receive(timeout, key) if not tsk { bus::send(nil, lock, true) count = bus::get(active) or 0 if count > workers { bus::set(active, count - 1) bus::receive(0, idle) bus::receive(0, lock) break } else { bus::receive(0, idle) bus::receive(0, lock) } } } if tsk and type(tsk['fn']) == 'function' { status, err = pcall(tsk.fn, unpack(tsk.args or {})) if not status { io.stderr::write("[Worker Error]: #{err}\n") } } } }
В воркере реализован механизм динамического масштабирования пула потоков на основе семафора свободных задач.
Как это работает:
Токены: У нас будет отдельный канал, ключ в Linda. Когда воркер свободен и готов брать задачу, он отправляет туда токен (true).
Проверка диспетчером: При вызове
spawn, диспетчер пытается забрать один токен без блокировки (таймаут 0).Защита от ложных токенов: Перед тем как объявить себя свободным, воркер неблокирующе проверяет, нет ли уже ожидающей задачи.
-
Решение о масштабировании:
Если токен получен, значит хотя бы один воркер простаивает — отдаем задачу.
Если токена нет, значит все воркеры заняты. Если мы еще не достигли лимита, диспетчер создает нового воркера на лету.
-
Уменьшение пула при простое:
Таймаут: Вместо бесконечного блокирования на очереди задач, воркер ждет задачу
timeoutсекунд.Смерть по таймауту: Если время вышло, воркер проверяет текущее количество активных потоков. Если их больше, чем минимально заданное, поток завершает свою работу.
Мьютекс для синхронизации: Так как воркеры и диспетчер работают параллельно, нам нужно безопасно изменять счетчик
active. Делаем это через блокирующий ключ в Linda с лимитом 1.
Каналы: Обертка над Linda для синхронизации и обмена данными, каждый канал — это просто уникальный ключ/строка на центральной шине. Так как Lanes и Linda не предоставляют «нативного» примитива wait_for_read_OR_write (Linda позволяет ждать только чтения), был реализован механизм Unbuffered Channels, протокол рукопожатия через логику двух ключей: sender кладет данные и ждет ack, receiver забирает данные и шлет ack.
channel
channel = class { bus = {} new: (b, capacity=-1)=>{ bus = b self.closed = false self.cap = capacity self.key = "channel/#{os.clock!}/#{math.random()}/key" self.ack = "channel/#{os.clock!}/#{math.random()}/ack" if self.cap != 0 { bus::limit(self.key, self.cap) } else { bus::limit(self.key, 1) } } put: (...)=>{ return if self.closed args ={ ... } payload = (#args > 1) and args or args[1] if self.cap != 0 { bus::send(self.key, payload) } else { sent = bus::send(self.key, payload) bus::receive(self.ack) if sent sent } } get: =>{ return if self.closed k, val = bus::receive(self.key) bus::send(self.ack, true) if self.cap == 0 and val != nil val } close: =>{ self.closed = true } check: =>{ bus::receive(0, self.key) } count: =>{ bus::count(self.key) } syn: (...)=>{ if ... { n = ... bus::receive(n, self.ack) } else { bus::send(self.ack, true) } } push: (...)=>{ bus::send(0, self.key, ...) } __call: (...)=>{ return { chan: self, op: "closed" } if self.closed args = { ... } if #args > 0 { { chan: self, op: "write", args: args } } else { { chan: self, op: "read" } } } }
С архитектурой каналов тесно связан метод select:
К объекту канала добавлен метаметод
__call, он анализирует аргументы.... Если они есть — это операция записи (write), возвращается дескриптор и тип операции с аргументами. Если нет — это операция чтения (read), возвращается дескриптор и тип операции.selectитерируется по переданной таблице и так как ключами являются таблицы-дескрипторы, мы проверяем поле операции внутри ключа, это своего рода Syntactic Sugar на go-like работу с каналами.При записи, вариативные аргументы упаковываются и отправляются. При чтении они распаковываются и передаются в callback-функцию.
Наличие
defaultделаетselectнеблокирующим
Ниже листинг псевдо-сервиса, который комбинирует вытесняющую многозадачность (Lanes) для утилизации ядер CPU и кооперативную многозадачность (Cqueues) для удержания тысяч одновременных I/O соединений.
Псевдо-роли:
Главный поток: Его задачи — забиндить порт, запустить сервисные потоки и воркеры, запустить cqueues-цикл
Сервисные потоки: поток для
acceptи поток статистики подключенийВоркеры: типа выполняют полезную нагрузку, внутри каждого воркер-потока крутится свой cqueues-цикл в ограниченном наборе сопрограмм.
вся коммуникация через каналы и atomic-операции
pseudo-server
#!/bin/llua HOST, PORT, MAX, THRDS = '127.0.0.1', 12345, 300, 1 spawn = require("spawn").init({ workers: 1 }) HOST = arg[1] if arg[1] PORT = tonumber arg[2] if arg[2] MAX = tonumber arg[3] if arg[3] THRDS = tonumber arg[4] if arg[4] socket = require "socket" cqueues = require "cqueues" signal = require "cqueues.signal" signal.block(signal.SIGINT, signal.SIGQUIT) total = spawn.atomic(0) coroutines = spawn.atomic(0) connections = spawn.atomic(0) queue = spawn.channel! done = spawn.channel THRDS + 2 server = socket.bind HOST, PORT thread = (queue)->{ local spawn, cqueues, socket cqueues = require "cqueues" spawn = require("spawn").init() socket = require "cqueues.socket" cq = cqueues.new! while true { quit = spawn.select{ [done!]: ->{"quit"} default: ->{ if coroutines! < MAX { spawn.select { [queue!]: (...)->{ cq::wrap (...)->{ fd = ... skt = socket.fdopen fd if skt { connections::add 1 skt::write "long work flow\nplease wait ...\n" skt::flush! cqueues.sleep 2 -- long work flow connections::add -1 skt::write "bye\n" skt::flush! try { skt::close! } finally { coroutines(cq::count!) } } }, ... } default: ->{ spawn.sleep 0.020 } } } else { spawn.sleep 0.020 } } } break if quit == "quit" cq::step 0 } } spawn((fd, clients)->{ local spawn, socket, server spawn = require("spawn").init() socket = require "socket" server = socket.tcp! server::setfd fd server::listen! while true { quit = spawn.select { [done!]: ->{"quit"} default: ->{ client = server::accept! try { clients::put client::getfd! } finally { total::add 1 } } } break if quit == "quit" } })(server::getfd!, queue) spawn(()->{ local spawn i = 1 CR = "\r" EL = "\27[K" frames = { "/", "-", [[\]], "|" } greeting = "Connections[%s] queue[%s] coroutines[%s] processed[%s]: " spawn = require("spawn").init() while spawn.sleep 0.050 { quit = spawn.select { [done!]: ->{"quit"} default: ->{ frame = "" i = 1 if i > 4 frame = frames[(i % #frames) + 1] io.write(CR .. EL .. greeting::format(connections!, queue::count!, coroutines!, total!) .. frame) io.flush! i += 1 } } break if quit == "quit" } })() for _ = 1, THRDS { spawn(thread)(queue) } cq = cqueues.new! cq::wrap(->{ listener = signal.listen(signal.SIGINT, signal.SIGQUIT) while true { signo = listener::wait! for i = 1, THRDS + 2 { spawn.select { [done(true)]: ->{} default: ->{} } } break } }) cq::loop!
Естественно, стоит отметить, что представленная реализация на базе spawn, channels и cqueues является концепцией, а не архитектурным паттерном. Её основная задача — продемонстрировать гибкость гибридной архитектуры, совмещающей вытесняющую многозадачность и кооперативный I/O.
Плюс всегда хотел показать злопыхателям, что Lua не только язык конфигов, на нем можно и нужно писать полноценные, многопоточные приложения, а с Lattelua это еще и инструмент, позволяющий строить архитектуру, а не бороться с синтаксисом.
А стоило ли?
Вопрос, на самом деле, интересный. Если смотреть со стороны, это выглядит примерно так: есть маленький, простой и элегантный язык Lua — и вместо того, чтобы писать на нём, кто-то берёт и начинает писать другой язык поверх него.
С парсером, AST, трансформациями, компилятором и всеми сопутствующими радостями жизни.
Рациональная часть мозга периодически говорит:
«Может, проще было написать библиотеку?»
Или:
«Lua же и так минималистичный — зачем ещё один синтаксис?»
Но на практике всё оказалось чуть интереснее. Lua — очень хороший runtime, лёгкий, быстрый, встраиваемый, с предсказуемой моделью выполнения.
Но как язык для больших приложений он иногда заставляет писать много шаблонного кода:
бойлерплейт вокруг классов
однообразные паттерны обработки ошибок
инфраструктурные конструкции для потоков
повторяющиеся обёртки над API
Со временем начинаешь замечать, что половина кода — это не логика программы, а структурный шум.
И вот здесь возникает соблазн сделать то, что делали программисты уже десятки лет: не писать больше кода — а поднять уровень абстракции.
Lattelua — это про новый синтаксис и трансформации, которые в итоге всё равно превращаются в обычный Lua.
Он пытается расширить его выразительность, оставляя тот же runtime, те же библиотеки, ту же экосистему. Фактически, Lua остаётся машинным языком проекта, а Lattelua становится языком, на котором удобно писать людям.
С практической точки зрения это даёт несколько вещей:
можно добавлять конструкции, которых нет в Lua (и не будет)
можно сокращать повторяющийся код
можно экспериментировать с архитектурой языка, не трогая runtime
И самое интересное — всё это остаётся совместимым с существующим Lua-миром. Любая программа на Lattelua — это в конце обычный Lua-код. Просто Lua.
Стоило ли всё это делать?
Если смотреть строго прагматично — возможно, нет. Lua и так прекрасно работает. Но разработка языков редко бывает чисто прагматичным занятием.
Это больше похоже на исследование: что будет, если немного сдвинуть границы привычного инструмента? В процессе таких экспериментов иногда появляются вещи, которые потом начинают жить своей жизнью.
И если хотя бы несколько разработчиков посмотрят на Lua чуть по-другому — возможно, это уже было не зря.
Комментарии (20)

domix32
18.03.2026 09:38То есть теперь фигурные скобки обозначают
таблицы
генераторы таблиц
области видимиости функций
области стурктурного связывания переменных
успехов не запутаться во всём этом.
Конечно, какой-то странный набор фишек получился.
Если функция не имеет аргументов, ее можно вызвать с помощью оператора
!, вместо пустых круглых скобок.!вызов - предпочтительный способ вызова функций без аргументов.А это вот вдвойне странно. Я сначала подумал там элвис оператор подменили на буратино, а это оказывается тоже вызов функции. Чем поясняется идиоматичность такого способа?

zmc Автор
18.03.2026 09:38То есть теперь фигурные скобки обозначают
А что вас смущает, сударь никогда не слышал про Golang, JavaScript, Scala, Julia
А это вот вдвойне странно
Чем поясняется идиоматичность такого способа?Так исторически сложилось) наследство от MoonScript. Я только могу предположить, что Leafo вдохновлялся ассоциацией
!с "выполнить"По началу самому это казалось неуклюжим, а по итогу, как не крути, это удобно

domix32
18.03.2026 09:38Скалу и жюлиа не трогал. Зато плюсы как главный язык. Это тот в котором больше 10 различных способов создать новый объект. Что как вы понимаете ни разу не играет в угоду понятности языка.
Lua исходно задумывался, чтобы быть простым и понятным, но от чего-то эта кофейная луна почему-то решила уйти от этого. Ванильная Lua уже поддерживал multiple returns аналогично go. Зачем в lattelua добавлять для этого оборачивание в фигурные скобки - непонятно. Оно и с точки зрения парсера сложнее - приходится обрабатывать специальный случай таблицы, насколько я понимаю - и с точки зрения пользователя. Оно хотя бы поддерживает многоуровневую распаковку как в JS?
const user = { id: 1, profile: { displayName: "Alice", avatar: "alice.jpg" } }; const { profile: { displayName } } = user; // profile - не биндится и работает исключительно как accessor // displayName - биндится при совпадении с именем поля объектаНу и отдельно стоит наверное уточнить вопрос - оно же как и в lua использует таблицы и прототипы для работы объектов и базового полиморфизма, да? Или lattelua решили сразу все объектами считать?

zmc Автор
18.03.2026 09:38В принципе все понятно:- статью не читал, но осуждаю. Если действительно интересно - потрудитесь, уделите 20 минут своего драгоценного времени на прочтение и все вопросы отпадут, как то так...

domix32
18.03.2026 09:38Не читал дальше референса, ибо там подробностей не то что бы много про имплементацию языка. Учитывая, что это третий этап шугаризации Lua детали конкретного диалекта может отличаться заметно. Особенно, если считать, что это всё же оригинальные имплементации, а не прямые деривативы PUC Rio Lua или Lua JIT.
Наличие классов в языке не отвечает на вопрос про таблицы. JS как и Lua используют систему прототипов для создания базового полиморфизма и наследования. В новых редакциях JS завезли уже полноценные классы, наследования, расширения и прочее, хотя под капотом они превращаются во всё те же метаобъекты с прототипами. В Lua классов не завезли даже в новых редакциях, то есть они появились в диалектах и способов реализации оных есть не единичное количество. Поэтому вопрос метатаблиц/прототипов для меня вопрос открытый. Встраивание документов тоже может быть как байдингом так и полной lua имплементацией интерпретатора языка. Поэтому мне показалось хорошей идеей спросить эти детали у того кто этим пользуется.
Из недовольств только перегрузка поведений похожих синтаксических конструкций. К самой статье претензий нет.

zmc Автор
18.03.2026 09:38Вы, судя по всему, воспринимает Lattelua как изолированный новый язык или новый интерпретатор, в то время как это — синтаксический сахар (transpiler).
это третий этап шугаризации
Детали имплементации в статье, чисто семантически это и есть Lua, просто с другим «лицом».
Поэтому вопрос метатаблиц/прототипов для меня вопрос открытый.
Вопрос про метатаблицы/прототипы здесь закрывается однозначно: классы в LatteLua — это синтаксический сахар над классическим Lua-паттерном метатаблиц. Никакой скрытой «JS-подобной» реализации на уровне C-движка нет. Это прозрачная обертка, которая избавляет от написания:
setmetatable(self, { __index = ... })Если вы умеете работать с метатаблицами в Lua, вы уже знаете, как работают классы в LatteLua.
как байдингом так и полной lua имплементацией интерпретатора языка
LatteLua не пытается переписать интерпретатор. Все внешние интеграции (как пример с cqueues или lanes) — это обычные Lua-модули.
Синтаксис просто делает работу с ними визуально чище (например, через операторы::для вызова методов или короткие лямбды->). Это не «биндинг» в смысле C-API, это просто удобный способ вызывать стандартные функции.LatteLua — это не «черный ящик» с собственной реализацией ООП. Это инструмент, который берет ваши знания о метатаблицах Lua и позволяет записывать их в стиле современных языков (Swift/Kotlin/JS), не теряя при этом прямого доступа к «кишкам» Lua. Если вдруг нужно посмотреть, во что превращается ваш класс — достаточно запустить транспайлер и увидеть обычный Lua-скрипт.
И вот это всё есть в статье)

zmc Автор
18.03.2026 09:38В новых редакциях JS завезли уже полноценные классы, наследования, расширения и прочее, хотя под капотом они превращаются во всё те же метаобъекты с прототипами.
Слово «полноценные» подразумевает изменение фундаментальной модели.
В классических ООП-языках (Java, C++) класс — это «чертеж». Сначала есть класс, потом по его образу создается объект. Память выделяется ровно под структуру класса.
В JS (и в Lua) «класс» не является чертежом. Это живой объект-прототип, который делегирует свои свойства другим объектам.
Утверждать, что в JS появились «полноценные классы», но они остались «прототипами» — это оксюморон в чистом виде, всё равно что сказать:
«Мы построили полноценный современный электрокар, хотя под капотом у него всё та же лошадь, просто её не видно за обшивкой».
domix32
18.03.2026 09:38под полноценными классами я подразумевал
1. введение ключевого слова class
2. добавление синтаксиса для наследования или расширения
3. наличие публичных и приватных членов/методовВ JS всё это завезли в том числе и в стандарт, но дефолтные имплементации в том же V8 имеют MIR, который генерируется крайне одинаковым, что и для прототипов, просто синтаксис становится более Java-подобным. Именно этот момент я и назвал "остались прототипами".

zmc Автор
18.03.2026 09:38Вы заблуждаетесь, ключевое слово class и обвязка - это синтаксический сахар над прототипным наследованием, в этом смысле ни чего никуда не завезли.
LatteLua делает для Lua ровно то же самое, что стандарт ES6 сделал для JavaScript: он не ломает мощную прототипную модель (метатаблицы), а дает ей современный, "полноценный" синтаксис, но это сахар а не концепт языка.
pecheny
Странно, что ориентируясь на этот критерий, вы проигнорировали вопрос типизации. Ведь, скорее общепринетая точка зрения, что для больших приложений динамическая типизация подходит хуже статической.
В обзор, наверно, стоило добавить Teal и Haxe, например.
zmc Автор
Это не столько проигнорированный критерий, сколько осознанная необходимость, здесь lua "машинный" язык, по отношению к lattelua. Было бы странно добавлять типизацию, при том что итог компиляции все тот же динамический lua.
Иными словами: типизация - это инструмент, а не цель. Примеры: SmallTalk, Erlang, Ruby да в конце концов Python с его огромной кодовой базой.
Лично мне понравился Lua++
cmyser
Typescript тихо смеётся в сторонке )
zmc Автор
Это истерика, не смех. И все мы знаем почему)
А смешно знаете что, то что в ветке о производительности статических ЯПов, упоминается средство контроля дисциплины разработчика (typescript)
cmyser
Ага)
Я хотел обратить внимание на то что вы решили не делать типы, а вон кто то решил сделать)
И тайпскрипт сейчас же факто стандарт
zmc Автор
Ага, всё так, только псевдо-статический тайпскрипт противоречит общепринятой точке зрения, что для больших приложений статическая типизация подходит лучше динамической. Тайпскрипт типизировали явно для других целей.
cmyser
Для каких же ?)
zmc Автор
Тут как в песне - если надо объяснять, то не надо объяснят ...
pecheny
Типизация языка и типизация рантайма – вещи связанные, но не слишком.
Для больших приложенией важнее типизация именно языка. То есть типизация на уровне взаимодействия человек-инструменты в процессе создания, а не типизация в процессе выполнения.
Типизация – действительно, инструмент. Чем больше и сложнее приложение, тем нужнее этот инструмет, чтобы разработка не захлебнулась.
Возможно ли разрабатывать "большие-сложные" без него? Да, используя другие механизмы защиты от чрезмерной сложности. Но эти механизмы сложнее и требоовательнее к разработчикам и с практической точки зрения относятся скорее к исключениям.
TypeScript – это как раз такой практический ответ на запрос "писать большое-сложное под динамическую среду".
Kvil_habr
luau
withkittens
И Luau