Маленькая книга о Lua #
Введение #
Есть прекрасная книга «Программирование на Lua». Её написал автор языка и это настоящий учебник “из первых рук”, освещающий все тонкости и особенности. Там даже упражнения есть. Будете ли вы читать её просто для ознакомления с еще одним языком программирования? Наверное, нет.
Другая крайность — «Lua за 60 минут». Я видел подобные статьи и не могу однозначно сказать, приносят ли они пользу или причиняют вред. Для меня знакомство с Lua началось именно с этой статьи, но понял я только то, что нуждаюсь в более глубоких источниках, чем прокомментированные сниппеты.
«Маленькие книги» Карла Сегуина настолько точно отвечают на запрос, что я набрался смелости присоединиться к жанру. Если моя “маленькая книга” будет хотя бы наполовину настолько хороша — сочту задачу выполненной.
Читайте подряд. Последовательность изложения отличается от канонической, зато вам не встретится раздражающее: “Вы поймете это потом”. Это быстрое чтиво, простое и легкое (надеюсь!), которое поможет понять, стоит ли переходить к более глубокому изучению или уже пора остановиться и больше не тратить время.
Применимость #
Применимость - это ответ на вопрос “зачем”. Существует совершенно безумное количество языков программирования и у каждого есть свой ответ на этот вопрос. Lua появился в недрах университетской кафедры компьютерной графики как встраиваемый язык для приложений на С.
Описывая Lua, автор называет его расширяемым (то есть умеющим вызывать функции приложения) и расширяющим (то есть умеющим предоставлять свои функции приложению). Будучи сам написан на C, он легко встраивается в C-приложения и взаимодействует с ним. Это совсем не означает, что единственная аудитория языка это программисты на C, но… да, им он полезней всего.
При этом Lua предлагает и самостоятельный интерпретатор, который может использовать внешние динамические библиотеки - как написанные специально для Lua, так и любые другие - с помощью механизма FFI. Во втором случае, правда, дополнительно потребуется внешняя библиотека или интерпретатор LuaJIT (написанный другим автором, но полностью совместимый с оригинальным Lua), в котором не только реализован FFI, но есть еще и JIT-компиляция, многократно ускоряющий и без того быстрый Lua.
Кстати, о быстродействии. Lua быстр - настолько, что из интерпретаторов с ним могут сравниться лишь Python и JavaScript, а LuaJIT в некоторых задачах их даже опережает (но это спорный момент, поэтому лучше остановимся на “сравним по быстродействию”).
Lua компактен - настолько, что его используют в маршрутизаторах Mikrotik, телефонных станциях Asterisk и даже “зашивают” в микросхемы.
И он прост. Его включают в сетевые сканеры nmap и wireshark, он работает внутри баз данных Redis и Tarantool, на нем пишут плагины для медиаплеера Rhythmbox… даже биржевых ботов на платформе Quik. Это немного похоже на “бойцовский клуб” - многие используют Lua, ничего о нем не зная, просто как часть платформы.
Кроме того, на Lua неплохо получаются веб-приложения - благодаря реализованной в проекте OpenResty интеграции c Nginx получаются весьма выносливые к нагрузкам системы. Такие как AliExpress, например. Или CloudFlare.
С легкостью создаются настольные приложения с графическим интерфейсом - с помощью IUP, QT, wxWidgets, FLTK или чего-нибудь еще. Не только под Linux, но и под Windows, MacOS или вообще без графической среды, в “сыром” framebuffer.
Графика, “то, ради чего” Lua писался изначально, открывает дорогу в игровую индустрию. Love2D, специально написанный игровой движок, работает не только в настольных операционных системах, но и на мобильных устройствах. Unity, трехмерный игровой движок, лежит в основе довольно серьезных игровых проектов. Для игр класса AAA, правда, потребуется написать платформу на более “машинном” языке, но внутри MTA, World of Warcraft и S.T.A.L.K.E.R. используется все тот же Lua.
А вот задачи реального времени на Lua делать нельзя, даже если использовать микросхемы NodeMCU. Это интерпретатор для виртуальной машины и доступа к реальному оборудованию у него нет. Не поможет даже написать “подложку” с прямым доступом и управлять ею (в играх так и делается), это будет лишь приближением к желаемому - виртуальная машина асинхронно выполняет свои задачи и вы никогда не сможете выполнить что-то “прямо сейчас”. Как бы ни была мала эта задержка, “взрослые” задачи реального времени не для Lua. Оставим это Erlang-у.
Установка #
Можно собрать Lua из исходников с сайта lua.org. Это самый суровый путь, но если вы линуксоид, то вам не привыкать. При помощи MinGW, CygWin или Windows Platform SDK вы точно так же можете собрать его и для Windows. Но это необязательно - во многих дистрибутивах Linux бинарные сборки доступны из менеджера пакетов. Для Windows их тоже можно найти и скачать из Интернета. Ну, или установить при помощи Chocolatey или NuGet - тамошних аналогов пакетных менеджеров.
Запуск программ #
Если просто запустить интерпретатор - он запустится в интерактивном режиме, где весьма неплох в качестве калькулятора.
$ lua
Lua 5.3.5 Copyright (C) 1994-2018 Lua.org, PUC-Rio
> 2 + 2 * 2
6
В интерактивном режиме можно поэкспериментировать со всеми возможностями языка, но для реального применения лучше все-таки написать программу и передавать имя с файлом программы интерпретатору для выполнения.
$ lua hello.lua
Hello, World!
Содержимое файла hello.lua - каноничная первая программа. Те, кто считает, что первая программа должна быть более осмысленной, упускают самое главное её предназначение - проверить работоспособность установленной среды.
print ("Hello, world!") -- print это функция вывода из стандартной библиотеки
Вы можете использовать Lua в командных файлах Linux, как и любой другой скриптовый язык.
#!/usr/bin/env lua
print "Hello, world!" -- для вывода одной строки можно обойтись без скобок
Сохраните эту программу в файл hello.lua, выполните команду chmod +x hello.lua, и вы сможете запускать её прямо из консоли:
$ ./hello.lua
Hello, world!
Комментарии #
Самая бесполезная часть программы - текст комментариев для интерпретатора не существует. Но это еще и самый лучший способ объяснить, что происходит, прямо в коде.
-- это короткий комментарий
--[[ длинные комментарии нужны не только для долгих "лирических отступлений",
но и позволяют временно "отключить" часть кода, не удаляя его ]]
Переменные #
Переменные не нужно специально объявлять или каким-то образом инициализировать: при первой записи они создается, при повторных - перезаписывается, по завершении программы - удаляется.
Также нет необходимости определять тип данных, которые будут хранится в переменной - при каждой записи определяется тип сохраняемого значения и выделяется требуемое количество памяти.
Имена переменных могут содержать латинские буквы, цифры (но не могут с них начинаться) и знак подчеркивания. Нет никаких специальных правил - СamelCase
, isHungarianNotation
, snake_case
(помните Маленького принца с его “питоном, проглотившим слона”?) или любой другой способ могут быть использованы без ограничений и в любом сочетании.
Имена переменных (как и остальные языковые конструкции) регистрозависимы - and
является зарезервированным словом и не может использоваться в качестве переменной, а And
и AND
- могут.
Единственное пожелание автора - не использовать переменные, которые начинаются со знака подчеркивания, за которым следуют заглавные буквы. Внутренние переменные могут совпасть с вашими и программа может повести себя, скажем так, странно.
Выражения #
Для записи данных используется операция присваивания. Математически это не равенство, а линейное отображение справа налево. Слева от знака операции должно быть имя переменной, справа - выражение.
4 = a -- это не работает; слева от знака операции только имена переменных
Выражение это способ представления данных при помощи цифр, операций и имен переменных. А значение выражения - это результат его вычисления. Всякий раз, когда интерпретатору встречается выражение, он его вычисляет, а всякий раз, когда встречается присваивание - сохраняет значение в память и связывает с именем переменной.
a = 4 -- теперь у нас есть переменная "a" и в ней содержится число 4
a, b = 4, 8 -- так тоже можно; это называется множественным присваиванием
a = 2 + 2 * 2-- значение можно записать как результат математической операции
a = b / 2 -- чтение переменных происходит при использовании их в выражениях
a, b = b, a -- простой и элегантный способ поменять значения переменных местами
i = i + 1 -- просто увеличение значения переменной на 1
x = a = b -- теперь и X, и A равны 4
Когда в переменной пропадает нужда - она уничтожается. В общем, все как везде, где произносятся слова “автоматическая сборка мусора”
Пустое значение #
Оно всего одно и записывается латинским словом nil
, что переводится как “ничего”. Это не нуль, не пустая строка и не нулевой указатель. Это ничего. Его не существует.
При чтении несуществующей переменной вы получите nil
. При записи в переменную значения nil вы эту переменную уничтожите. А если вы попробуете присвоить nil
несуществующей переменной, ничего не произойдет. Вообще.
Логические значения #
Их два - true (“истина”) и false (“ложь”).
a, b = true, false -- их можно напрямую присваивать
-- они могут быть результатом логических операций
x = not b -- true; логическое НЕ
x = a and b -- false; логическое И
x = a or b -- true; логическое ИЛИ
--[[ можно выполнять эти операции и с другими типами данных,
логика в этом случае несколько своеобразная, но она есть ]]
x = not nil -- true; nil аналогичен false
x = not 0 -- false; все остальные значения ведут себя как true, даже 0
x = 4 and 5 -- 5; and возвращает первый аргумент, если он ложный, иначе второй
x = 4 or 5 -- 4; or возвращает первый аргумент, если он НЕ ложный, иначе второй
x = 3 and 4 or 5 -- 4; это аналог тернарной операции "a?b:c" в Си
Числа #
Для хранения чисел Lua использует 64-битные блоки памяти. Это аналогично типу double в С. В версии 5.3 появились целые числа, но это мало что меняет. Единственная разница - вы можете явно создать число с плавающей точкой, если используете десятичную точку, после которой может быть и нуль.
n = 42 -- ответ на Главный вопрос жизни, вселенной и всего такого
n = 42.0 -- это значение типа double, а не int
n = 0x2A -- он же в шестнадцатиричной системе счисления
n = 420e-1 -- в экспоненциальной форме
n = 0x2A0p-1 -- или даже в экспоненциальной форме шестнадцатиричного счисления
x = 3.1415926 -- у вещественных чисел дробная часть отделяется точкой
y = .5 -- нуль перед десятичным разделителем необязателен
z = -500 -- как и в случае с отрицательными значениями; фактически это "z = 0 - 500"
Для чисел доступны основные арифметические операции:
a = 2 + 2 -- 4; сложение
a = 2 - 2 -- 0; вычитание
a = 2 * 2 -- 4; умножение
a = 2 ^ 2 -- 4; возведение в степень
a = 5 / 2 -- 2.5; деление
a = 5 //2 -- 2; целочисленной деление (без дробной части)
a = 5 % 2 -- 1; остаток от целочисленного деления
a = 2 + 2 * 2 -- 6; приоритеты операций, как в школе
a =(2 + 2)* 2 -- 8; но его так же можно менять скобками
А также операции сравнения:
a, b = 3, 4
x = a > b -- false; больше
x = a < b -- true; меньше
x = a >= b -- false; больше или равно
x = a <= b -- true; меньше или равно
x = a == b -- false; равно
x = a ~= b -- true; не равно
Строки #
Строка - это массив байт. Строка может содержать нулевые символы и это не станет признаком конца строки, символы могут быть в любой кодировке или вообще непечатными. В строках можно хранить любые двоичные данные. Между строками в 1 байт и строками в 10 мегабайт нет разницы. И нет ограничений, кроме размера оперативной памяти.
Строки нельзя менять, но можно перезаписывать. То есть нельзя влезть в строку и поменять в ней пятый байт, но можно сформировать такую же строку, но с другим пятым байтом и записать в ту же переменную. Как вы понимаете, это означает копирование строки со всеми накладными расходами этой операции.
s = "Для записи строк используются кавычки"
s = 'одиночные кавычки тоже допустимы'
s = "можно комбинировать 'одиночные' и \"двойные\" кавычки в любом сочетании"
s = [[А если в тексте много строк,
никто не мешает хранить его в строке целиком.
Да, при помощи двойных квадратных скобок, как в многострочным комментарии.]]
c = #'Hello' -- 5; операция # позволяет узнать длину строки (байт, не букв!)
s = "строки"..'можно'..[[объединять]] -- это называется "конкатенация"
w = [[
Экранирование специальных символов делается как в Си:
\a - звонок (beep)
\b - возврат на одну позицию (backspace)
\r - возврат каретки (carriage return)
\n - перевод строки (new line)
\t - горизонтальная табуляция
\v - вертикальная табуляция
\\ - обратная косая черта (backslash)
\' - одиночная кавычка
\" - двойная кавычка
]]
-- строки можно сравнивать
x = s == w -- false, строки НЕ равны
y = s ~= w -- true, строки НЕ равны
Приведение типов #
Если к числу применяются строковые операции, оно превращается в строку. И наоборот, если это возможно.
a = 100..500 -- 100500
a = "10" + 7 -- 17
a = 12 + "a" -- ошибка
Это спорная идея. Лучше использовать явные преобразования и не рисковать.
a = tostring( 10 ) -- "10"; строка
b = tonumber("10") -- 10 ; число
c = tonumber("XY") -- nil
Таблицы #
Используйте таблицы, если вам нужны массивы, словари, структуры, объекты. Таблицы - это всё. Хотя, на самом деле, таблицы это просто способ хранить множество пар “ключ-значение”, а все остальное - это то, какие вы выбираете ключи и какие значения.
Если в качестве ключей использовать натуральные числа, то это будет массив.
a = {42,616,999}
a[#a + 1] = "Земля-616" -- функция # возвращает количество элементов
print (a[4]) --> Земля-616
А еще “натуральные” означает, что нумерация индексов начинается с единицы, а не с нуля. Программистам на C нужно всегда об этом помнить.
Массивы не надо объявлять и под них не выделяется пространство. И индексы у них необязательно последовательные. Можно использовать большие числа и не бояться, что закончится память - в таблице будет только те ключи, которые вы внесли. Если ключи в таблице не являются натуральной последовательностью, то это разряженный массив.
a = {}
a[13] = 666
a[100500] = "Alpha Centauri"
a[42] = "The Ultimate Question of Life, the Universe, and Everything answer"
print(#a) --> 0, для разряженных массивов операция # неприменима
Значения элементов таблицы сами могут быть таблицами. Ограничений на вложенность никаких, таблицы могут быть многомерными матрицами.
a = {
{101,102,103},
{201,202,203},
{301,302,303}, -- эта запятая лишняя, но это допустимо
}
print (a[2][3]) --> 203
Индексы могут быть строковыми. С их помощью можно реализовать ассоциативные массивы (их еще называют словарями).
a = {
["one"] = 1,["two"] = 2,["three"] = 3, four = 4, five = "пять"
}
print (a.one) --> 1
print (a["four"]) --> 4
Можно заметить, что элементы таблицы определены по-разному и по-разному же вызываются. Вы можете обращаться с таблицей как со словарем, так и как со структурой - это будет все та же таблица.
Фрагменты #
У английского “chunk” есть масса смыслов. Если вы во имя точности предпочитаете англицизмы, используйте “чанк” - вполне сложившийся термин (в описании, например, HTTP-протокола). Если нет, то “фрагмент” ничем не хуже. Суть в том, что это просто какое-то количество инструкций языка без начала, конца и какого-то обрамления.
a = 1
print (a) --> 1
a = a + 1
b = a / 2
print (b) --> 1
В программе на Lua не нужно как-то специально оформлять “точку входа”, как это делается в C функцией main()
. Что касается точки выхода, то её тоже нет - выполнив все инструкции фрагмента, интерпретатор останавливается.
Вы, конечно, можете написать что-то вроде этого:
function main ()
a = 1
print (a) --> 1
a = a + 1
b = a / 2
print (b) --> 1
end
main ()
…но особого смысла в этом нет. Да, сначала объявится функция, но её потом все равно надо будет явным образом вызвать после объявления. И интерпретатору совершенно без разницы, какое у нее имя и надо ли её вообще выполнять.
Полученный код выполняется “как есть”, все остальное - на ваше усмотрение.
Блоки и области видимости #
Несколько инструкций (тот самый “фрагмент”) можно разместить между словами do
и end
. Это то же самое, что фигурные скобки в C/C++ или begin .. end
в Pascal. Блок, группирующий инструкции, является отдельной исполнимой сущностью и применим везде, где применима единичная инструкция.
По умолчанию все переменные глобальные, но при помощи слова local
можно явным образом определять переменные, доступные только внутри блока.
a = 1
do
local a = 10
print(a) --> 10
b = 2
end
print(a) --> 1
print(b) --> 2
Блоки могут быть вложенными. Во внутренних блоках локальные переменные внешних доступны так же, как и глобальные. При создании внутри блока глобальной переменной она будет доступна везде.
Управление потоком #
Программа - это набор действий, выполняемых последовательно. В теории автоматов любую программу можно представить в виде графа, где есть начальное, конечное и множество промежуточных состояний, по которым переходит исполнитель, выполняя инструкции. Поток выполнения - это маршрут, которым исполнитель добирается от начальной до конечной точки.
А команды управляющие этим маршрутом называются инструкциями управления потоком. Что позволяет вложить в программу множество маршрутов, которые будут зависеть от разных факторов.
Безусловный переход #
Есть разные мнения по поводу инструкции goto
, но иногда она действительно полезна. Она имеет неудобный синтаксис и массу ограничений - нельзя “впрыгивать” внутрь блока, “выпрыгивать” из функции и “впрыгивать” внутрь области видимости локальной переменной. Это сделано намеренно - чтобы не возникало желания ею пользоваться без насущной необходимости.
do
a = 0
::loop:: -- имя метки подчиняется тем же правилам, что и имена переменных
a = a + 1 -- это не блок, отступы просто для читаемости
if a % 2 == 0 then goto continue end
if a > 100 then goto exit end -- прыгать внутрь области видимости переменной нельзя
print (a)
::continue::
goto loop
local b = -a
::exit::
-- но если после метки до конца области видимости ничего нет, то можно
end
Условия #
Программа не всегда должна делать одно и то же.
В зависимости от условий поток выполнения может разделяться на ветки, поэтому условное выполнение называется ветвлением. Каждая ветка может разделяться ещё на ветки и так далее.
if a > 0 then
print ("'a' is positive") -- если выполняется условие
end
if b > 0 then
print ("'b' is positive")
else
print ("'b' is NOT positive") -- если не выполняется
end
if c > 0 then
print ("'c' is positive")
elseif c < 0 then -- это вместо switch-case, elseif-ов может быть много
print ("'c' is negative")
else
print ("'c' is zero")
end
Циклы #
Циклы нужны для многократного повторения одинаковых действий.
Цикл с предусловием проверяют условие повторения и выполняют блок, пока оно истинно. Циклы с постусловием сначала выполняют блок, а потом проверяют, достигнуто ли условие завершения.
while false do
print ('Hello?')
end -- не выполнится ни разу
repeat
print ('yes, hello!')
until true -- выполнится один раз
Цикл с выходом из середины не имеют граничных условий, поэтому в блоке необходимо явным образом предусмотреть выход. При помощи break
можно “выпрыгнуть” из цикла, функции и вообще из любого блока. Нет continue
, но его можно реализовать с помощью goto
.
a = 0
while true do
::continue::
a = a + 2
if a == 13 then -- правильно выбирайте условие выхода из цикла!
break
end
if a % 10 == 0 then goto continue end -- пропускаем всё, кратное 10
print (a)
end
В цикле со счётчиком некоторая переменная изменяет своё значение от заданного начального значения до конечного значения с некоторым шагом, и для каждого значения этой переменной блок выполняется один раз.
for i = 0,9,1 do -- если шаг равен 1, третий параметр можно пропустить
print(i)
end
print(i) --> nil; счетчик является локальной переменной и снаружи не доступен
Совместный цикл задает выполнение некоторой операции для объектов из множества.
x = {4, 8, 15, 16, 23, 42}
for k,v in ipairs(x)
print ('code #'..k..' is '..v)
end
print(k,v) --> nil nil
Функции #
В последнем примере ipairs()
это функция. И не просто функция, а итератор. Она получает таблицу и при первом вызове возвращает её первое значение, а при последующих - второй, третье и так далее.
Работает она примерно так:
x = {4, 8, 15, 16, 23, 42}
function values(t)
local i = 0
return function()
i = i + 1
return t[i] -- мы могли бы возвращать еще и i, но ipairs у нас уже есть
end
end
for v in values(x) do -- обратите внимание, что здесь мы тоже обходимся без ключа
print (v)
end
Этот коротенький пример сразу дает нам массу информации.
Во-первых, функции являются значениями первого класса. То есть их можно присваивать переменным, передавать параметром в другие функции и использовать в качестве возвращаемого значения.
sum = function (a,b)
return a + b
end
print (sum (2,2)) --> 4
function mul (a, b) -- более привычный способ записи всего лишь "семантический сахар"
return a*b
end
print (mul (2,2)) --> 4
Во-вторых, значение не знает имя переменной, в котором оно содержится. Таким образом все функции анонимны. Но не всегда…
function fact(n)
if n == 0 then return 1 else return n*fact(n-1) end
end
В-третьих, возвращаемых значений может быть несколько. Не один, не два, а столько, сколько захочет вернуть функция. Если при этом мы используем множественное присваивание, то можем получить все значения. Если имен переменных меньше - то “лишние” значения будут отброшены. А если больше - то они получат nil
.
Ну, и четвертых, функция является блоком со своей областью видимости для локальных переменных, имея доступ к переменным глобальным. А, будучи объявлена внутри другого блока, локальные переменные внешнего блока воспринимает как глобальные. Это и позволяет нам создавать замыкания - то есть функции, сохраняющие контекст. Весь контекст, без уточнений. Будьте благоразумны и не создавайте лишние сущности.
Кстати, кроме ipairs
в стандартной библиотеке Lua есть еще pairs
- более общий итератор, который позволяет работать с любыми таблицами, не только с массивами. Но в случае с массивами мы получим значения не по возрастанию ключей, а произвольно.
Сопрограммы #
Прежде, чем мы перейдем к сопрограммам (для вас, любители англицизмов, это “корутины”), несколько слов о многозадачности.
Мы уже говорили об графе состояний, исполнителе и потоке выполнения. В многозадачном алгоритме исполнителей несколько и у каждого из них - свое состояние и свой путь в графе состояний. Многопоточные алгоритмы - просто другое название для многозадачных.
Не так важно, выполняются ли потоки разными физическими процессорами или разными потоками одного, переключаются ли они операционной системой, “главным” потоком или явно передают управление друг другу - главное, что каждый поток (строго говоря “нить”, от английского “thread” - для англоманов “тред”) имеет свое состояние.
В Lua реализована “совместная многопоточность”, когда потоки явно уступают выполнение друг другу и сохраняют состояние до следующего момента, когда выполнение столь же явно будет передано им снова. Никакого разделения на физические процессоры или совместного использования процессорного времени. Никакой вытесняющей многозадачности, неблокирующего доступа, семафоров, диспетчеров. Только явная передача управление и сохранение состояния. Это логическая многозадачность - на самом деле в любой момент времени выполняется только один поток. Просто их несколько и они переключаются между собой.
c = coroutine.create(
function (t)
local i = coroutine.yield("initialized")
repeat
i = coroutine.yield(t[i])
until not t[i]
return "finished"
end
)
print (c,coroutine.status(c)) --> thread: 0x416e52a8 suspended
s = {2,3,5,7,11,13,17,19}
print (coroutine.resume(c,s)) --> true initialized
print (coroutine.resume(c,1)) --> true 2
print (coroutine.resume(c,3)) --> true 5
print (coroutine.resume(c,9)) --> true finished
print (c,coroutine.status(c)) --> thread: 0x416e52a8 dead
print (coroutine.resume(c,5)) --> false cannot resume dead coroutine
В этом примере описан весь функционал сопрограмм. Давайте разберемся с ним, шаг за шагом.
Вызов corutine.create
получает функцию и возвращает спящую (“suspended”) сопрограмму. Её можно “разбудить” вызовом coroutine.resume
и тогда она начинает выполняться.
Выполняется она до тех пор, пока не встретиться вызов coroutine.yield
. В этот момент управление возвращается к вызывающему потоку. Следующий вызов coroutine.resume
восстановит выполнение сопрограммы, передавая управление ровно в то место, где она была приостановлена.
Сопрограмма может быть вечной - если она время от времени уступает выполнение, то в какой-то момент её можно просто не возобновить. Но она может и завершится и перейти в состояние “dead”, возобновить из которого её будет уже невозможно.
Вызовы .resume
и .yield
могут не только передавать управление, но и обмениваться данными. В примере первый вызов .resume
передает сопрограмме таблицу простых чисел, как если бы она передавалась функции в параметре, два других вызова передают данные внутрь сопрограммы, а она, в свою очередь, передает при помощи .yield
обратно запрашиваемые значение из таблицы. При попытке получить значение за пределами таблицы сопрограмма заканчивается и возвращает сообщение при помощи return
, как самая заурядная функция.
Вот, собственно, и все. Эта простая реализация дает возможность реализовывать многозадачные алгоритмы даже не в многозадачной среде. Впрочем, никто не мешает вам создать на С приложение на с “настоящей” многопоточностью и запустить по виртуальной машине в каждом потоке, предоставив им возможность вызывать специально написанные потокобезопасные функции вашего приложения.
ООП, “которого нет” #
Структура с методами это простейший вариант объекта. Метод это функция, сохраненная в элементе таблицы.
a = {
name = "Nick",
sayHello = function (t)
print ("Hello, "..t.name)
end
}
a.sayHello(a) --> Hello, Nick
a:sayHello() --> Hello, Nick
Обратите внимание на две последние строчки. Они выполняют одно и то же, но по разному.
Чтобы функция знала, с какой таблицей она работает, ей это надо сообщить. В первом случае таблица явно передается параметром, во втором (когда используется двоеточие вместо точки) имя таблицы неявно передается первым параметром.
Есть несколько способов определять методы.
a = {
name = "Nick",
whoami = function (t) -- метод можно определить сразу в таблице
return t.name
end
}
-- а можно и отдельно от неё
function a.hello(t,name) -- здесь таблица передается в явном виде
local me = name or t.whoami()
print("Hello, "..me)
end
function a:bye(name)
local me = name or self.name -- а здесь появляется "магическая" переменная self
print("Goodbye, "..me)
end
-- способ вызова метода не зависит от того, как он был определен
a:hello() --> Hello, Nick
a.bye(a) --> Goodbye, Nick
a.hello(a,"John") --> Hello, John
a:bye("John") --> Goodbye, John
print(a:whoami()) --> Nick
Все это тоже семантический сахар.
Метатаблицы #
Латинское “meta” буквально означает “после” в том смысле, в каком слово “метафизика” означает “не только физика”. Метатаблица способна менять обычное поведение других таблиц.
a,b = {},{}
setmetatable (a,b) -- назначаем одну таблицу метатаблицей для другой
print (getmetatable (a) == b) --> true
b.x = 2
print (a.x) --> 1
print (getmetatable(a).x) --> 2
Что же нам это дает? В описанном примере - практически ничего. Вся сила метатаблиц в метаметодах.
Метаметоды #
Это “магические” методы, которые позволяют менять существующее поведение таблицы. “Магические” они потому, что их логика зависит от того, как они называются.
t = {
a = 42
}
print (t.a) --> 42
print (t.b) --> nil; стандартное поведение таблицы
-- создадим метатаблицу с измененной логикой
mt = {
-- этот метод вызывается при чтении несуществующей переменной
__index = function (t,k)
return k.." : ключ отсуствует"
end
}
setmetatable (t,mt) -- задаем метатаблицу для нашей таблицы и повторяем те же действия
-- теперь таблица ведет себя иначе
print (t.a) --> 42
print (t.b) --> b: ключ отсуствует"
Если вы попытаетесь прочесть из таблицы значение по несуществующему ключу, вы получите nil
. Но если у таблицы есть метатаблица, а в ней - метод __index
, то будет вызван он.
t, mt = {}, {}
t.a.b = 42 -- ошибка: t.a равно nil, а не пустая таблица
-- определяем новую логику
function mt:__index(k)
self[k] = {}
setmetatable (self[k], mt)
return self[k]
end
setmetatable (t,mt) -- и применяем её к нашей таблице
t.a.b.l.e = 42 -- больше никаких проблем с таблицами любой вложенности
print (t.a.b.l.e) --> 42
Наследование:
n = {name = "Nick"}
j = {name = "John"}
m = {
__index = m, -- если присвоить этому методу таблицу, поиск ключа будет вестить в ней
hello = function (t)
print ("Hello, "..t.name)
end
}
setmetatable(n,m)
setmetatable(j,m)
n:hello() --> ошибка: вы пытаетесь вызвать метод 'hello' (а он равен nil)
Внимание! Это красивый, но неправильный пример.
И дело тут не в том, что __index
вместо функции является таблицей (для этого “магического” метода это допустимо). И не в том, что элемент таблицы ссылается на саму таблицу - это тоже нормально. Просто до завершения объявления таблицы она не существует и метод __index
не к чему “привязывать”.
А вот правильный вариант:
n = {name = "Nick"}
j = {name = "John"}
m = { -- сначала создаем метатаблицу
hello = function (t)
print ("Hello, "..t.name)
end
}
m.__index = m -- потом назначаем её
setmetatable(n,m)
setmetatable(j,m)
n:hello() --> Hello, Nick
j:hello() --> Hello, John
Здесь мы затрагиваем одну интересную особенность. Пока элемент не определен - он недоступен. Возможно, стоило бы разрешить “ранее связывание”, чтобы первый вариант тоже был рабочим. Возможно, это даже будет сделано в будущем, но это уж как решит профессор Иерусалемски - автор и “пожизненный великодушный диктор” Lua.
Впрочем, для функций милостиво сделано исключение.
--[[ специально определяем функцию как переменную,
чтобы быть увереным, что тут нет никакого "скрытого сахара" ]]
fact = function (n)
if n == 1 then
return 1
else
return n * fact(n-1) -- формально определение функции еще не завершено
end
end
print (fact(5)) --> 120; тем не менее, все работает (но только начиная с версии 5.3)
Не индексом единым…
m = {
__index = function (t,k)
print ("Чтение "..k)
return t[k]
end,
__nexindex = function (t,k,v)
print ("Запись "..k)
t[k] = v
end
}
a = {x = 12}
setmetatable (a,m)
print (a.x) --> 12
a.y = 1 --[[ операция уходит в бесконечный цикл и завершается,
когда интерпретатор это понимает]]
(простите, но это был еще один неправильный пример)
Вопреки ожиданиям, программа ведет себя не так, как ожидалось. Это потому, что при обращении к таблице сначала выполняется обращение к элементам, а потом (если их нет) идет поиск “альтернативной логики” в метатаблице (опять же, если она есть).
Если элементы существуют, интерпретатору нет необходимости обращаться к метаметодам. Но мы можем “вынудить” его это делать.
m = {
__index = function (t,k)
print ("Чтение "..k)
return t.data[k]
end,
__newindex = function (t,k,v)
print ("Запись "..k)
t.data[k] = v
end
}
a = {data = {}}
setmetatable (a,m)
a.x = 12 --> Запись x
print (a.x) --> Чтение x
--> 12
a.y = 1 --> Запись y
print (a.z) --> Чтение z
--> nil
Мы переносим значения таблицы в таблицу внутри таблицы. Этот прием позволяет перехватывать все операции чтения и записи в таблицу и менять их.
Или не позволять получать элементы, который мы посчитаем закрытыми, как в “классическом” ООП. Хотя на этот счет профессор высказался предельно ясно: “если вам нужны приватные свойства, просто не обращайтесь к ним”.
Арифметические метатметоды #
__add
: сложение (+
)__sub
: вычитание (-
)__mul
: умножение (*
)__div
: деление (/
)__mod
: остаток от деления (%
)__pow
: возведение в степень (^
)__unm
: унарный (одноместный) минус (-
)__idiv
: целочисленное деление (//
)
В общем случае, если операнды не числа и не строки (которые можно преобразовать в числа), то при выполнении операции будут искаться метаметоды сначала в первом (левом) операнде, а затем во втором (правом). Как только метаметод будет найден, ему передадутся оба операнда, а результат выполнения будет обрезан до одного значения. Иначе будет выдана ошибка.
Обратите внимание на то, что если нужный метаметод будет найдет во втором операнде, они все равно будут переданы в том порядке, в котором записаны в выражении. Эту ситуацию нужно предвидеть и корректно обработать.
Логические метаметоды #
__band
: битовое И (&
)__bor
: битовое ИЛИ (|
)__bxor
: битовое ИЛИ-НЕ (~
)__bnot
: битовое одноместное НЕ (~
)__shl
: битовый сдвиг влево (<<
)__shr
: битовый сдвиг вправо (>>
)
Логика схожая.
Строковые метаметоды #
__concat
: конкатенация (..
)__len
: длина (#
)
Разница в том, что метаметоды будут использоваться, если аргументы не строки и не числа (которые можно привести к строкам). Если в таблице отсутствует реализация __len
, то будет вызвана стандартная функция #
, а она, как мы помним, не везде применима.
Метаметоды сравнения #
__eq
: равенство (==
)__lt
: меньше (<
)__le
: меньше или равно (<=
)
Тут немного веселее. Во-первых, оба операнда должны быть таблицами. Во-вторых, при встрече операций “больше” и “больше или равно”, они предварительно будут “перевернуты” в “меньше” и “меньше или равно”. В-третьих, результат будет преобразован к логическому типу (то есть к значениям true или false)
Метаметоды доступа #
__index
: чтение из таблицы__newindex
: запись в таблицу нового значения__call
: обращение к таблице как к функции__gc
: выполняется перед уничтожением таблицы сборщиком мусора
Тут надо заметить, что первые два метода вызываются только при обращении к отсутствующим элементам таблицы. Эти методы получают в параметрах саму таблицу и ключ.
Третий метод очень удобен в качестве “конструктора” - метода по умолчанию. Он получает в параметрах таблицу, за которой следуют все оригинальные аргументы вызова. Ну, а последний метод можно использовать в качестве “деструктора”.
Перегрузка операций #
Если при выполнении арифметической операции один из операндов является таблицей, то произойдет ошибка. Но с целой “колодой” метаметодов в рукаве мы можем это изменить.
Если один из операндов сложения является таблицей, интерпретатор будет искать метод __sum
в его метатаблице. Если оба операнда отвечают этим требованиям - будет использован метаметод первого (левого) операнда.
odd = {1,3,5,7,9} -- таблица нечетных чисел
even = {2,4,6,8} -- таблица четных чисел
set = {
__add = function (a,b) -- передаются операдны операции сложения
if type(b) ~= "table" then -- операнд может не быть таблицей
a[#a + 1] = b -- тогда просто добавляем его ко множеству
else -- в противном случае
for _,v in pairs (b) do
a[#a + 1] = v -- добавляем по одному все элементы этой таблицы
end
end
return a
end
}
setmetatable(odd,set) -- превращаем таблицы во "множества"
setmetatable(even,set)
even = even + 10 -- будьте осторожны, ситуацию "even = 10 + even" мы не предусмотрели
for _,v in pairs(odd + even) do
print(v) -- сумма множеств представляем собой множество всех элементов подмножеств
end
Исходя из постулата “таблица это объект” можно реализовать перегрузку всех арифметических и логических операций, чтения и записи, вызова функции… но все это работает только для таблиц - мета-таблицы нельзя назначать никаким другим типам данных. Это сделано специально, чтобы не давать слишком много возможностей “выстрелить себе в ногу”.
Стандартная библиотека #
В Lua граница между языком и библиотеками очень четкая. Все, что не касается непосредственно языка, вынесено в стандартную библиотеку. Можно собрать Lua без нее и тогда ваша программа не сможет даже вывести на экран строку “привет, мир”. Что является огромным преимуществом при использовании Lua внутри другого приложения в качестве встроенного языка, который должен уметь только то, что ему позволено и не оставит скрипту ни малейшего шанса “выбраться из песочницы”.
Стандартных библиотек немного и их функционал не впечатляет. Причина в том, что во имя переносимости в стандартных библиотеках есть только то, что может быть реализовано на всех платформах, где есть ANSI C. Все остальные библиотеки так или иначе являются сторонними.
Модули #
Принцип повторного использования кода подразумевает, что написанный функционал может быть использован повторно. Причем необязательно автором. Отчуждаемый код - это модули, пакеты, библиотеки, все то, благодаря чему нам не нужно каждый раз писать все самим. Девиз “все уже украдено написано до нас” особенно близок адептам Python-а, но не чужд и нам.
local lunajson = require 'lunajson' -- теперь мы умеем работать с JSON
local jsonstr = '{"Hello":["lunajson",1.5]}'
local t = lunajson.decode(jsonstr)
print(t.Hello[2]) --> 1.5
print(lunajson.encode(t)) --> {"Hello":["lunajson",1.5]}
Разумеется, не все так просто. Для того, чтобы использовать пакет, он должен присутствовать в одном из мест, где его будет искать require
.
В переменной package.path
содержится строка с вариантами шаблона путей для поиска. Вы можете увидеть их при помощи команды print
.
> print (package.path)
/usr/share/lua/5.3/?.lua;/usr/share/lua/5.3/?/init.lua;/usr/lib/lua/5.3/?.lua;/usr/lib/lua/5.3/?/init.lua;./?.lua;./?/init.lua
Подставьте вместо вопросительного знака имя модуля - и у вас будет список всех возможных файлов, который будет пытаться открыть команда require
.
Это обычная переменная и её можно переопределить или дополнить прямо в программе. И после этого команда require
будет исходить из её нового значения.
Проще всего разместить подключаемый модуль в той же директории, что и ваша программа. Создайте файл с именем module.lua
и впишите в него следующее:
local complex = {
__add = function (x, y)
return {a = x.a + y.a, b = x.b + y.b}
end,
__tostring = function (z)
-- этот метаметод вызывается, когда с таблицей пытаются обращаться, как со строкой
return z.a..'+'..z.b..'i'
end
}
return function (a, b)
z = {a = a, b = b}
setmetatable (z, {
__index = complex
})
return z
end
Теперь в своей программе вы можете написать так:
local complex = require "complex"
x = complex(3,2)
y = complex(4,5)
print (x + y) -- вот здесь перед выводом выполняется метатметод __tostring
Пакеты #
Имя модуля может содержать точки. При поиске модуля точка превращается в разделитель директории (прямая косая черта для Linux и обратная - для Windows, операционные системы без иерархической файловой системы могут использовать знак подчеркивания). Пакет - это все дерево модулей в одной директории.
Глубина вложенности не важна, дерево пакета может быть довольно разветвленным. Модули пакета являются автономными и связаны только логически.
А если вы снова внимательно посмотрите на значение переменной package.path
, то увидите там такую подстроку:
./?/init.lua
Это позволяет загружать пакет, в модуле init.lua
которого будет выполняться загрузка его компонентов или другие действия инициализации.
Заключение #
Теперь вы знаете, что такое Lua. А дальше ваш ждет “Программирование на Lua”, та самая книга, о которой мы говорили в начале. Если вы решили включить Lua в свой инструментальный набор, эта книга станет для вас настольной. В том числе и потому, что в ней автор рассказывает, что и почему в Lua реализовано именно так, а не иначе.
А еще - там есть такой громадный пласт материала, как встраивание в приложение на C и взаимодействие к кодом, написанным на C. Я посчитал, что для “маленькой книги” это слишком глубокий материал.
Мы также обошли стороной luarocks
- это пакетный менеджер, который Hisham Muhammad написал и поддерживает с 2006 года. Это не единственный менеджер пакетов, но для Lua он стал стандартом “де-факто”. Строго говоря, он не является частью языка, поэтому я не стал его описывать. Так же, как ZeroBrane Studio, которая является великолепной IDE и удачным примером приложения, полностью написанного на Lua.
Засим откланиваюсь. Спасибо, что дочитали.