Alerc
1 supporter
Маленькая книга о Lua

Маленькая книга о Lua

Apr 07, 2024

image

Карлу Сегуину - с благодарностью за "маленькую библиотеку".

Введение

Есть прекрасная книга "Программирование на Lua", написанная автором языка. Это настоящий учебник, освещающий все тонкости и особенности языка. Там даже упражнения есть. Будете ли вы её читать просто для ознакомления с еще одним языком программирования? Я думаю, что нет.

Другая крайность - это "Lua за 60 минут". Я видел подобные статьи и не могу однозначно сказать, приносят ли они пользу или причиняют вред. Для меня знакомство с Lua началось именно с этой статьи, но я понял из нее только то, что нуждаюсь в более глубоких источниках, чем прокомментированные сниппеты.

"Маленькие книги" Карла Сегуина настолько точно отвечают на мой запрос, что я набрался смелости присоединиться к жанру. Если моя "маленькая книга" будет хотя бы наполовину настолько хороша, то я сочту задачу выполненной.

Читайте подряд. Последовательность изложения отличается от канонической, зато вам ни разу не встретится раздражающее "вы поймете это потом". Это быстрое чтиво, простое и легкое (надеюсь!), которое поможет понять, стоит ли переходить к более глубокому изучению или уже пора остановиться и больше не тратить время.

Применимость

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

Описывая Lua, автор называет его расширяемым (то есть умеющим вызывать функции приложения) и расширяющим (то есть умеющим предоставлять свои функции приложению). Будучи сам написан на C, он легко встраивается в C-приложения и взаимодействует с ним. Это совсем не означает, что единственная аудитория языка это программисты на C, но... да, им он полезней всего.

При этом Lua предлагает и самостоятельный интерпретатор, который может использовать внешние динамические библиотеки - как написанные специально для Lua, так и любые другие - с помощью механизма FFI. Во втором случае, правда, дополнительно потребуется библиотека expat или интерпретатор LuaJIT (написанный другим автором, но полностью совместимый с оригинальным Lua), в котором не только реализован FFI, но есть еще и JIT-компиляция, многократно ускоряющий и без того быстрый Lua.

Кстати, о быстродействии. Lua быстр - настолько, что из интерпретаторов с ним могут сравниться лишь Python и JavaScript, а LuaJIT в некоторых задачах их даже опережает (но это спорный момент, поэтому лучше остановимся на "сравним по быстродействию").

Lua компактен - настолько, что его используют в маршрутизаторах Miktotik, телефонных станциях 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 - тамошнего аналога пакетного менеджера.

Запуск программ

Если просто запустить интерпретатор - он запустится в интерактивном режиме, где весьма неплох в качестве калькулятора.

$ 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!

Комментарии

Самая бесполезная часть программы - текст комментариев для интерпретатора не существует. Но это еще и самый лучший способ объяснить, что происходит, прямо в коде.

-- это короткий комментарий
--[[ длинные комментарии нужны не только для долгих "лирических отступлений",
     но и позволяют временно "отключить" часть кода, не удаляя его ]]

Переменные

Переменные не нужно специально объявлять или каким-то образом инициализировать: при первой записи они создается, при повторных - перезаписывается, по завершении программы - удаляется.

Также нет необходимости определять тип данных, которые будут хранится в переменной - при каждой записи определяется тип сохраняемого значения и выделяется требуемое количество памяти.

Имена переменных могут содержать латинские буквы, цифры (но не могут с них начинаться) и знак подчеркивания. Нет никаких специальных правил - СamelCaseisHungarianNotationunderscoredelimitednames или любой другой способ могут быть использованы без ограничений и в любом сочетании.

Имена переменных (как и остальные языковые конструкции) регистрозависимы - and является зарезервированным словом и не может использоваться в качестве переменной, а And и AND - могут.

Единственное пожелание автора - не использовать переменные, которые начинаются со знака подчеркивания, за которым следуют заглавные буквы. Внутренние переменные могут совпасть с вашими и программа может повести себя, скажем так, странно.

Выражения

Для записи данных используется операция присваивания. Математически это не равенство, а линейное отображение справа налево. Слева от знака операции должно быть имя переменной, справа - выражение.

4 = a -- это не работает; слева от знака операции только имена переменных

Выражение это способ представления данных при помощи цифр, операций и имен переменных. А значение выражения - это результат его вычисления. Всякий раз, когда интерпретатору встречается выражение, он его вычисляет, а всякий раз, когда встречается присваивание - сохраняет значение в память и связывает с именем переменной.

a = 4 -- теперь у нас есть переменная "a" и в ней содержится число 2
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 = [[А если в тексте много строк,
никто не мешает хранить его в строке целиком.
Да, при помощи двойных квадратных скобок, как в многострочным комментарии.]]

с = #'Hello' -- 5; операция # позволяет узнать длину строки (количество байт, не букв!)
s = "строки"..'можно'..[[объединять]] -- это называется "конкатенация"

w = [[ 
Экранирование специальных символов делается как в Си:
\a - звонок (beep)
\b - возврат на одну позицию (backspace)
\r - перевод страницы
\n - перевод строки (newline)
\r - возврат каретки (carriage return)
\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 без нее и тогда ваша

Gefällt dir dieser Beitrag?

Kaufe Alerc einen Kaffee

Mehr von Alerc