Programming in Lua 5.3.3-FULL

Programming in Lua 5.3.3-FULL

icon_04http://www.lua.org/pil/p1.html by
Roberto Ierusalimschy
Programming
in Lua 5.3
( F U L L )
© 2013, 2003

1.Programming in Lua 5.3.3 / 2.Corona SDK 

ГЛАВА 1 - Начинаем

Продолжая традицию, наша первая программа на Lua просто печатает "Hello World":

print("Hello World")

Если вы используете отдельный интерпретатор Lua, то все, что вам надо для запуска вашей первой программы, - это запустить интерпретатор - обычно он называется lua или Lua5.2 - с именем текстового файла, содержащего вашу программу. Если вы сохранили приведенную выше программу в файле hello. lua, то вам следует выполнить следующую команду:

% lua hello.lua

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

--defines a factorial functionfunction fact (n)    if n == 0 then    return 1    else    return n * fact(n-l)    endendprint ("enter a number:")a = io.read ("*n") -- reads a numberprint(fact(a))

1.1 - Chunk / Куски

Каждый фрагмент кода , который выполняет Lua, например, файл или одну строку в интерактивном режиме, является фрагмент. Более конкретно, кусок просто последовательность операторов.

Lua не нужен разделитель между подряд идущими операторами, но вы можете использовать точку с запятой, если хотите. Я лично использую точку с запятой только для разделения операторов, записанных в одной строке. Разбиение на строки не играет никакой роли в синтаксисе Lua; например, следующие четыре куски допустимы и эквивалентны:

а = 1b = а * 2а = 1;b = а * 2= 1; b = а * 2а = 1 b = а * 2 --некрасиво, но действует

Кусок может быть столь же просто как в примере «Helio World», или состоять из набора операторов и определений функций (которые на самом деле являются просто присваиваниями, как мы увидим позже), как в примере с факториалом. Блок может быть так велик, как вы хотите. Поскольку Lua используется также как язык для описания данных, блоки в несколько мегабайт не являются редкостью. Интерпретатор Lua не имеет каких-либо проблем при работе с большими блоками.

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

% luaLua 5.2 Copyright (С) 1994-2012 Lua.org, PUC-Rio>

Соответственно, каждая команда, которую вы вводите (как, например, print "Hello World"), выполняется немедленно, после того как вы ее введете. Для выходя из интерпретатора, просто наберите символ конца файла (ctrl-D в UNI, ctrl-z в Windows) или позовите функцию exit из библиотеки операционной системы - вам нужно набрать os. exit ( ).

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

Вы можете использовать опцию -i для того, чтобы заставить Lua перейти и интерактивный режим после выполнения заданного блока:

% lua -i prog

Команда вроде этой выполнит блок в файле prog и затем перейдет в интерактивный режим. Это особенно полезно для отладки и ручного тестирования. В конце данной главы мы рассмотрим другие опции командной строки для интерпретатора Lua.

Другим способом запускать блоки является функция dofile, которая немедленно выполняет файл. Например, допустим, что у вас есть файл им. Lua со следующим кодом:

function norm (х, у)    return^2 + у^2)^0.5endfunction twice (х)    return 2*хend

Тогда в интерактивном режиме вы можете набрать

> dofile ("lib1.Lua ")    --load your library> n = norm(3.4, 1.0)> print(twice(n))       --> 7.0880180586677

Функция dofile также полезна, когда вы тестируете фрагмент кода. Вы можете работать с двумя окнами: в одном находится текстовый редактор с вашей программой (например, в файле prog. lua), и в другом находится консоль с запущенным интерпретатором Lua в интерактивном режиме. После того как вы сохранили изменения в вашей программе, вы выполняете dofile ("prog. lua") в консоли для загрузки нового кода; затем вы можете начать использование нового кода, вызывая функции и печатая результаты.

1.2.Некоторые лексические соглашения

Идентификаторы (или имена) в Lua являются строками из латинских букв, цифр и знака подчеркивания, не начинающимися с цифры; например: 

i        j      i10       _ijaSomewhatLongName      _INPUT

Вам лучше избегать идентификаторов, состоящих из подчеркивания, за которым следуют заглавные латинские буквы (например, _version); они зарезервированы для специальных целей в Lua. Обычно я использую идентификатор _ (одиночное подчеркивание) для пустых (dummy) переменных.

В старых версиях Lua понятие того, что является буквой, зависело от локали. Однако подобные буквы делают вашу программу неподходящей для запуска на системах, которые не поддерживают данную локаль. Поэтому Lua 5.2 рассматривает в качестве букв только буквы из следующих диапазонов: A-z и a-z.

Следующие слова зарезервированы, вы не можете использовать их в качестве идентификаторов:

and
break
do
else
elseif
end
false
goto
for
funcon
if
in
local
nil
not
or
repeat
return
then
true
until
while

Lua учитывает регистр букв: and - это зарезервированное слово, однако And и and - это два разных идентификатора. Until, while
Комментарий начинается с двух знаков минуса (--) и продолжается до конца строки. Lua также поддерживает блочный комментарий, который начинается с --[[ и идет до следующего ]]. Стандартный способ закомментировать фрагмент кода - это поместить его между --[[  и  --]], как показано ниже:

--[[print (10) -- no action (commented out)--]]

Для того чтобы снова сделать этот код активным, просто добавьте один минус к первой строке:

---[[print (10) -- no action (commented out)--]]

В первом примере --[[ в первой строке начинает блочный комментарий, и двойной минус в последней строке также находится в этом комментарии. Во втором примере ---[ начинает обычный однострочный комментарий, поэтому первая и последняя строки становятся обычными независимыми комментариями. В этом случае print находится вне комментариев.

1.3.Глобальные переменные

Глобальным переменным не нужны описания; вы их просто используете. Не является ошибкой обратиться к неинициализированной переменной; вы просто получите значение nil в качестве результата:

print(b)     --> nilb = 10print(b)     --> 10

Если вы присвоите nil глобальной переменной, то Lua поведет себя, как будто эта переменная никогда не была использована:

b = nilprint(b)     --> nil

После этого присваивания Lua может со временем вернуть себе память, занимаемую данной переменной.

1.4.Отдельный интерпретатор

Отдельный (stand-alone) интерпретатор (также называемый lua.с в связи с названием его исходного файла или просто Lua по имени выполнимого файла) - это небольшая программа, которая позволяет непосредственное использование Lua. В этой секции представлены ее основные опции.

Когда интерпретатор загружает файл, то он пропускает первую строку, если она начинается с символа '#'. Это позволяет использовать Lua как скриптовый интерпретатор в UNIX-системах. Если вы начнете ваш скрипт с чего-нибудь вроде

#!/usr/local/bin/lua

(предполагая, что интерпретатор находится в /usr/local/bin) или

#!/usr/bin/env lua,

то вы можете непосредственно запускать ваш скрипт без явного запуска интерпретатора Lua.

Интерпретатор вызывается следующим образом:

lua [options] [script [args]]

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

Опция -е позволяет непосредственно задать код прямо в командной строке, как показано ниже:

% lua -е "print(math.sin(12))"       --> -0.53657291800043

(UNIX требует двойных кавычек, чтобы командный интерпретатор (shell) не разбирал скобки).

Опция -1 загружает библиотеку. Как мы уже видели ранее, -i переводит интерпретатор в интерактивный режим после обработки остальных аргументов. Таким образом, следующий вызов загрузит библиотеку lib, затем выполнит присваивание х=10 и наконец перейдет в интерактивный реясим.

% lua -i -llib -е "х = 10"

В интерактивном режиме вы можете напечатать значение выражения, просто набрав строку, начинающуюся со знака равенства, за которым следует выражение:

> = math.sin(3)    --> 0.14112000805987> а = 30> = а            --> 30

Эта особенность позволяет использовать Lua как калькулятор.

Перед выполнением своих аргументов интерпретатор ищет переменную окруясения с именем lua_init_5_2 или, если такой переменной нет, LUA INIT. Если одна из этих переменных присутствует и ее значение имеет вид @имяфайла, то интерпретатор запускает данный файл. Если lua_init_5_2 (или lua_init) определена, но не начинается с символа '@' то интерпретатор предполагает, что она содержит выполнимый код на Lua и выполняет его. lua_init дает огромные возможности по конфигурированию интерпретатора, поскольку при конфигурировании нам доступна вся мощь Lua. Мы можем загрузить пакеты, изменить текущий путь, определить свои собственные функции, переименовать или уничтожить функции и т. п.

Скрипт может получить свои аргументы в глобальной переменной arg. Если у нас есть вызов вида %lua script a b c, то интерпретатор создает таблицу arg со всеми аргументами командной строки перед выполнением скрипта. Имя скрипта расположено по индексу 0, первыи аргумент (в примере это "а") расположен по индексу 1 и т. д. Предшествующие опции расположены по негативным индексам, поскольку они расположены перед именем скрипта. Например, рассмотрим следующий вызов:

% lua -е "sin=math.sin" script а b

Интерпретатор собирает аргументы следующим образом:

arg [-3] = “lua”
arg [-2] = “_e”
arg [-1] = “sin=math.sin”
arg [0] = “script”
arg [1] = “a”
arg [2] = “b”

Чаще всего скрипт использует только положительные индексы (в примере это arg[1] и arg[2]).

Начиная с Lua 5.1 скрипт также может получить свои аргументы при помощи выражения ... (три точки). В главной части скрипта это выражение дает все аргументы скрипта (мы обсудим подобные выражения в разделе 5.2).

Упражнения

Упражнение 1.1. Запустите пример с факториалом. Что случится с вашей программой, если вы введете отрицательное число? Измените пример, чтобы избежать этой проблемы.

Упражнение 1.2. Запустите пример twice, один раз загружая файл при помощи опции -1, а другой раз через dofile. Что быстрее?

Упражнение 1.3. Можете ли вы назвать другой язык, использующий (-) для комментариев?

Упражнение 1.4. Какие из следующих строк являются допустимыми идентификаторами?

_end End end until? nil NULL

Упражнение 1.5. Напишите простой скрипт, который печатает свое имя, не зная его заранее.

ГЛАВА 2 - Типы и значения

Lua - язык с динамической типизацией. В языке нет определений типов, каждое значение несет свой собственный тип.

В Lua существует восемь базовых типов: nil, boolean, number, string, userdata, function, thread и table.Функция type возвращает тип для любого переданного значения:

print (type ("Hello world"))           --> string  print(type(10.4*3))                  --> namber    print (type(print))                  --> functionprint(type(type))                   --> functionprint(type(true))                   --> booleanprint (type(nil))                    --> nilprint(type(type(X)))                --> function

Последняя строка всегда вернет string вне зависимости от значения х, поскольку результат функции type всегда является строкой.

У переменных нет предопределенных типов; любая переменная может содержать значения любого типа:

print(type (а))                     --> nil ('а' еще не определена)а = 10   print(type(а))                      --> numberа - "a string!!"print(type(а))                      --> stringа = print                           --> да, это возможно!a (type (а))                      --> function

Обратите внимание на последние две строки: функции являются значения первого класса в Lua; ими можно манипулировать, как и любыми другими значениями. (Больше об этом будет рассказано в главе 6.)

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

2.1. Nil

Nil - это тип, состоящий из всего одного значения, nil, основной задачей которого является отличаться от всех остальных значений. Lua использует nil для обозначения отсутствующего значения. Как мы уже видели, глобальные переменные по умолчанию имеют значение nil до своего первого присваивания, вы также можете присвоить nil глобальной переменной, чтобы удалить ее.

2.2. Boolean (логические значения)

Тип boolean имеет два значения, true и false, которые служат для представления традиционных логических значений. Однако эти значения не монополизируют все условные значения: в Lua любое значение может представлять условие (condition). Соответствующие проверки (проверки условия в различных управляющих структурах) трактуют оба значения nil и false  как ложные и все остальные значения как истинные. В частности, Lua трактует ноль и пустую строку как истину в логических условиях.

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

2.3. Числа

Тип number представляет значения с плавающей точкой, заданные с двойной точностью. В Lua нет встроенного целочисленного типа.

Некоторые опасаются, что даже такие простые операции, как увеличение на единицу (инкремент) и сравнение, могут некорректно работать с числами с плавающей точкой. Однако на самом деле это не так. Практически все платформы сейчас поддерживают стандарт IEEE 754 для представления чисел с плавающей точкой. Согласно этому стандарту, единственным возможным источником ошибок является случай, когда число не может быть точно представлено. Операция округляет свой результат, только если результат не может быть точно представлен в виде соответствующего значения с плавающей точкой. Любая операция, результат которой может быть точно представлен, будет иметь точное значение.

Ha самом деле любое целое число вплоть до 2^53 (приблизительно 10^16) имеет точное представление в виде числа с плавающей точкой с двойной точностью (double). Когда вы используете значение с плавающей точкой с двойной точностью для представления целых чисел, нет никаких ошибок округления, за исключением случая, когда значение по модулю превосходит 2^53. В частности, Lua способен представлять любые 32-битовые целые значения без проблем с округлениями.

Конечно, дробные числа будут иметь проблемы с округлением. Эта ситуация не отличается от случая, когда у вас есть бумага и ручка. Если мы хотим записать 1/7 в десятичном виде, то мы где-то должны остановиться. Если мы используем десять цифр для представления числа, то 1/7 станет 0.142857142. Если мы вычислим 1/7*7 с десятью цифрами, то мы получим 0.999999994, что отличается от 1. Более того, числа, которые имеют конечное представление в виде десятичных дробей, могут иметь бесконечное представление в виде двоичных дробей. Так,12.7-20+7.3 не равно нулю, поскольку оба числа 12.7 и 7.3 не имеют точного двоичного представления (см. упражнение 2.3).

Прежде чем мы продолжим, запомните, целые числа имеют точное представление и поэтому не имеют ошибок с округлением.

Большинство современных CPU выполняет операции с плавающей точкой так же быстро (или даже быстрее), чем с целыми числами. Тем не менее легко скомпилировать Lua так, чтобы для числовых значений использовался другой тип, например длинные целочисленные значения или числа с плавающей точкой с одинарной точностью. Это особенно полезно для платформ без аппаратной поддержки чисел с плавающей точкой, таких как, например, встроенные системы. За деталями обратитесь к файлу luaconf.h в исходных файлах Lua.

Мы можем записывать числа, при необходимости указывая дробную часть и десятичную степень. Примерами допустимых числовых констант являются:

4   0.4   4.57е-3   0.3е12   5Е+20

Более того, мы можем также использовать шестнадцатеричные константы, начиная их с Ох. Начиная с Lua 5.2 шестнадцатеричные константы также могут иметь дробную часть и двоичную степень (перед степенью ставится 'р' или 'Р'), как в следующих примерах:

Oxff (255)  0х1АЗ (419)  0x0.2 (0.125)  0х1р-1 (0.5)  Оха.bр2 (42.75)

(Для каждой константы мы добавили ее десятичное представление.)

2.4. Строки

Строки в Lua имеют обычное значение: последовательность символов. Lua поддерживает все 8-битовые символы, и строки могут содержать символы с любыми кодами, включая нули. Это значит, что вы можете хранить любые бинарные данные в виде строк. Вы также можете хранить юникодные строки в любом представлении (UTF-8, UTF-16 и т. д.). Стандартная библиотека, которая идет вместе с Lua, не содержит встроенной поддержки для этих представлений. Тем не менее вы вполне можете работать с UTF-8 строками, что мы рассмотрим в разделе 21.7.

Строки в Lua являются неизменяемыми значениями. Вы не можете поменять символ внутри строки, как вы это можете в С; вместо этого вы создаете новую строку с желаемыми изменениями, как показано в следующем примере:

а = "one string”b = string.gsub (a, "one", "another")    --> изменим часть строкиprint(a)                               --> one stringprint(b)                               --> another string

Строки в Lua подвержены автоматическому управлению памятью, так же как и другие объекты Lua (таблицы, функции и т. д.). Это значит, что вам не надо беспокоиться о выделении и освобождении строк; этим за вас займется Lua. Строка может состоять из одного символа или целой книги. Программы, работающие со строками в 100К или ЮМ символов, - не редкость в Lua.

Вы можете получить длину строки, используя в качестве префикса оператор '#' (называемый оператором длины):

а = "hello"print(#а)           --> 5print(#"goodbye") --> 8

Литералы

Мы можем помещать строки внутри одиночных или двойных кавычек:

а = "a line"b = 'another line'

Эти виды записи эквиваленты; единственным отличием является то, что внутри строки, ограниченной одним типов кавычек, вы можете непосредственно вставлять кавычки другого типа.

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

Строки в Lua могут содержать следующие escape-последовательности:

а звонок (bell)b back spacef перевод страницы (form feed)n новая строка (newline)r возврат каретки (carriage return)t таб (horizontal tab)v вертикальный таб (vertical tab)\ backslash" двойная кавычка (double quote)' одинарная кавычка (single quote)

Следующий пример иллюстрирует их использование:

> print ("one linennext linen”in quotes”, 'in quotes’”)one linenext line“in quotes”, 'in quotes'> print('a backslash inside quotes: ’\”)a backslash inside quotes: ''> print("a simpler way: '\' ")a simpler way: ''

Мы можем задать символ в строке при помощи его числового значения, используя конструкции вида ddd и xhh, где ddd - это последовательность не более чем из трех десятичных цифр, a hh - последовательность ровно из двух шестнадцатеричных цифр. В качестве сложного примера две строки "alonl23"" и '971o104923'"обладают одним и тем же значением в системе, использующей ASCII: 97 - это ASCII-код для 'а', 10 - это код для символа перевода строки, и 49 - это код для цифры '1' (в этом примере мы должны записать значение 49 при помощи трех десятичных цифр 49, поскольку за ним следует другая цифра; иначе Lua трактовал это как код 492). Мы можем также записать ту лее самую строку как 'x61x6cx6fx0aх31х32х33х22', представляя каждый символ его шестнадцатеричным значением.

Длинные строки

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

раде = [[ An HTML Page    Lua]]write(page)

Иногда вы можете захотеть поместить в строку что-то вроде а=b [с [i] ] (обратите внимание на ] ] в этом коде) или вы можете захотеть поместить в строку часть кода, где какой-то фрагмент уже закомментарен. Для работы с подобными случаями вы можете поместить любое количество знаков равенства между двумя открывающими квадратными скобками, например [===[. После этого строка завершится только на паре закрывающих квадратных скобок с тем же самым количеством знаков равенства (]===] для нашего примера). Сканер будет игнорировать пары скобок с другим количеством знаков равенства. Путем выбора подходящего количества знаков равенства вы можете заключить в строку любой фрагмент.

То же самое верно и для комментариев. Например, если вы начинаете длинный комментарий с — [ = [, то он будет продолжаться вплоть до ]=]. Эта возможность позволяет закомментировать любой фрагмент кода, содержащий уже закомментированные фрагменты.

Длинные строки очень удобны для включения текста в ваш код, но вам не следует использовать их для нетекстовых строк. Хотя строки в Lua могут содержать любые символы, это не очень хорошая идея - использовать эти символы в своем коде: вы можете столкнуться с проблемами с вашим текстовым редактором; более того, строки вида ”rn" могут превратиться в "n". Поэтому для представления произвольных бинарных данных лучше использовать управляющие последовательности, начинающиеся с символа такие как"х13х01хА1xBB".Однако это представляет проблему для длинных строк из-за получающейся длины.

Для подобных ситуаций Lua 5.2 предлагает управляющую последовательность z: она пропускает все символы в строке до первого непробельного символа. Следующий пример иллюстрирует его использование:

data = "x00x01x02x03x04x05x06x07z            x08x09x0Ax0Bx0Cx0Dx0Ex0F"

Находящийся в конце первой строки z пропускает последующий конец строки и индентацию следующей строки так, что за байтом х07 сразу же следует байт х08 в получающейся строке.

Приведения типов

Lua предоставляет автоматическое преобразование значений между строками и числами на этапе выполнения. Любая числовая операция, примененная к строке, пытается преобразовать строку в число:

print("10" + 1)       --> 11print("10 + 1")        --> 10+1print("-5.Зе-10"*"2")   --> -1.06е-09print("hello" + 1)     -- ERROR (cannot convert "hello")

Lua применяет подобные преобразования не только в арифметических операторах, но также и в других местах, где ожидается число, например для аргумента math.sin.

Аналогично, когда Lua ожидает получить строку, а получает число, он преобразует число в строку:

print(10 .. 20) --> 1020

(Оператор .. служит в Lua для конкатенации строк. Когда вы его записываете сразу после числа, то вы должны отделить их друг от друга при помощи пробела; иначе Lua решит, что первая точка - это десятичная точка числа.)

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

В конце концов, строки и числа - это разные типы, несмотря на все эти преобразования. Сравнение вроде 10="10"дает ложное значение, поскольку 10- это число, а "10" - это строка.

Если вам нужно явно преобразовать строку в число, то вы можете использовать функцию tonumber, которая возвращает nil, если строка не содержит число:

line = io.readO          --прочесть строкуn = tonumber(line)       --попробовать перевести ее в числоif n == nil then  error(line .. "is not a valid number")else    print(n*2)end

Для преобразования числа в строку вы можете использовать функцию tostring или конкатенировать число с пустой строкой:

print(tostring(10) == "10")   --> trueprint(10 .. "" == "10")         --> true

Эти преобразования всегда работают.

2.5. Таблицы

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

Таблицы являются главным (на самом деле единственным) механизмом структурирования данных в Lua, притом очень мощным. Мы используем таблицы для представления обычных массивов, множеств, записей и других структур данных простым, однородным и эффективным способом. Также Lua использует таблицы для представления пакетов и объектов. Когда мы пишем io.read, мы думаем о «функции read из модуля io». Для Lua это выражение означает «возьми из таблицы io значение по ключу read».

Таблицы в Lua не являются ни значениями, ни переменными; они объекты. Если вы знакомы с массивами в Javaили Scheme, то вы понимаете, что я имею в виду. Вы можете рассматривать таблицу как динамически выделяемый объект; ваша программа работает только со ссылкой (указателем) на него. Lua никогда не прибегает к скрытому копированию или созданию новых таблиц. Более того, вам даже не нужно объявлять таблицу в Lua; на самом деле даже нет способа объявить таблицу. Вы создаете таблицы при помощи специального выражения, которое в простейшем случае выглядит как {};

а = {}               --создать таблицу и запомнить ссылку на нее в 'а'к = "х"а[к] = 10             --новая запись с ключом "х" и значением 10а[20] = "great"        --новая запись с ключом 20 и значением "great" print(а["x"])          --> 10 к = 20 print (а[к])   --> "great" а["х"] = а["х"] +1    -- увеличить запись "х" print(а ["х"])        --> 11

Таблица всегда анонимна. Не существует постоянной связи между переменной, которая содержит таблицу, и самой таблицей:

а["х"] = 10   b = а            -- 'b' ссылается на ту же таблицу, что и 'а' --> 10print(b["х"]      --> 10b["х"] = 20   print (а["х"])    --> 20а = nil         --только 'B' по-прежнему ссылается на таблицу b = nil         --на таблицу не осталось ссылок

Когда в программе не остается больше ссылок на таблицу, сборщик мусора в Lua со временем уничтожит таблицу и переиспользует ее память.

Каждая таблица может содержать значения с разными типами индексов, и таблица растет по мере добавления новых записей:

а = { }        -- пустая таблица-- создать 1000 новых записейfor i = 1, 1000 do а[i] = i*2 endprint(a[9])              --> 18a["x"]= 10 print(a["x"])     --> 10 print(a["y"])     --> nil

Обратите внимание на последнюю строку: как и в случае глобальных переменных, неинициализированные поля таблицы возвращают nil. Так же как и для глобальных переменных, вы можете присвоить полю таблицы nil, для того чтобы его уничтожить. Это не совпадение: I .на хранит глобальные переменные в обыкновенных таблицах. Мы рассмотрим это подробнее в главе 14.

Для представления записей вы используете имя поля как индекс. I .на поддерживает это представление, предлагая следующий «синтак

сический сахар»: вместо a ["паше"] вы можете писать а.name. Таким образом, мы можем переписать последние несколько строк предыдущего примера более чистым образом:

а.х = 10       --то же, что и а["х"] = 10print(а.х)      --то же, что и print(а["х"])print(а.у)      --то же, что и print(а["у"])

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

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

а = {}х = "у"а[х] = 10           -- записать 10 в поле "у"print(а[х])         --> 10 - значение поля "у"print(а.х)          --> nil - значение поля "х" (не определено)print(а.у)          --> 10 - значение поля "у"

Чтобы представить традиционный массив или список, просто используйте таблицу с целочисленными ключами. Нет ни способа, ни необходимости объявлять размер; вы просто инициализируете те элементы, которые вам нужны:

--прочесть 10 строк, запоминая их в таблицеа = {}for i = 1, 10 do  а [ i ] = io.reaD()end

Поскольку вы можете индексировать таблицу по любому значению, вы можете начать индексы в массиве с любого числа, которое вам нравится. Однако в Lua принято начинать массивы с единицы (а не с нуля, как в С), и некоторые средства Lua придерживаются этого соглашения.

Обычно, когда вы работаете со списком, вам нужно знать его длину. Она может быть константой или может быть где-то записана. Обычно мы записываем длину списка в поле с нечисловым ключом; по историческим причинам некоторые программы используют для этих целей поле "n".

Часто, однако, длина явно не задается. Поскольку любому неинициализированному полю соответствует значениеnil, то мы можем использовать это значение для определения конца списка. Например, если вы прочли десять строк в список, то легко запомнить, что его длина равна 10, поскольку его ключами являются числа 1, 2, ..., 10.Этот подход работает только со списками, в которых нет дыр, которые содержат значение nil. Мы называем подобные списки последовательностями (sequence).

Для последовательностей Lua предлагает оператор длины #’. Он возвращает последний индекс или длину последовательности. Например, вы можете напечатать строки, прочитанные в предыдущем примере, при помощи следующего кода:

--print the lines for i = 1, #a do   print (a [i] ) end

Поскольку мы можем индексировать таблицу значениями любого типа, то при индексировании таблицы возникают те же тонкости, что и при проверке на равенство. Хотя мы можем индексировать таблицу и с помощью целого числа 0, и с помощью строки "0", эти два значения различны и соответствуют разным элементам таблицы. Аналогично строки "+1", "01" и "1" также соответствуют разным элементам таблицы. Когда вы не уверены насчет типа ваших индексов, используйте явное приведение типов:

i = 10;  j ="10";  к = " + 10= {}a [i] = "one value"а [j] = "another value"а [к] = "yet another value"print (а [i])           -->one valueprint (а [j])           -->another valueprint (а [k])           -->yet another valueprint(a[tonumber(j)])   --> one valueprint(a[tonumber(k)])   --> one value

Если не обращать внимания на эти тонкости, то легко внести в программу трудно находимые ошибки.

2.6. Функции

Функции являются значениями первого класса в Lua: программы могут записывать функции в переменные, передавать функции как аргументы для других функций и возвращать функции как результат. Подобная возможность придает огромную гибкость языку; программа может переопределить функцию, чтобы добавить новую функциональность, или просто удалить функцию для создания безопасного окружения для выполнения фрагмента ненадежного кода (например, кода, полученного по сети). Более того, Lua предоставляет хорошую поддержку функционального программирования, включая вложенные функции с соответствующим лексическим окружением; просто подождите до главы 6. Наконец, функции первого класса играют важную роль в объектно-ориентированных возможностях Lua, как мы увидим в главе 16.

Lua может вызывать функции, написанные на Lua, и функции, написанные на С. Обычно мы используем функции, написанные на С, для того чтобы получить высокое быстродействие и доступ к возможностям, недоступным непосредственно из Lua, таким как доступ к средствам операционной системы. Все стандартные библиотеки в Lua написаны на С. Они включают в себя функции для работы со строками, работы с таблицами, ввод/вывод, доступ к базовым возможностям операционной системы, математические функции и отладку.

Мы обсудим функции Lua в главе 5 и функции на С в главе 27.

2.7. userdata и нити

Тип userdata позволяет запоминать произвольные данные языка С в переменных Lua. У этого типа нет встроенных операций, за исключением присваивания и проверки на равенство. Значения данного типа используются для представления новых типов, созданных приложением или библиотекой, написанной на С; например, стандартная библиотека ввода/вывода использует их для представления открытых файлов. Мы более подробно обсудим этот тип позже, когда перейдем к С API.
Тип нить (thread) будет рассмотрен в главе 9, где мы рассмотрим сопрограммы.

Упражнения

Упражнение 2.1. Что является значением выражения type (nil) ==nil? (Вы можете использовать Lúa для проверки своего ответа.) Можете ли вы объяснить результат? Упражнение 2.2. Что из приведенного ниже является допустимыми числами? Каковы их значения?
.0е12  .е12  О.Ое  0x12  OxABFG  ОхА  FFFF  OxFFFFFFFF
Ox   OxlPlO   O.lel   0x0.lpl
Упражнение 2.3. Число 12.7 равно дроби 127/10, где все числа являются десятичными. Можете ли вы представить его как значение двоичной дроби? А число 5.5?
Упражнение 2.4. Как вы запишете следующий фрагмент XML в строку Lúa?

<![СDATA[ Hello world ]]>

Используйте как минимум два разных способа.
Упражнение 2.5. Допустим, вам нужно записать длинную последовательность произвольных байт как строковую константу в Lúa. Как вы это сделаете? Обратите внимание на читаемость, максимальную длину строки и быстродействие.
Упражнение 2.6. Рассмотрите следующий код: а = {} ; а. а = а
Что будет значением а. а. а. а? Какое-либо а в этой последовательности как-то отличается от остальных?
Теперь добавьте следующую строку к предыдущему коду:
а. а. а. а = 3
Что теперь будет значением а. а. а. а?

ГЛАВА 3 - Выражения 

Выражения представляют значения. Выражения в Lua включают числовые константы и строковые литералы, переменные, унарные и бинарные операции и вызовы функций. Выражения также включают в себя нестандартные определения функций и конструкторы для таблиц.

3.1. Арифметические операторы

Lua поддерживает стандартные арифметические операторы:  бинарные '+' (сложение), '-' (вычитание), '*' (умножение), '/' (деление), '^' (возведение в степень), '%'(остаток от деления)  иунарный‘-’ (изменение знака).Все из них работают с числами с плавающей точкой.  Например,    х^ 0.5 вычисляет квадратный корень из х, а х^ (-1/3) вычисляет обратное значение к кубическому корню из х.

Следующее правило определяет оператор остатка от деления:

а % b == а - math.floor(a/b)*b

Для целочисленных операндов у него стандартное значение, и результат имеет тот же знак, что и второй операнд. Для вещественных операндов у него есть некоторые дополнительные возможности. Например, х%1 дает дробную часть х, а х-х%1 дает его целую часть. Аналогично х-х%0.01 дает х точно с двумя десятичными знаками после запятой:

х = math.piprint- х%0.01) --> 3.14

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

local tolerance = 10function isturnback (angle)angle = angle % 360return (math.abs(angle - 180) < tolerance)end

Это определение работает даже для отрицательных углов:

print(isturnback (-180)) --> true

Если вы хотите работать с радианами вместо градусов,  мы просто изменим константы в функциях:

local tolerance = 0.17function isturnback (angle)angle = angle % (2*math.pi)return (math.abs(angle - math.pi) < tolerance)end

Все, что нам нужно, - это операция angle % (2*math. pi) для приведения любого угла к интервалу [0,2 pi].

3.2. Операторы сравнения

Lua предоставляет следующие операторы сравнения:

<    >    <=   >=   ==

Все эти операторы всегда дают булево значение.

Оператор == проверяет на равенство; оператор ~= - это отрицание равенства. Мы можем использовать оба этих оператора к любым двум значениям. Если значения имеют различные типы, то Lua считает, что они не равны. В противном случае Lua сравнивает их соответственно их типу. Значение nil равно только самому себе.

Lua сравнивает таблицы и объекты типа userdata по ссылке, то есть два таких значения считаются равными, только если они являются одним и тем же объектом. Например, после выполнения следующего кода:

а = {}; а.х = 1; а.у = 0 b = {}; b. х = 1; b. у = 0 с = а

мы получим а == с, но а ~= b.

Мы можем применять операторы порядка лишь к паре чисел или паре строк. Lua сравнивает строки в алфавитном порядке, следуя установленной для Lua локали. Например, для португальской локали Latin-1 мы получим "acai"<"acai"<"acorde". Значения типов, отличных от строк и чиссл, могут быть сравнены только на равенствзначений-(и неравенство).

При сравнении значений различных типов нужно быть аккуратным: помните, что "О" отличается от 0. Более того,2<15 очевидно истинно, но "2"<"15" ложно. В случае, когда вы пытаетесь сравнить строку и число, например2<"15", возникает ошибка.

3.3. Логические операторы

Логическими операторами являются and, or и not. Как и управляющие конструкции, логические операторы трактуют false и nil как ложные, а все остальные - как истинные значения. Оператор and возвращает свой первый операнд, если он ложен, иначе он возвращает свой второй операнд. Оператор or возвращает свой первый операнд, если он не ложен; иначе он возвращает свой второй операнд:

print(4 and 5)           --> 5    print(nil and 13)          --> nilprint(false and 13)        --> falseprint(4 or 5)            --> 4print(false or 5)          --> 5

Оба оператора (and и or) использует сокращенное вычисление, то есть они вычисляют свой второй операнд, только когда это необходимо.  Это гарантирует, что выражения вроде       (type (v) =="table"and v.tag=="hl") не вызовут ошибок при их вычислении: Lua не будет пытаться вычислить v. tag, когда v не является таблицей.

Полезной конструкцией в Lua является х=х or v, эквивалентная следующему коду:

if not х then х = v end

To есть значение x устанавливается равным значению по умолчанию v, если х не определено (при условии, что хне равно false).

Другой полезной конструкцией является (a and b) or с  или просто a and b or с, поскольку у оператора and более высокий приоритет, чем у or. Она эквивалентна выражению а?b: с в языке С, при условии что b не ложно. Например, мы можем выбрать максимум из двух чисел х и у при помощи следующего оператора:

max => у) and х or у

Когда х > у, то первое выражение в операторе and истинно, поэтому он возвращает свое второе значение (х),которое всегда истинно (поскольку это число) и затем оператор or возвращает свой первый операнд, х. Если выражение х > у ложно, то результат оператора and также ложен, и поэтому оператор or возвращает свой второй операнд, у.

Оператор not всегда возвращает булево значение:

print(not nil)                --> true    print(not false)              --> trueprint(not 0)                --> falseprint(not not 1)              --> trueprint(not not nil)             --> true

3.4. Конкатенация

Lua обозначает оператор конкатенации как . . (две точки). Если операнд является числом, то Lua переведет его в строку. (Некоторые языки используют для конкатенации оператор ' + ', но в Lua 3+5 отличается ОТ 3 . .5.)

print("Hello”.."World")      -->Hello Worldprint (0 .. 1)             -->01print(000 .. 01)           -->01

Помните, что строки в Lúa являются неизменяемыми значениями. Оператор конкатенации всегда создает новую строку, не изменяя своих операндов:

а = "Hello"print.. " World")      --> Hello Worldprint (a)              --> Hello

3.5. Оператор длины

Оператор длины работает со строками и таблицами. Со строками он дает количество байт в строке. С таблицами он возвращает длину последовательности, представленной таблицей.

С оператором длины связано несколько распространенных идиом для работы с последовательностями.

print(а[#а])               -- печатает последний элемент последовательности 'а'а[#а] = nil              -- удаляет последний элемента[#а + 1] = v            -- добавляет 'v' к концу списка

Как мы видели в предыдущей главе, оператор длины непредсказуем для списков с дырками (nil). Он работает только для последовательностей, которые мы определили как списки без дырок. Более точно последовательность— это таблица, где ключи образуют последовательность 1,..,n для некоторого n. (Помните, что любой ключ со значением nil на самом деле в таблице отсутствует.) В частности, таблица без числовых ключей — это последовательность длины ноль.

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

а = {} а[1] = 1а[2] = nil        --ничего не делает, так как а[2] уже nil а[3] = 1 а[4] = 1

Легко сказать, что длина этого списка четыре и у него есть дырка по индексу 2. Однако что можно сказать о следующем примере?

а = {} а[1] = 1 а[10000] = 1

Должны ли мы рассматривать это а как список с 10 000 элементами, где 9998 элементов равны nil? Теперь пусть программа делает следующее:

а[10000] = nil

Что же произошло с длиной списка? Должна ли она быть 9999, поскольку программа удалила последний элемент? Или может быть 10 000, так как программа просто изменила значение последнего элемента на nil? Или же длина должна стать 1?

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

Еще более проблемными являются значения nil в конце списка. Какой должна быть длина следующего списка?

а = {10/ 20/ 30/ nil, nil}

3.6. Приоритеты операторов

Приоритеты операторов в Lua заданы в таблице ниже, от самого старшего к самому низшему:

   ^     not          #         -      (унарный)     *             /          %     +            -     ..     <>         <=      >=       ~=      ==     and     or

Все бинарные операторы ассоциативны влево, за исключением '^'(экспоненцирование ) и '. .' (конкатенация), которые ассоциативны вправо. Поэтому следующие выражения слева эквивалентны выражениям справа:

a+I < b/2+1                  (a+i)<((b/2)+1)5+x^2*8                      5+((x^2)*8)  a < y and y <= z             (a < y) and (y <= z) -x^2                         -(x^2)     X^y^z                        X^(y^z)

Когда сомневаетесь, всегда используйте скобки. Это легче, чем смотреть в руководстве, и, скорее всего, потом, когда вы будете читать этот код, у вас снова возникнут сомнения.

3.7.Конструкторы таблиц

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

Простейший конструктор - это пустой конструктор, {}, который создает пустую таблицу; мы раньше это уже видели. Конструкторы также инициализируют списки. Например, оператор

days = ("Sunday", "Monday", "Tuesday", "Wednesday","Thursday", "Friday", "Saturday"}

проинициализирует days[1] значением "Sunday" (первый элемент конструктора имеет индекс 1, а не 0), days [2] - значением "Monday" ит. д.:

print(days[4]) --> Wednesday

Lua также предлагает специальный синтаксис для инициализации таблиц по полям, как в следующем примере:

а = (х=10, у=20}

Эта строка эквивалентна следующим командам:

а = {}; а.х=10; а.у=20

Исходное выражение проще и быстрее, поскольку Lua сразу создает таблицу с правильным размером.

Вне зависимости от того, каким конструктором мы пользовались для создания таблицы, мы всегда можем добавлять и удалять поля из нее:

w ==0, у=0, label="console"= {math.sin(0), math.sin(1), math.sin(2)}w(l] = "another field"     -- добавить ключ 1 к таблице 'w'X . f = w              -- добавить ключ “f” к таблице 'x'print (w [ "x"] )       --> 0print(w[1])             --> другое полеprint(x.f [1] )          --> другое полеw.x = nil              -- удалить поле "х"

Однако создание таблицы сразу с правильным конструктором более эффективно и наглядно.

Мы можем смешивать эти два стиля инициализации (списком и по полям) в одном и том же конструкторе:

polyline = {color="blue",            thickness=2,            npoints=4,            {x=0, y=0},      -- polyline [1]            {x=-10, y=0},     -- polyline [2]            {x10, y=1},      -- polyline [3]             {x=0, у1}        -- polyline [4]   }

Приведенный выше пример также показывает, как можно вкладывать конструкторы один в другой для представления более сложных структур данных. Каждый из элементов polyline [i] - это таблица, представляющая собой запись:

print(polyline[2].х)     --> -10print(polyline[4].у)     --> 1

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

opnames = { ["+"] = "add", [“-”]= "sub",           ["*"] = "mul", ["/"] = "div"}i = 20; s = [“-”]a = { [i + 0] = s, [i+1] = s..s, [i+2] = S..S..S}print(opnames[s])       --> subprint (a [22])          --> ---

Этот синтаксис более неудобен, но и более общ: рассмотренные ранее формы конструктора являются частными случаями этого более общего синтаксиса. Конструктор {х=0,у=0} эквивалентен {[”х"]=0,["у"]=0}, и конструктор{"r","g","b"} эквивалентен {[1]="r",[2]= "g",[3]="b"}

Вы всегда можете поставить запятую после последней записи в конструкторе. Эти запятые необязательны:

а = {[l]="red", [2]="green", [3]="blue",}

Это освобождает программы, генерирующие конструкторы Lua, от необходимости обрабатывать последний элемент особым образом.

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

{х=10, у=45; "one", "two", "three"}

Упражнения

Упражнение 3.1. Что напечатает следующая программа?

for i = -10, 10 doprint (i, i % 3)end

Упражнение 3.2. Что является результатом выражения 2^3^4? А что насчет 2 ^ -3 ^4 ?

Упражнение 3.3. Мы можем представить многочлен а

an x^n + a(n-1) x^(n-1) + ... + a1 x1 + a0

в Lua как список его коэффициентов {a0, a1 ..., an}.

Напишите функцию, которая получает многочлен (представленный таблицей) и значение х возвращает значение полинома в x.

Упражнение 3.4. Можете ли вы написать функцию из предыдущего упражнения так, чтобы использовать nсложений и n умножений (и не использовать возведение в степень)?

Упражнение 3.5. Как вы можете проверить, является ли значение булевым, не прибегая к функции type?

Упражнение 3.6. Рассмотрим следующее выражение:

(х and у and (not z) ) or ( (not у) and x)

Нужны ли в этом выражении круглые скобки? Как бы вы посоветовали использовать их в данном выражении?

Упражнение 3.7. Что напечатает следующий фрагмент кода?

Объясните.

Sunday = "monday"; monday = "sunday"t = {sunday = "monday", [sunday] = monday}print(t.Sunday, t[Sunday], t[t.Sunday])

Упражнение 3.8. Предположим, вы хотите создать таблицу, которая с каждой управляющей последовательностью (escape sequence) для строк связывает ее значение. Как бы вы написали конструктор такой таблицы?

ГЛАВА 4 - Операторы

Lua поддерживает почти традиционный набор операторов, похожий на набор, используемый в С или Pascal. Традиционные операторы включают в себя присваивание, управляющие конструкции и вызовы процедур. Lúa также поддерживает менее распространенные операторы, такие как множественное присваивание и определение локальных переменных.

4.1. Операторы присваивания

Присваивание - это базовое средство изменения значений переменной и поля таблицы:

а = "hello" .. "world" t. n = t. n + 1

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

а, b = 10, 2*х

переменная  а получает значение 10,  а переменная b -  значение 2*х.

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

х, У = У, х                   --поменять местами 'х' и 'у'a[i], a[j] = а [j], а[i]      --поменять местами 'a[i]' и 'а[j]'

Lua всегда приводит количество значений к числу переменных: когда количество значений меньше, чем количество переменных, то Lua дополняет список значений соответствующим количеством nil, а когда количество значений больше, то лишние значения просто отбрасываются:

а, b, с = 0, 1print(а, b, с)                    --> 0 1 nilа, b = а+1, b+1, b+2              -- значение b+2 отбрасываетсяprint(а, b)                       --> 1 2  а, b, с = 0print (а, b, с)                   --> 0 nil nil

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

а, b, с = 0, 0, 0print(а, b, с) --> 0 0 0

В действительности большинство предыдущих примеров в чемто искусственны. Я редко использую множественное присваивание просто для того, чтобы соединить несколько не связанных между собой присваиваний в одну строку. В частности, множественное присваивание не быстрее, чем набор соответствующих одиночных присваиваний. Тем не менее часто нам действительно нужно множественное присваивание. Мы уже видели пример, меняющий две переменные значениями. Более частое использование заключается в получении сразу нескольких значений, возвращенных функцией. Как мы обсудим в разделе 5.1, функция может вернуть сразу несколько значений. В таких случаях обычно используется множественное присваивание, чтобы получить все эти значения. Например, в присваивании a, b=f () вызов f дает два значения: первое из них записывается в  а, а второе - в b

4.2. Локальные переменные и блоки

Кроме глобальных переменных, Lúa также поддерживает и локальные переменные. Мы создаем локальные переменные при помощи оператора local:

j = 10         --глобальная переменнаяlocal i = 1    --локальная переменная

В отличие от глобальных переменных, область действия локальной переменной ограничена блоком, где она была объявлена. Блок - это тело управляющей конструкции, тело функции и блок кода (файл или строка, где переменная была объявлена):

x = 10                     -- локальная в блокеlocal i = 1while i <= x do  local x = i*2            -- локальная внутри блока while  print(x)                 --> 2, 4,6,8,...  i = i + 1endif i > 20 then  local x                  -- локальная внутри “then”  x = 20  print (x + 2)            -- (напечатает 22, если условие выполнится)else  print(x)                 --> 10 (глобальная)endprint(x)                   --> 10 (глобальная)

Обратите внимание, что этот пример не будет работать так, как ожидается, если вы его введете в интерактивном режиме. В интерактивном режиме каждая строка - это самостоятельный блок (за исключением случая, когда строка не является законченной конструкцией). Как только вы введете вторую строку примера (local i=l), Lua выполнит ее и начнет новый блок кода (следующая строка). К тому моменту область действия локальной переменной i уже завершится. Чтобы решить эту проблему, мы можем явно заключить весь этот блок между ключевыми словами do-end. Когда вы введете do, блок закончится, только когда вы введете соответствующий ему end, поэтому Lua не будет пытаться выполнить каждую строку как отдельный блок.

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

do    local а2 = 2*а    local d = (b^2 - 4*а*с)^(1/2)    х1 = (-b + d)/а2    х2 = (-b - d)/a2end -- область действия 'а2' и 'd' заканчивается здесьprint (xl, х2)

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

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

local a, b = 1, 10if a < b then    print(a)       --> 1    local a        -- подразумевается '= nil’    print(a)       --> nilend                -- заканчивает блок, начатый ‘then’print(a, b)        --> 1 10

Распространенной идиомой в Lua является следующая:

local foo = foo

Этот код создает локальную переменную fоо и инициализирует ее значением глобальной переменной fоо.(Локальная переменная foo становится видимой только после этого объявления.) Эта идиома оказывается полезной, когда блоку необходимо сохранить значение оригинальной переменной, если оно изменяется где-то далее в коде; также это ускоряет доступ к этой переменной.

Поскольку многие языки вынуждают декларировать все локальные переменные в начале блока (или процедуры), некоторые считают, что объявлять переменные в середине блока

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

4.3. Управляющие конструкции

Lua предоставляет небольшой и довольно традиционный набор управляющих конструкций, используя if для условного выполнения и while, repeat и for для итерирования. Все управляющие конструкции обладают явным окончанием: end завершает if, for и while, в то время как until завершается repeat.

Условное выполнение управляющей структуры может дать любое значение. Помните о том, что Lúa рассматривает все значения, отличные от false и nil, как истинные. (В частности, Lúa рассматривает ноль и пустую строку как истинные значения.)

if then else

Оператор if проверяет условие и выполнять свою then-часть или свою else-часть соответственно. Часть elseявляется необязательной.

if а < 0 then а = О endif а < b then return a else return b endif line > MAXLINES then    showpage ()    line = 0end

Для записи вложенных операторов if вы можете использовать elseif. Это аналогично else, за которым следует if, но при этом не возникает потребности вомногих end:

if op == "+" then  r = a + belseif op — then  r = a - belseif op == "*" then  r = a*belseif op == "/" then  r = a/belse  error("invalid operation")end

Поскольку в Lua нет оператора switch, то такие конструкции довольно распространены.

while

Как следует из названия, данный оператор повторяет свое тело, пока условие истинно. Как обычно, Lua сперва проверяет условие; если оно ложно, то цикл завершается; в противном случае Lua выполняет тело цикла и повторяет данный процесс.

local i = 1while a[i] do  print(a[i])  i = i + 1end

 repeat

Как следует из названия, оператор repeat-until повторяет свое тело до тех пор, пока условие не станет истинным. Проверка условия осуществляется после выполнения тела цикла, поэтому тело цикла будет выполнено хотя бы один раз.

-- напечатать первую непустую строкуrepeat  line = io.readOuntil line ~= “”print(line)

В отличие от многих языков, в Lua в область действия локальных переменных входит условие цикла:

local sqr = х/2 repeat  sqr = (sqr + x/sqr)/2  local error = math.abs(sqr^2 - x)until error < x/10000 — локальная переменная 'error' видна здесь 

Числовой оператор for

Оператор for существует в двух вариантах - числовой for и общий for.

Числовой оператор for имеет следующий вид:

for var = expl, ехр2, ехрЗ do  end

Этот цикл будет выполнять something для каждого значения var от exp1 до ехр2, используя ехрЗ как шаг для увеличения var. Это третье выражение (ехрЗ) необязательно; когда оно отсутствует, то Lua в качестве шага использует 1. В качестве типичных примеров таких циклов можно рассмотреть

for i = 1, f (х) do print (х) endfor i = 10, 1, -1 do print (i) end

Если вы хотите получить цикл без верхнего предела, то вы можете использовать константу math. huge:

for i = 1, math.huge do  if (0.3*i^3 - 20*i^2 - 500 >= 0) then    print(i)    break  endend

У цикла for есть некоторые тонкости, которые вам лучше знать, чтобы использовать его хорошо. Во-нервых, все три выражения вычисляются только один раз, перед началом цикла. Например, в нашем первом примере Lua выполнит f (х) всего один раз. Во-вторых, управляющая переменная является локальной переменной, автоматически объявленной для оператора for, и она видна только внутри цикла. Типичной ошибкой является мнение, что эта переменная все еще существует после конца цикла:

for i = 1, 10 do print(i) endmax = i — возможно неверно! Здесь 'i' глобальная

Если вам нужно значение управляющей переменной после цикла (обычно когда вы выходите раньше времени из цикла), то вы должны сохранить ее значение в другой переменной:

-- найти значение в спискеlocal found = nilfor i = 1, #a do  If a[i] < 0 then    found = i -- save value of 'i'    break  endendprint(found)

В-третьих, вы не должны никогда менять значение управляющей переменной: эффект подобных изменений непредсказуем. Если вы хотите закончить цикл for для его нормального завершения, используйте break (как мы сделали в предыдущем примере).

Оператор for общего вида

Оператор for общего вида пробегает все значения, возвращаемые итерирующей функцией:

-- напечатать все значения в таблице 't'for k, v in pairs(t) do print(k, v) end

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

Несмотря на свою внешнюю простоту, оператор for общего вида - это очень мощная конструкция языка. Сподходящими итераторами вы можете обойти практически все, что угодно, в легкочитаемой форме. Стандартные библиотеки предоставляют несколько итераторов, позволяющих нам перебирать строки файла (io. lines), пары из таблицы (pairs), элементы последовательности (ipars), слова внутри строки (string . gmatch)  и Т. Д.

Конечно, мы можем писать и свои собственные итераторы. Хотя использование оператора for в общей форме легко, задача написания функции-итератора имеет свои тонкости; мы рассмотрим эту тему позже, в главе 7.

Оператор цикла общего вида имеет две общие особенности с числовым оператором цикла: переменные цикла локальны для тела цикла, и вы никогда не должны записывать в них какие-либо значения.

Рассмотрим более конкретный пример использования оператора for общего вида. Допустим, у вас есть таблица с названиями дней недели:

days = {"Sunday", "Monday", "Tuesday", "Wednesday",        "Thursday", "Friday", "Saturday")

Теперь вы хотите перевести название дня в его положение в неделе. Вы можете обойти всю таблицу в поисках заданного имени. Однако, как вы скоро узнаете, вам редко понадобится искать в Lúa. Более эффективным подходом будет построение обратной таблицы, например revDays, в которой названия дней являются индексами, а значениями являются номера дней.

Эта таблица будет выглядеть следующим образом:

revDays = {["Sunday") = 1, ["Monday") = 2,           ["Tuesday") = 3, ["Wednesday") = 4,           ["Thursday") = 5, ["Friday") = 6,           ["Saturday") = 7}

Тогда все, что вам нужно для того, чтобы найти номер дня, — это обратиться к этой обратной таблице:

х = "Tuesday"print(revDays[х] ) —> 3

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

revDays = {}for k,v in pairs (days) do  revDays[v] = кend

Этот цикл выполнит присваивание для каждого элемента days, где переменная к получает ключ (1, 2, ...), a vполучает значение ("Sunday", "Monday", ... ).

4.4. break, return и goto

Операторы break и return позволят нам выпрыгнуть прямо из блока. Оператор goto позволяет нам перепрыгнуть практически в любое место функции.

Мы используем оператор break для завершения цикла. Этот оператор прерывает внутренний цикл (for, repeatили while), содержащий его. Также он может использоваться для возврата из функции, поэтому вам необязательно использовать оператор return, если вы не возвращаете никакого значения.

Из синтаксических соображений оператор return может быть только последним оператором блока: другими словами, либо последним оператором, либо прямо перед end, else или until. В следующем примере return - последний оператор блока then.

local i = 1while a[i] do  if a[i] == v then return i end  i = i + 1end

Обычно это именно те места, где мы используем return, поскольку любые другие операторы, следующие за ним, никогда бы не выполнились. Иногда на самом деле бывает полезно написать return в середине блока; например, вы можете отлаживать функцию и хотите избежать ее выполнения. В подобных случаях вы можете использовать явный блок do вокруг оператора return:

function foo ()  return                          --« SYNTAX ERROR  -- 'return' is the last statement in the next block  do return end                   -- OK        end

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

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

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

Типичным и хорошо используемым применением оператора goto является эмулирование некоторой конструкции, которую вы узнали из другого языка, но которая отсутствует в Lua, например continue, многоуровневый break, redo и т. и. Оператор continue - это просто переход на метку в конце цикла, операторredo перепрыгивает к началу блока:

while some_condition do  ::redo::  if some_other__condition then goto continue  else if yet_another_condition then goto redo  end     <some code>  ::continue::end

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

while some_condition do  if some_other_condition then goto continue end    local var = something       : :continue: :end

Вы можете подумать, что этот оператор goto перепрыгивает прямо в область действия переменной var. Однако метка continue находится после последнего непустого оператора блока, и поэтому не в области действия переменной var.

Оператор goto также полезен при написании конечных автоматов. В качестве примера листинг 4.1 является примером программы, проверяющей, содержит ли ее ввод четное количество нулей. Существуют более удачные способы написания этой программы, но данный подход весьма полезен, если вы хотите автоматически перевести конечный автомат в код на Lua (подумайте об автоматической генерации кода).

В качестве другого примера рассмотрим простую игру в лабиринт. Лабиринт содержит несколько комнат, в каждой до четырех дверей: север, юг, восток и запад. На каждом таге пользователь вводит направление движения. Если в этом направлении есть дверь, то пользователь входит в соответствующую комнату; иначе программа печатает предупреждение. Целью является дойти от начальной комнаты до конечной комнаты.

Эта игра является типичным автоматом, где текущая комната является состоянием. Мы можем реализовать эту игру, используя один блок для каждой комнаты и оператор goto для перехода из одной комнаты в другую. Листинг 4.2 показывает, как можно написать простейший лабиринт из четырех комнат.

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

Листинг 4.1. Пример конечного автомата с использованием goto

::si:: do  local с = io.read(l)  if c == '0' then goto s2  elseif c == nil then print'ok'; return  else goto si  endend::s2:: do  local c = io.read(l)  if c == '0' then goto si  elseif c == nil then print'not ok'; return  else goto s2  endendgoto si

Листинг 4.2. Игра в лабиринт

goto rooml - начальная комната::rooml:: do  local move = io.read()  if move == "south" then goto room3  elseif move == "east" then goto room2  else    print ("недопустимый ход")    goto room1 - остаемся в этой же комнате  endend::room2:: do  local move = io.rea()  if move == "south" then goto room4  elseif move == "west" then goto room1  else    print ("недопустимый ход")    goto room2  endend::room3:: do  local move = io.readO  if move == "north" then goto rooml  elseif move == "east" then goto room4  else    print("недопустимый ход")    goto гоотЗ  endend::room4:: do  print("Поздравляем, вы выиграли!")end

Упражнения

Упражнение 4.1. Большинство языков с С-подобным синтаксисом не предлагает конструкцию elseif. Почему эта конструкция больше нужна в Lua, чем в других языках?

Упражнение 4.2. Напишите четыре различных способа реализовать безусловный цикл в Lua. Какой из них вам больше нравится?

Упражнение 4.3. Многие считают, что repeat-until используется редко и поэтому не должен присутствовать в минималистических языках вроде Lua, Чты вы думаете об этом?

Упражнение 4.4. Перепишите конечный автомат из листинга 4.2 без использования goto.

Упражнение 4.5. Можете ли вы объяснить, почему в Lua присутствует ограничение на то, что нельзя выпрыгнуть из функции? (Подсказка: как бы вы реализовали данную возможность?)

Упражнение 4.6. Предполагая, что goto может выпрыгнуть из функции, объясните, что программа на листинге 4.3 должна делать. (Попытайтесь рассуждать о метке с использованием тех же самых правил, которые используются для описания области действия локальных переменных.)

Листинг 4.3. Странное (и неверное) использование goto

function getlabel ()  return function () goto LI end  : : L1 : :  return 0endfunction f (n)  if n == 0 then return getlabel()  else    local res = f(n - 1)    print(n)    return res  endendx = f(10)x()

ГЛАВА 5 - Функции

Функции являются главным механизмом абстракции операторов и выражений в Lua. Функции могут выполнять определенное задание (в других языках это часто называется procedure или subroutine) или вычислить и вернуть значения. В первом случае мы используем вызов функции как оператор; во втором случае мы используем его как выражение:

Print 8*9, 9/8)а = math.sin(3) + math.cos(10)print(os.date())

В обоих случаях список аргументов заключен в круглые скобки, обозначающие вызов; если у функции нет аргументов, то мы все равно должны написать () для обозначения вызова функции. Существует специальное исключение из этого правила: если у функции всего один аргумент и этот аргумент, либо литерал (строка символов), либо конструктор таблицы, то круглые скобки необязательны:

print "Hello World"                         print("Hello World")dofile 'a.lua'                              dofile ('a.lua')print [[a multi-line                        print([[a multi-line  message]] message]])              f{x-10, y=20}                               f((x=10, y=20))type{}                                      type ({})

Lua также предлагает специальный синтаксис для объектно-ориентированных вызовов, оператор двоеточие. Выражение вроде о:foo(х) — это просто способ записать  о.foo(о,х), то есть позвать о.foo, добавляя о как дополнительный аргумент. В главе 16 мы обсудим подобные вызовы (и объектно-ориентированное программирование) более подробно.

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

Как мы видели в других примерах, определение функции следует традиционному синтаксису, например как показано ниже:

-- сложить элементы последовательности 'а'function add (а)  local sum = О for i = I, #a do    sum = sum + a [ i]  end  return sumend

В этом синтаксисе определение функции содержит имя (в примере add), список параметров и тело, которое является списком операторов.

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

function f (а, b) print (a, b) end

Она обладает таким поведением:

f(3)                --> 3 nilf(3, 4)             --> 3 4f(3, 4, 5)          --> 3 4 (5 отбрасывается)

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

function incCount (n)  n = n or 1  count = count + nend

У этой функции есть один параметр по умолчанию; если мы вызовем ее incCount() без аргументов, то она увеличит count на единицу. Когда вы вызываете incCount(), Lua сперва инициализирует n значением nil; оператор or возвращает свой второй аргумент, и в результате Lua присваивает переменной n значение 1.

5.1. Множественные результаты

Мало распространенной, но тем не менее очень удобной особеннос­тью Lua является то, что функция может вернуть несколько значений. Некоторые предопределенные функции в Lua возвращают несколько значений. В качестве примера можно взять функцию string.find, которая ищет шаблон в строке. Эта функция возвращает два индек­а, когда находит шаблон: индекс начала шаблона в строке и индекс конца шаблона. Множественное присваивание позволяет программе получить оба результата:

s, е = string.find ("hello Lua users", "Lua")print(s, e)                   --> 7 9

(Обратите внимание, что индекс первого символа строки равен 1.)

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

function maximum (а)  local mi = 1                         -- индекс максимального элемента  local m = a[mi]                      -- максимальное значение  for i = 1, #a do    if a[i] > m then      mi = i; m = a [i]    end  end  return m, miendprint(maximum({8,10,23,12,5}))         --> 23 3

Lua всегда приводит количество значений, возвращенных функци­ей, к обстоятельствам ее вызова. Когда мы вызываем функцию как оператор, то Lua отбрасывает все возвращенные значения. Когда мы используем вызов функции в выражении, то Lua сохраняет только первое значение. Мы получим все значения, только когда вызов функ­ции является последним (или единственным) выражением в списке выражений. В Lua эти списки встречаются в четырех конструкциях: множественное присваивание, аргументы для вызова функции, конст­руктор таблицы и оператор return. Чтобы проиллюстрировать все эти случаи, мы рассмоторим следующие определения функций: Lua всегда приводит количество значений, возвращенных функци­ей, к обстоятельствам ее вызова. Когда мы вызываем функцию как оператор, то Lua отбрасывает все возвращенные значения. Когда мы используем вызов функции в выражении, то Lua сохраняет только первое значение. Мы получим все значения, только когда вызов функ­ции является последним (или единственным) выражением в списке выражений. В Lua эти списки встречаются в четырех конструкциях: множественное присваивание, аргументы для вызова функции, конст­руктор таблицы и оператор return. Чтобы проиллюстрировать все эти случаи, мы рассмоторим следующие определения функций:

function fooO()end             -- ничего не возвращаетfunction fool()return "a" end -- возвращает 1 значениеfunction foo2()return "a","b" end -- возвращает 2 значения

В множественном присваивании вызов функции как последнее (или единственное) выражение использует столько результатов, сколько нужно для соответствия списку переменных:

х,у = foo2()                        -- х="а", у="b"х = foo2()                          -- х="а", "b" отбрасываетсяx,y,z = 10,foo2()                   -- х=10, у="а", z="b"

Если функция не возвращает значения или возвращает, но не так много, как требуется, то в качестве недостающих значений использу­ется nil:

х,у = fooOO                         -- x=nil, y=nilх,у = fool()                        -- х="а", y=nilx,y,z = foo2()                      -- x="a", y="b", z=nil

Вызов функции, который не является последним элементом в списке, дает ровно одно значение:

х, у =foo2(), 20                    -- х="а", у=20х,у = foo0(), 20, 30                -- x=nil, у=20, 30 отбрасывается

Когда вызов функции является последним (или единственным) аргументом другого вызова, то все результаты первого вызова идут как аргументы на вход второго вызова. Мы уже видели примеры этой конструкции с функцией print. Поскольку функция print может получать переменное число аргументов, оператор print(g()) печа­тает все значения, возвращенные функцией g.

print(fooO ())                      -->print(fool ())                      --> aprint(foo2 ())                      --> a  bprint(foo2 (), 1)                   --> a  1print(foo2() .. "x")                --> ax (смотрите далее)

Когда вызов функции foo2 оказывается внутри выражения, Lua приводит число возвращенных значений к одному; поэтому в послед­ней строке конкатенация использует только "а". Если мы запишем f(g(х)) и уf фиксированное число аргументов, то Lua приводит число возвращенных значений к числу аргументов f, как мы уже видели ранее. Конструктор таблицы также использует все значения, возвращен­ные функцией, без каких-либо изменений:

t = {foo()}                         --t = {} (пустая таблица)t = 1(}                             --t = {“a”}t = (fool О }                       --t = {“a”, “b”}

Как всегда, это поведение встречается, только если вызов являет­ся последним выражением в списке; вызовы в любых других местах дают ровно по одному значению:

t = {fooO(), foo2(), 4} -- t[1] = nil, t[2] = "a", t [ 3 ] = 4

Наконец, оператор return f () возвращает все значения, которые вернула f:

function foo (i)  if i == 0 then return foo0()  elseif i == 1 then return fool()  elseif i == 2 then return foo2()  endendprint(foo(1))             --> aprint(foo(2))             --> a bprint(foo(0))             --> (нет значений)print(foo(3))             --> (нет значений)

Вы можете «заставить» вызов вернуть только одно значение, за­ключив его в дополнительную пару круглых скобок:

print((fooO()))           --> nilprint((fool()))           --> aprint((foo2()))           --> a

Будьте внимательны: оператор return не требует скобок вокруг возвращаемого значения. Поэтому выражение вроде return(f(х)) всегда возвращает ровно одно значение, вне зависимости от того, сколько значений возвращает функция f. Иногда это именно то, что вам нужно; иногда нет. Специальной функцией, возвращающей несколько значений, яв­ляется table.unpack. Она получает на вход массив и возвращает все элементы этого массива, начиная с 1:

print(table.unpack{10,20,30}) --> 10 20 30a,b = table.unpack{10,20,30}  -- a=10, b=20, 30 отбрасывается

Важным использованием unpack является обобщенный механизм вызова функции. Обобщенный механизм позволяет вам вызвать лю­бую функцию с любыми аргументами динамически. В ANSI С, напри­мер, не существует способа построить обобщенный вызов. Вы можете объявить функцию, которая получает переменное число аргументов (при помощи stdarg.h), и вы можете вызывать различные функции, используя указатели на функции. Однако вы не можете позвать функ­цию с переменным числом аргументов: каждый раз, когда вы пише­те на С, у вас фиксированное число аргументов, и каждый аргумент имеет фиксированный тип. В Lua если вы хотите позвать функцию - f  с переменным числом аргументов из массива - а, то можете просто написать следующее:

f (table.unpack(а))

Вызов функции unpack возвращает все значения из а, которые ста­новятся аргументами вызова f. Например, рассмотрим следующий вызов:

print (string .find ("hello", "1l") )

Вы можете динамически построить эквивалентный вызов при по­мощи следующего кода:

f = string.findа = {"hello", "11"}print(f(table.unpack(a)))

Обычно unpack использует оператор длины, для того чтобы узнать, сколько элементов следует вернуть, поэтому он работает только с по­следовательностями. Если нужно, то его можно явно ограничить:

print(table.unpack({"Sun", "Mon", "Tue", "Wed"}, 2, 3))             --> Mon Tue

Хотя функция unpack написана на С, мы можем записать ее на Lua, используя рекурсию:

function unpack (t, i, n)  i = i or 1  n = n or #t  if i <= n then    return t[i], unpack(t, i + 1, n)  endend

Первый раз, когда мы вызываем ее с единственным аргумен­том, в i записывается 1, а в n

аписывается длина последовательно­сти. Затем функция возвращает t[1] вместе с всеми результатами unpack(t, 2, n), что, в свою очередь, возвращает t[2] и все резуль­таты вызова unpack(t, 3, n) и т. д., останавливаясь после n элемен­тов.

5.2. Функции с переменным числом аргументов

Функция в Lua может иметь произвольное число аргументов (variadic). Например, мы уже вызывали функциюprint с одним, дву­мя и большим числом аргументов. Хотя print определена в С, мы и на Lua можем писать функции с переменным числом аргументов. В качестве следующего примера функция ниже возвращает сумму всех своих аргументов:

function add (...)  local s = 0  for i, v in ipairs{...} do    s = s + v  end  return sendprint(add(3,4,10,25,12)) --> 54

Три точки (...) в списке параметров обозначают, что эта функция имеет переменное число аргументов. Когда мы вызываем такую функ­цию, Lua собирает все ее аргументы в список; мы называем эти собран­ные аргументыдополнительными аргументами функции. Функция может получить доступ к своим дополнительным опять при помощи трех точек, теперь уже как в качестве выражения. В нашем приме­ре выражение {...} дает массив со всеми собранными аргументами. Функция перебирает элементы этого массива для того, чтобы найти их сумму. Мы называем выражение . . . выражением с переменным числомаргументов (vararg expression). Оно ведет себя как функция, возвра­щающая много значений, возвращая все дополнительные аргументы текущей функции. Например, команда print(...)напечатает все до­полнительные аргументы текущей функции. Аналогично следующая команда создаст две локальные переменные со значениями первых двух дополнительных аргументов (или nil, если таких аргументов нет).

local а, b = ...

На самом деле мы можем имитировать стандартный механизм пе­редачи параметров в Lua, переводя следующую конструкцию

function foo (а, b, с)function foo(...)  local a, b, c = ...

Тем, кому нравится механизм передачи параметров в Perl, это по­нравится.

Функция, показанная ниже, просто возвращает все переданные ар­гументы:

function id (...)return ... end

Следующая функция ведет себя так же, как и foo, за исключением того, что перед ее вызовом она печатает сообщение со всеми передан­ными аргументами:

function fool (...)  print ("calling foo:", ...)  return foo(...)end

Это довольно полезный трюк для отслеживания всех вызовов к за­данной функции. Давайте рассмотрим еще один полезный пример. Lua предоставля­ет отдельные функции для форматирования текста ( string.format) и для записи текста ( io.write ). Довольно просто объединить эти две функции в одну функцию с переменным числом аргументов:

function fwrite (fmt, ...)  return io.write(string.format(fmt, ...))end

Обратите внимание на присутствие параметра fmt перед точками. Функции с переменным числом аргументов могут иметь любое коли­чество фиксированных параметров перед частью с переменным чис­лом параметров. Lua присваивает первые значения этим переменным; остальные (если есть) идут как дополнительные параметры. Ниже мы покажем несколько примеров вызовов и соответствующих парамет­ров:

Вызов                                                        Параметры

fwrite()                 fmt = nil, нет дополнительных параметровfwrite ("a")             fmt = "а", нет дополнительных параметровfwrite("%d%d", 4, 5)     fmt = "%d%d", дополнительные 4 и 5

(Обратите внимание, что вызов fwrite () приведет к ошибке, по­скольку string.format требует строку как свой первый аргумент.) Для обхода всех дополнительных параметров функция может ис­пользовать выражение { .. .} для того, чтобы собрать их всех в табли­цу, как мы это сделали в определении функции add. В редких случаях, когда переданные аргументы могут принимать значение nil, таблица, созданная при помощи не будет настоя­щей последовательностью. Например, не существует способа для того, чтобы по этой таблице узнать, были ли в конце списка аргумен­тов nil’ ы. Для этих случаев Lua предлагает функцию table.pack. Эта функция получает произвольное число аргументов и возвращает но­вую таблицу, содержащую все свои аргументы, как и {...}, но в этой таблице будет дополнительное поле п, содержащее полное число ее аргументов. Следующая функция использует table.pack для того, чтобы среди ее аргументов были значения nil.

function nonils (...)  local arg = table.pack(...)  for i = 1, arg.n do    if arg[i] == nil then return false end  endreturn trueprint(nonils(2,3,nil))            --> false print(nonils(2,3))                --> true print(nonils())                   --> true print(nonils(nil))                --> false

Запомните, однако, что {. . .} быстрее и чище, чем table.pack.

5.3. Именованные аргументы

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

полезно указать параметр по имени. Для того чтобы проиллюстри­ровать это, давайте рассмотрим функциюos.renam e (из библиотеки os), которая переименовывает файл. Довольно часто мы забываем, какое имя идет первым, новое или старое; поэтому мы можем захотеть переопределить эту функцию так, чтобы она получала два именован­ных параметра:

— неверноrename(old="temp.lua", new="templ.lua")

В Lua нет непосредственной поддержки для этого синтаксиса, но мы можем добиться требуемого эффекта путем небольшого синтаксического изменения. Идея заключается в том, чтобы собрать все аргументы в таблицу и использовать эту таблицу как единственный аргумент функции. Специальный синтаксис, который Lua предостав­ляет для вызова функции, с конструктором таблицы как единствен­ным аргументом, поможет нам добиться этого:

rename{old="temp.lua", new="templ.lua"}

Соответственно, мы переопределяем функцию rename только с одним параметром и получаем настоящие аргументы из этого пара­метра:

function rename (arg)return os.rename(arg.old, arg.new)end

Этот способ передачи параметров особенно полезен, когда у функ­ции много аргументов и большинство из них необязательные. Напри­мер, функция, которая создает новое окно в библиотеке GUI, может иметь десятки аргументов, большинство из которых необязательные, и лучше всего их передать, используя имена:

Листинг 5 .1. Функция с именованными необязательными параметрами

function Window (options)  -- check mandatory options  if type(options.title) ~= "string" then    error("no title")  elseif type(options.width) ~= "number" then    error("no width")  elseif type(options.height) ~= "number" then    error("no height")  end  -- everything else is optional  _Window(options.title,          options.x or 0, -- значение no умолчанию          options.у or 0, -- значение no умолчанию          options.width, options.height,          options.background or "white", — значение no умолчанию          options.border -- значение no умолчанию false (nil)         )endw = Window{ x=0, y=0, width=300, height=200,            title = "Lua", background="blue",            border = true          }

Упражнения

Упражнение 5.1. Напишите функцию, которая получает произ­вольное число строк и возвращает их соединенными вместе.

Упражнение 5.2. Напишите функцию, которая получает массив и печатает все элементы этого массива. Рассмотрите преиму­щества и недостатки использования table.unpack в этой функции.

Упражнение 5.3. Напишите функцию, которая получает произ­вольное число значений и возвращает их все, кроме первого.

Упражнение 5.4. Напишите функцию, которая получает массив и печатает все комбинации элементов этого массива.

(Подсказка: вы можете использовать рекурсивную формулу для числа комбинаций:  С(n, m)=С(n-1; m-1) + С(n- 1, m). Для получения всех С(n,m) комбинаций из n элементов в группы размера m вы сперва добавляете первый элемент к ре­зультату и затем генерируете все С( n -1, m -1) комбинаций из оставшихся элементов в оставшихся местах.  Когда n меньше, чем m, комбинаций больше нет. Когда m равно нулю, сущес­твует только одна комбинация, и она не использует никаких элементов.)

ГЛАВА 6 - Еще о функциях

Функции в Lua являются значениями первого класса с соответствующей лексической областью действия.

Что значит, что функции являются «значениями первого класса»? Это значит что в Lua функция - это значение, обладающее тем же правами, что и стандартные значения воде чисел и строк. Мы можем сохранять функции в переменных (локальных и глобальных) и в таблицах, мы можем передавать функции как аргументы и возвращать их из других функций.

Что значит для функций «лексическая область действия»? Это значит, что функции могут обращаться к переменным, содержащим их функции1. Как мы увидим в этой главе, это вроде безобидное свойство дает огромную мощь языку, поскольку позволяет применять в Lua многие могущественные приемы из мира функционального программирования. Даже если вы совсем не интересуетесь функциональным программированием, все равно стоит немного узнать о том, как использовать эти возможности, поскольку они могут сделать вашу программу меньше и проще.

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

a = {p = print}              a.p("Hello World")                --> Hello Worldprint = math.sin                  -- 'print' теперь ссылается на синусa.p(print (1))                    --> 0.841470sin = a.p                         -- 'sin' теперь ссылается на функцию printsin (I0, 20)                      --> 10 20

(Позже мы увидим полезные применения этой возможности.)

Если функции являются значениями, то существуют ли выражения, которые создают функции? Да. В частности, стандартный способ создать функцию в Lua, как, например,

function foo (х) return 2*х end,

это просто пример того, что мы называем синтаксическим сахаром.; это просто более красивый способ написать следующий код:

foo = function (х) return 2*х end

По этому определение функции - это на самом деле оператор (присваивания), который создает значение типа"function" и присваивает его переменной. Мы можем рассматривать выражение function(х) body end как конструктор функции, точно так же, как {} является конструктором таблицы. Мы называем результат выполнения подобных конструкторов анонимной функцией. Хотя мы часто присваиваем функции глобальным переменным, давая им что-то вроде имени, бывают случаи, когда функции остаются анонимными. Давайте рассмотрим несколько примеров.

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

network =,{  {name =,"grauna",          IP= "210.26.30.34"},  {name =,"arraial",         IP = "210.26.30.23"},  {name =,"lua",             IP = "210.26.23.12"},  {name =,"derain",          IP = "210.26.23.20"},}

Если вы хотите отсортировать таблицу но полю name в обратном алфавитном порядке, то вы можете просто записать:

table.sort(network, function (a,b) return (a.name > b.name) end)

Посмотрите, как удобно было использовать анонимную функцию в этом операторе.

Функция, которая получает другую функцию как аргумент, является тем, что мы называем функцией высшего порядка. Функции высшего порядка являются удобным программным механизмом, и использование анонимных функций для создания их функциональных аргументов является большим источником гибкости. Однако запомните, что функции высших порядков не являются чем-то особенным, они просто следствие способности Lua работать с функциями как значениями первого класса.

Для того чтобы проиллюстрировать использование функций высших порядков, мы напишем упрощенное определение часто встречающейся функции высшего порядка, производной. Следуя неформальному определению, производная функции ƒ в точке x-это значение (ƒ(x+d)-ƒ(x))/d, когда d становится бесконечно малой. В соответствии с этим определением мьт можем написать приближенное значение производной следующим образом:

function derivative (f, delta)  delta = delta or le-4  return function (x)    return (f(x + delta) - f(x))/delta  endend

Получив функцию f, вызов derivative(f) вернет приближенное значение ее производной, которое является другой функцией:

с = derivative(math.sin)> print(math.cos(5.2), c (5.2))--> 0.46851667130038               0.46856084325086print(math.cos(10), c(10) )--> -0.83907152907645              -0.83904432662041

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

6.1. Замыкания

Когда мы пишем функцию, заключенную внутри другой функции, то она имеет полный доступ к локальным переменным окружающей ее функции; мы называем это лексической областью действия (lexical scoping). Хотя это правило видимости может показаться очевидным, на самом деле это не так. Лексическая область видимости вместе с функциями, которые являются объектами первого класса, является очень мощной концепцией в языке программирования, но многие языки этого не поддерживают.

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

names = {"Peter", "Paul", "Mary")grades = {Mary = 10, Paul = 7,Peter table.sort (names, function (nl, n2)  return grades[nl] > grades[n2]                    --сравнить оценкиend)

Теперь допустим, что вы хоте создать функцию для решения данной задачи:

function sortbygrade (names, grades)  table.sort(names, function (nl, n2)    return grades[nl] > grades[n2]                  --сравнить оценки  end)end

Интересной особенностью в этом примере является то, что анонимная функция, передаваемая функции sort,обращается к параметру grades, который является локальным для заключающей их функции sortbygrade.Внутри этой анонимной функции grades не является ни глобальной переменной, ни локальной переменной, а тем, что мы называем нелокальной переменной. (По историческим причинам для обозначения нелокальных переменных в Lua также используется термин upvalue.)

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

function newCounter ()  local i = О  return function ()            -- анонимная функция    i = i + 1    return i  endendc1 = newCounter ()print (cl () ) --> 1print (cl () ) --> 2

В этом коде анонимная функция ссылается на нелокальную переменную i для учета значения. Однако к тому времени, как мы позовем анонимную функцию, переменная i уже выйдет из своей области видимости, поскольку функция, которая создала эту переменную (new- Counter), уже завершится. Тем не менее Lua правильно обрабатывает эту ситуацию, используя понятие замыкания (closure). Проще говоря, замыкание - это функция плюс все, что ей нужно для доступа к нелокальным переменным. Если мы снова позовем newCounter, то она создаст новую локальную переменную i, поэтому мы получим повое замыкание, работающее с этой новой переменной:

с2 = newCounter ()print (с2 () }      --> 1print(cl О)         --> 3print (с2 () )      --> 2

Таким образом, c1 и с2 - это разные замыкания одной и той же функции, и каждая использует свою независимо инстанциированную локальную переменную i.

На самом деле в Lua значением является замыкание, а не функция. Функция - это просто прототип для замыкания. Тем не менее мы будем использовать термин «функция» для обозначения замыкания всегда, когда это не будет приводить к путанице.

Замыкания оказываются очень удобным инструментом во многих случаях. Как мы уже видели, они оказываются удобными в качестве аргументов функций высших порядков, таких как sort. Замыкания также полезны для функций, которые строят другие функции, как функция newCounter в нашем примере или же функция для нахождения производной; этот механизм позволяет программам на Lúa использовать продвинутые методы из мира функционального программирования. Замыкания также удобны для различных вызываемых функций(callback). Типичный пример возникает, когда вы создаете различные кнопки в своей библиотеке для создания GUI. У каждой кнопки есть своя функция, которая должна быть вызвана, когда пользователь нажимает на эту кнопку; обычно нужна, чтобы разные кнопки приводили к различным действиям. Например, калькулятору нужно десять кнопок, по одной на каждую цифру. Вы можете их создать с помощью подобной функции:

function digitButton (digit)  return Button{ label = tostring(digit) ,                 action = function ()                   add_to_display(digit)                 end              }end

В этом примере мы предполагаем, что Button - это функция из библиотеки, которая создает новые кнопки; label - это метка кнопки; action — это замыкание, которое нужно позвать, когда кнопка будет нажата. Замыкание может быть заметно спустя длительное время после того, как digitButton выполнилась, и после того, как локальная переменная digit вышла из области своей видимости, но тем не менее замыкание все равно может к ней обращаться.

Замыкания также оказываются полезными в совсем другом случае. Поскольку функции хранятся в обычных переменных, мы можем легко переопределять функции в Lua, включая даже стандартные. Эта возможность является одной из причин, почему Lua столь гибок. Часто, когда вы переопределяете функцию, вам все равно нужна старая функция. Например, вы хотите переопределить функцию sin, чтобы она работала с градусами вместо радиан. Эта новая функция преобразует свой аргумент и затем зовет исходную функцию sin для выполнения работы. Ваш код при этом может выглядеть, как показано ниже;

oldSin = math.sinmath.sin = function (x)  return oldSin(x*math.pi/180)end

Далее приведен немного более аккуратный способ выполнить это переопределение:

do  local oldSin = math.sin  local k = math.pi/180  math.sin = function (x)    return oldSin (x*k)  endend

Теперь мы сохраняем старую версию в локальной переменной; единственный способ обратиться к ней - через новую функцию.

Вы можете использовать этот же подход для создания безопасных окружений, также называемыхпесочницами(sandbox). Безопасные окружения крайне важны при выполнении кода из ненадежных источников, таких как Интернет. Например, чтобы ограничить файлы, функцию io. open, используя замыкания

do  local oldOpen = io.open  local access_OK = function (filename, mode)          end  io.open = function (filename, mode)    if access_OK (filename, mode) then      return oldOpen (filename, mode)    else      return nil, "access denied"    end  endend

Что делает этот пример особенно приятным, так, что после этого переопределения нет абсолютно никакого способа программе позвать исходный open, кроме как через новую версию с контролем. Небезопасная версия хранится в локальной переменной внутри замыкания, не достижима никак снаружи. С этим подходом вы можете строить песочницы для Lua нa самом Lua, получая при этом в качестве плюсов простоту и гибкость. Вместо какого-то универсального решения для всех проблем Lua предоставляет мета-механизм, так что вы можете подогнать свое окружение под ваши цели.

6.2. Неглобальные функции

Очевидным последствием того, что функции являются значениями первого класса, является то, что мы можем сохранять функции не только в глобальных переменных, но также и в локальных переменных и полях таблицы.

Мы уже видели различные примеры функций, хранимых в полях таблиц: большинство библиотек Lua использует этот механизм (например, io. read, math.sin). Для создания подобных функций в Lua нам нужно просто соединить стандартный синтаксис для функций с синтаксисом для таблиц:

Lib = {}Lib.foo = function (х,у) return х + у endLib.goo = function (x,y) return x - у endprint(Lib.foo(2, 3), Lib.goo(2, 3)) —> 5 -1Конечно, мы также можем использовать конструкторы:Lib = {foo = function (х,у) return х + у end,goo = function (х,у) return х - у end}

Более того, Lua также предоставляет еще один синтаксис для подобных функций:

Lib = {}function Lib.foo (x,y) return x + у endfunction Lib.goo (x,y) return x - у end

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

local f = function () endlocal g = function ()      f()               --'f' здесь видна  end

Lua также поддерживает следующий синтаксический сахар для локальных функций:

local function f ()     end

При определении рекурсивных локальных функций возникает тонкость. Наивный подход здесь не работает. Рассмотрим следующее определение:

local fact = function (n)  if n == 0 then return 1  else return n*fact(n-l) -- ошибка  endend

Когда Lua компилирует вызов fact(n-l) в теле функции, то локальная функция fact еще не определена. Поэтому данное определение попытается позвать глобальную функцию fact, а не локальную. Мы можем решить эту проблему, сперва определив локальную переменную и затем уже определяя саму функцию:

local factfact = function (n)  if n == 0 then return 1  else return n*fact(n-l)  endend

Теперь fact внутри функции ссылается на локальную переменную. Ее значение в момент, когда функцию определяют, ничего не значит; к тому моменту, когда функция будет выполняться, она уже получит правильное значение.

Когда Lua «раскрывает» свой синтаксический сахар для локальный функций, она не использует «наивный» путь. Вместо этого определение, как показано ниже:

local function foo () end,

переходит в

local foo; foo = function () end

Поэтому мы можем спокойно использовать этот синтаксис для рекурсивных функций.

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

local f, g - описали локальные переменныеfunction g ()     f () endfunction f ()     g () end

В этом примере для функции f нельзя написать local function f, поскольку в подобном случае Lua создаст новую локальную переменную f, оставив старую (на которую ссылается g) неинициализированной.

6.3. Оптимизация хвостовых вызовов

Другой интересной особенностью функций в Lua является то, что Lua выполняет оптимизацию хвостовых вызовов. (Это значит, что Lua поддерживает оптимизацию хвостовой рекурсии (tail recursion), хотя это здесь и не связано непосредственно с рекурсией, см. упражнение 6.3.)

Хвостовой вызов (tail call) - это фактически goto, выглядящее как вызов функции. Хвостовой вызов случается, когда функция вызывает другую функцию как последнее свое действие. Например, в следующем коде вызов функции g является хвостовым:

function f (х) return g(х) end

После того как f вызовет g, ей больше нечего делать. В подобных ситуациях программе нет необходимости возвращаться в вызывающую функцию, когда вложенный вызов завершился. Поэтому после хвостового вызова программе нет необходимости хранить какую-либо информацию о вызывающей функции на стеке. Когда вызов gзавершается, управление непосредственно переходит к точке, где была вызвана f. Реализации некоторых языков, например интерпретатор Lua, используют этот факт и не выделяют дополнительно места на стеке для хвостового вызова. Мы говорим, что эти реализации поддерживают устранение хвостовых вызовов (tail-call elimination).

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

function foo (n)  if n > 0 then return foo(n - 1) endend

Этот вызов никогда не приведет к переполнению стека.

Тонким моментом в устранении хвостовых вызовов является вопрос о том, что лее является хвостовым вызовом. Для некоторых вполне очевидных кандидатов требование о том, что вызывающая функция больше ничего не делает после вызова, не выполняется. Например, в следующем коде вызов функции g не является хвостовым.

function f(х) g(х) end

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

return д(х) + 1      -- необходимо выполнить сложениеreturn х or д(х)     -- необходимо привести к 1 значениюreturn (д(х))        -- необходимо привести к 1 значению

В Lua только вызов вида return func (args) является хвостовым. Однако и  fune, и ее аргументы могут быть сложными выражениями, поскольку Lua выполнит их перед вызовом. Например, следующий вызов является хвостовым:

return х[i].foo(х[j] + a*b, i + j)

Упражнения

Упражнение 6.1. Напишите функцию integral, которая получает функцию f и возвращает приближенное значение ее интеграла.

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

f = newpoly({3, 0, 1})print(f(0))               --> 1print (f (5))             --> 76print(f(10))              --> 301

Упражнение 6.3. Иногда язык, поддерживающий оптимизацию хвостовых вызовов, называетсяподдерживающим хвостовую рекурсию (properly tail recursive), если оптимизация хвостовых вызовов поддерживается только для рекурсивных вызовов. (Без рекурсивных вызовов максимальная глубина вызовов статически определена.)

Покажите, что это не справедливо в языке типа Lua: напишите программу, которая реализует неограниченную глубину вызовов без использования рекурсии. (Подсказка: см. раздел 8.1.)

Упражнение 6.4. Как мы видели, хвостовой вызов - это замаскированное goto. Используя эту идею, перепишите код для игры в лабиринт ив раздела 4.4, используя хвостовые вызовы. Каждый блок должен стать повой функцией, и каждый goto становится хвостовым вызовом.

ГЛАВА 7 - Итераторы и обобщенный for

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

7.1. Итераторы и замыкания

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

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

В качестве примера давайте напишем простой итератор для списка. В отличие от ipairs, этот итератор не будет возвращать индекс каждого элемента, а только его значение:

function values (t)  local i = О  return function () i = i + 1; return t[i] endend

В этом примере values это фабрика. Каждый раз, когда мы вызываем эту фабрику, она создает новое замыкание (итератор). Это замыкание хранит свое состояние и своих внешних переменных    t и  i. Каждый раз, когда мы вызываем этот итератор, он возвращает следующее значение из списка t. После последнего элемента итератор вернет nil, что обозначает конец итераций.

Мы можем использовать этот итератор в цикле while:

t = {10, 20, 30}iter = values(t)                   -- создаем итераторwhile true do  local element = iter()           -- вызываем итератор  if element == nil then break end  print(element)end

Однако гораздо легче использовать обобщенный оператор for. В конце концов, он был создан именно для подобного итерирования:

t = {10, 20, 30}for element in values(t) doprint(element)end

Обобщенный for выполняет всю закулисную работу для итерирования: он внутри хранит итерирующую функцию, поэтому нам не нужна переменная iter, он вызывает итератор для каждой новой итерации, и он завершает итерирование, когда итератор возвращает nil. (В следующей секции мы увидим, что обобщенный forделает даже больше, чем только это.)

В качестве более продвинутого примера, - листинг 7.1 для перебора всех слов из текущего входного файла. Для такого перебора нам нужно два значения: содержимое текущей строки (переменная line) и где мы находимся внутри этой строки (переменная pos). С этими данными мы можем всегда сгенерировать следующее слово. Основная часть итерирующей функции - это вызов string.find. Этот вызов ищет слово в текущей строке, начиная с текущей позиции. Он описывает «слово», используя шаблон '%w+', которому удовлетворяет один или более алфавитно-цифровых символов. Если этот вызов находит слово, то функция обновляет текущую позицию на первый символ после слова и возвращает это слово(Функция strin g .sub извлекает подстроку из line между заданными позиция­ми; мы детально разберем ее в разделе 21.2.) Иначе итератор читает следующую строку и повторяет поиск. Если больше строк нет, он возвращает nil, чтобы сообщить о конце обхода. Несмотря на его сложность, использование all words крайне просто:

for word in allwords() doprint (word)end

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

Листинг 7.1. Итератор для обхода всех слов из входного файла

function allwords ()  local line = io.read()         -- текущая строка  local pos =1                  -- текущая позиция в строке  return function ()             -- функция-итератор    while line do            -- повторять, пока есть строки      local s, e = string.find (line, "%w+", pos)      if s then      -- нашли слово?        pos = e + 1    -- следующая позиция после слова        return string.sub(line, s, e)  -- вернуть слово      else        line = io.read() -- слово ненайдено; пробуем след, строку        pos =1 -- начинаем с начала строки      end    end    return nil -- больше нет строк, конец обхода  endend

7.2. Семантика обобщенного for

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

Мы видели, что обобщенный for во время цикла хранит итерирующую функцию внутри себя. На самом деле он хранит три значения: итерирующую функцию, неизменяемое состояние (invariant state) и управляющую переменную. Теперь давайте обратимся к деталям.

Синтаксис обобщенного for приводится ниже:

for   in <exp-list> do    end

Здесь var-list - это список из одного или нескольких имен переменных, разделенных запятыми, a exp-list - это список из одного или нескольких выражений, также разделенных запятыми. Часто список выражений состоит из единственного элемента, вызова фабрики итераторов. В следующем коде, например, список переменных - это  k,vа список выражений состоит из единственного элемента pairs (t):

for k, v in pairs(t) do print(k, v) end

Часто список переменных также состоит всего из одной переменной, как в следующем цикле:

for line in io.lines () do  io.write(line, "n")end

Мы называем первую переменную в списке управляющей переменной. В течение всего цикла ее значение не равноnil, поскольку когда она становится равной nil, цикл завершается.

Первое, что делает цикл for, - это вычисляет значения выражений, идущих после in. Эти выражения должны дать три значения, используемые оператором for: итерирующая функция, неизменяемое состояние и начальное значение управляющей переменной. Как и во множественном присваивании, только последний (или единственный) элемент списка может дать более одного значения; и число этих значений приводится к трем, лишние значения отбрасываются, вместо недостающих добавляются nil. (Когда мы используем простые итераторы, фабрика возвращает только итерирующую функцию, поэтому инвариантное состояние и управляющая переменная получают значение nil.)

После этой инициализации for вызывает итерирующую функцию с двумя аргументами: инвариантным состоянием и управляющей переменной. (С точки зрения оператора for, это инвариантное состояние вообще не имеет никакого смысла. Оператор for только передает значение состояния с шага инициализации вызову итерирующей функции.) Затем for присваивает значения, возвращенные итерирующей функцией, переменным, объявленным в списке переменных. Если первое значение (присваиваемое управляющей переменной) равно nil , то цикл завершается. Иначе for выполняет свое тело и снова зовет итерирующую функцию, повторяя процесс.

Более точно конструкция вида

for var_l, ..., var_n in do end

эквивалента следующему коду:

do  local _f, _s, _var =  while true do    local var_l, ... , var_n = _f(_s, _var)    _var = var_l    if _var == nil then break end  endend

Поэтому если наша итерирующая функция - это , неизменяемое состояние s и начальное состояние для управляющей переменной есть , то управляющая переменная будет пробегать следующие значения  и т. д., до тех пор, пока ᵢ не станет равной nil. Если у for есть другие переменные, то они просто получат дополнительные значения, возвращаемые .

7.3. Итераторы без состояния

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

Как мы уже видели, оператор for вызывает итерирующую функцию с двумя аргументами: неизменяемое состояние и управляющая переменная. Итератор без состояния строит следующий элемент цикла, используя только эти два значения. Типичным примером этого итератора является ipairs, который перебирает все элементы массива:

a = {"one", "two", "three")for i, v in ipairs(a) do  print(i, v)end

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

local function iter (a, i)  i = i + 1  local v = a[i]  if v then    return i, v  endendfunction ipairs (a)  return iter, a, 0end

Когда Lua вызывает ipairs(а) для цикла for, она получает три значения: итерирующую функцию iter, а как инвариантное состояние и ноль в качестве начального значения для управляющей переменной. Затем Lua вызывает iter(а,0), что дает 1,а[1] (если только а[1] уже не nil). На следующей итерации вызывается iter(а,1), что возвращает 2, а[2], и т. д. до первого элемента, равного nil.

Функция pairs, которая перебирает все элементы таблицы, похожа, за исключением того, что итерирующая функция - это функция next, которая является стандартной функцией в Lua:

function pairs (t)  return next, t, nilend

Вызов next(t,k), где k - это ключ таблицы t, возвращает следующий ключ в таблице в произвольном порядке, а также связанное с этим ключом значение как второе возвращаемое значение. Вызов next(t,nil) возвращает первую пару. Когда больше нет пар, то next возвращает nil.

Некоторые предпочитают явно использовать next, избегая вызова pairs:

for k, v in next, t doend

Вспомним, что for приводит свой список выражений к трем значениям, в качестве которых здесь выступят next, t и nil; это именно то, что получается при вызове pairs.

Итератор для обхода связного списка является другим интересным примером итератора без состояния. (Как мы уже упомянули, связные списки нечасто встречаются в Lua, но иногда они нам нужны.)

local function getnext (list, node)  if not node then    return list  else    return node.next  endendfunction traverse (list)  return getnext, list, nilend

Здесь мы используем начало списка как инвариантное состояние (второе значение, возвращаемое traverse) и текущий узел в качестве управляющей переменной. Когда итерирующая функция getnext будет первый раз вызвана, node будет равен nil, и поэтому функция вернет list как первый узел. В последующий вызовах node будет уже не равно nil, и поэтому итератор вернет node. next, как и ожидается. Как обычно, использование этого итератора крайне просто:

list = nilfor line in io.lines() do  list = {val = line, next = list)endfor node in traverse(list) do  print(node.val)end

7.4. Итераторы со сложным состоянием

Часто итератору требуется хранить больший объем состояния, чем помещается в переменные инвариантного состояния и управляющей переменной. Простейшим решением является использование замыканий. Альтернативным решением будет запаковать все, что нужно итератору, в таблицу и использовать эту таблицу как инвариантное состояние для цикла. Используя таблицу, итератор можем хранить так много данных, как ему нужно. Более того, он может менять эти данные так, как он хочет. Хотя состояние - все время одна и та же таблица (поэтому она инварианта), содержимое таблицы может меняться на протяжении цикла. Поскольку такие итераторы хранят все свои данные в состоянии, обычно они игнорируют второй аргумент, предоставляемый обобщенным циклом for (переменная цикла).

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

Функция, начинающая цикл, довольно проста. Она должна вернуть итерирующую функцию и начальное состояние:

local iterator                     -- будет определена позжеfunction allwords ()  local state = {line — io.readO, pos = 1}  return iterator, stateend

Основную работу выполняет функция iterator:

function iterator (state)  while state.line do                       -- повторяем, пока есть строки                                            -- ищем следующее слово    local s, е = string.find (state. line, "%w+", state.pos)    if s then                               -- нашли слово?                                            -- обновить положение      state.pos = e + 1      return string.sub(state.line, s, e)    else -- word not found      state.line = io.readO                 -- пробуем следующую строку...      state.pos =1                          -- ... с начала    end  end  return nil                                -- больше строк нет, завершаем циклend

Всегда, когда это возможно, вам следует пытаться написать итераторы без состояния, такие, которые хранят все свое состояние в переменных цикла for. С ними вы не создаете новых объектов, когда начинаете цикл. Если эта модель не подходит, то вам следует попробовать замыкания. Кроме того, это более красиво, замыкание обычно является более эффективным в качестве итератора, чем таблица: во-первых, дешевле создать замыкание, чем таблицу; во-вторых, доступ к нелокальным переменным быстрее, чем доступ к полям таблицы. Позже мы увидим еще один способ писать итераторы, с использованием сопрограмм. Это является наиболее мощным решением, но оно несколько дороже.

7.5. Подлинные итераторы (true iterarators)

Термин «итератор» несколько неточен, поскольку на самом деле итерирует не итератор, а цикл for. Итераторы только предоставляют последовательные значения для итерирования. Может быть, более удачным термином был бы «генератор», но термин «итератор» уже получил широкое распространение в таких языках, как Java.

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

В качестве примера давайте еще раз перепишем итератор allwords с использованием этого подхода:

function allwords (f)  for line in io.lines() do    for word in string.gmatch(line, "%w+") do      f(word)                          -- позвать функцию    end  endend

Для использования этого итератора мы просто должны предоставить тело цикла как функцию. Если мы просто хотим напечатать каждое слово, то мы используем print:

allwords(print)

Часто в качестве тела цикла используется анонимная функция. Например, следующий фрагмент кода считает, сколько раз слово «hello» встречается в файле:

local count = Оallwords(function (w)  if w == "hello" then count = count + 1 endend)print(count)

Та же самая задача, записанная при помощи итераторов ранее рассмотренного стиля, не сильно отличается:

local count = Оfor w in allwords () do  if w == "hello" then count = count + 1 end endprint(count)

Подобные итераторы были очень популярны в старых версиях Lua, когда в языке еще не было оператора for. Как они соотносятся с итераторами ранее рассмотренного стиля? Оба стиля имеют примерно одни и те же накладные расходы: один вызов функции на итерацию. С другой стороны, проще писать итератор с использованием подлинных итераторов (хотя мы можем получить эту же легкость при помощи сопрограмм). С другой стороны ранее рассмотренный стиль более гибкий. Во-первых, он позволяет два и более параллельных итерирования. (Например, рассмотрим случай обхода сразу двух файлов, сравнивая их слово за словом.) Во-вторых, он позволяет использование break и return внутри цикла. С подлинными итераторами return возвращает из анонимной функции, но не из цикла. Поэтому я обычно использую традиционные (то есть рассмотренные ранее) итераторы.

Упражнения

Упражнение 7.1. Напишите итератор f romto - такой, что следующие два цикла оказываются эквивалентными:

for i in fromto(n, m)    endfor i = n, m    end

Можете ли вы реализовать это при помощи итератора без состояния?

Упражнение 7.2. Добавьте параметр шаг к предыдущему упражнению. Можете ли вы по-прежнему реализовать это при помощи итератора без состояния?

Упражнение 7.3. Напишите итератор uniquewords, который возвращает все слова из заданного файла без повторений. (Подсказка: начните с кода allwords на листинге 7.1; используйте таблицу, чтобы хранить все слова, которые вы уже вернули.)

Упражнение 7.4. Напишите итератор, который возвращает все непустые подстроки заданной строки. (Вам понадобится функция string, sub.)

ГЛАВА 8 - Компиляция, выполнение и ошибки

Хотя мы называем Lua интерпретируемым языком, Lua всегда пред- компилирует исходный код в в промежуточную форму перед его выполнением. (На самом деле многие интерпретируемые языки делают то же самое.) Наличие фйала компиляции может звучать странным по отношению к интерпретируемому языку вроде Lua. Однако основной чертой интерпретируемых языков является не то, что они не компилируются, а то, что возможно (и легко) выполнять сгенерированный на лету код. Мы можем сказать, что наличие функции вродеdofile - это то, что позволяет Lua называться интерпретируемым языком.
zatik

Упражнения

Упражнение 8.1. Часто бывает нужно добавить код к началу загружаемого блока. (Мы уже видели пример в этой главе, когда мы добавляли код к return.) Напишите функцию loadwithprefix, которая работает как load, за исключением того, что она добавляет свой дополнительный аргумент к началу загружаемого блока.

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

Упражнение 8.2. Напишите функцию multiload, которая обобщает loadwithprefix, получая на вход список читающих функций, как в следующем примере;

f = multiload("local x = 10;",io.lines("temp", "*L"),"print(x)")

Для приведенного выше примера mult i load должна загрузить блок, эквивалентный конкатенации строки "local. с содержимым файла temp и строки "print (х) ". Как и функция loadwithprefix, данная функция сама ничего конкатенировать не должна.

Упражнение 8.3. Функция string, rep в листинге 8.2 использует алгоритм двоичного умножения (binary multiplication algorithm) для конкатенации п копий заданной строки s. Для любого фиксированного n мы можем создать специализированную версию string, rep, раскрывая цикл в последовательность команд r=r..s и s=s..s. В качестве примера для n=5 мы получаем следующую функцию:

function stringrep__5 (s)local r = ""r = r . . ss = s . . ss = s . . sr = r . . sreturn rend

Напишите функцию, которая для заданного п возвращает функцию stringrep_n. Вместо использования замыкания ваша функция должна построить текст функции на Lua с соответствующими командами r=r..s и s=s..sи затем использовать load для получения итоговой функции. Сравните быстродействие функции string.rep и полученной вами функции.

Упражнение 8.4. Можете ли вы найти такое значение для р, что выражение pcall (pcall, f) вернет false как первое значение.

ГЛАВА 9 - Сопрограммы

Сопрограмма похожа на нить (в смысле многонитевости): это поток выполнения со своим стеком, своими локальными переменными и своим указателем команд (instruction pointer); но он разделяет глобальные переменные и почти все остальное с другими сопрограммами. Основное отличие между нитями и сопрограммами - это то, что программа с несколькими нитями выполняет все эти нити параллельно. Сопрограммы работают совместно: в любой момент времени программа выполняет только одну из своих сопрограмм, и эта выполняемая сопрограмма приостанавливает свое выполнение, только когда явно попросит это.
zatik

Упражнения

Упражнение 9.1. Используйте сопрограммы для того, чтобы преобразовать упражнение 5.4 в генератор для комбинаций, который может быть использован следующим образом:

for с in combinations ({"а", ”b", "с"}» 2) doprintResult(с)end

Упражнение 9.2. Реализуйте и запустите код из предыдущего раздела (невытесняющая многонитевость).

Упражнение 9.3. Реализуйте функцию transfer на Lua. Если подумать о том, что вызовы resume-yeild аналогичны вызову функции и возврату из нее, то эта функция будет как goto: она прерывает текущую сопрограмму и возобновляет любую другую сопрограмму, переданную как аргумент.

(Подсказка: используйте аналог процедуры dispatch для управления вашими сопрограммами. Тогда transferпередаст управление диспетчеру, сообщая о том, какую следующую сопрограмму нужно запустить, и диспетчер вызовет для нее resume).

ГЛАВА 10 - Законченные примеры

Заканчивая введение в язык, мы покажем три простые, но законченные программы. Первая программа решает задачу о восьми королевах. Вторая программа печатает самые часто встречающиеся слова в тексте. Последний пример - это реализация цепи Маркова, описанная Керниганом и Пайком в их книге «The Practice of Programming»(Addison-Wesley, 1999).
zatik

Упражнения

Упражнение 10.1. Измените программу с восьми королевами, чтобы она останавливалась после печати первого решения.

Упражнение 10.2. Альтернативной реализацией задачи о восьми королевах может быть построение всех перестановок чисел от 1 до 8 и проверка, какие из них допустимы. Измените програм-Упражнения

действие новой программы но сравнению со старой?

(Подсказка: сравните полное число перестановок с числом раз, когда исходная программа вызывает функцию 1зр1асеок.)

Упражнение 10.3. Когда мы применяем программу определения самых часто встречаемых слов, то обычно самыми часто встречаемыми словами оказываются короткие неинтересные слова вроде артиклей и предлогов. Измените программу так, чтобы она пропускала слова, состоящие из менее чем четырех букв.

Упражнение 10.4. Обобщите алгоритм цепи Маркова так, чтобы можно было использовать любой размер в качестве длины префикса.

ГЛАВА 11 - Структуры данных

Таблицы в Lua — это не просто структура данных, - это основная и единственная структура данных. Все структуры, которые предлагают другие языки, — массивы, записи, списки, очереди, множества - могут быть представлены в Lúa при помощи таблиц. Более того, таблицы в Lua эффективно реализуют все эти структуры.

В традиционных языках, таких как С и Pascal, мы реализуем большинство структур данных при помощи массивов и списков (где списки = записи + указатели). Хотя мы можем реализовать массивы и списки при помощи таблиц в Lúa (и иногда мы это делаем), таблицы - гораздо более мощные, чем массивы и списки; многие алгоритмы с использованием таблиц становятся почти тривиальными. Например, мы редко используем поиск в Lua, поскольку таблицы предоставляют прямой доступ к значениям различных типов.
zatik

Упражнения

Упражнение 11.1. Измените реализацию очереди так, чтобы оба индекса были бы равны нулю, если очередь пуста.

Упражнение 11.2. Повторите упражнение 10.3, только, вместо того чтобы использовать длину как критерий для отбрасывания слова, теперь программа должна прочесть из специального файла список слов, которые нужно пропускать.

Упражнение 11.3. Измените структуру графа так, чтобы она содержала метку для каждой дуги. Каждая дуга также должна быть представлена при помощи объекта с двумя полями: меткой и узлов, на который она показывает. Вместо множества соседних узлов каждый узел должен содержать список дуг, исходящих из данного узла

строки файла читала два имени узлов и метку (считая, что метка это число).

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

ГЛАВА 12 - Файлы данных и персистентность

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

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

Упражнения

Упражнение 12.1. Измените код из листинга 12.2, чтобы он выравнивал вложенные таблицы.

(Подсказка: добавьте дополнительный параметр функции serialize, содержащий строку выравнивания.)

Упражнение 12.2. Измените код из листинга 12.2 так, чтобы он использовал синтаксис ["key"] =value так, как предложено в разделе 12.1.

Упражнение 12.3. Измените код предыдущего упражнения так, чтобы он использовал синтаксис [ "key" ] =value, только когда это необходимо.

Упражнение 12.4. Измените код предыдущего упражнения так, чтобы он использовал конструкторы всегда, когда это возможно. Например, он должен представить таблицу {14,15,19} как {14,15,19}, а не как {[1] =14, [2] =15, [3] =19}. (Подсказка: начните с сохранения значений ключей 1, 2, ..., пока они не равны nil. Обратите внимание на то, что не нужно их снова сохранять при обходе остальной части таблицы.)

Упражнение 12.5. Подход, заключающийся в отказе от использования конструкторов, при сохранении таблиц с циклами, слишком радикальный. Можно сохранить таблицу в более приятном виде, используя конструкторы в общем случае и затем используя присваивания только для обработки общих таблиц и циклов.

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

ГЛАВА 13 - Метатаблицы и метаметоды

Обычно для каждого значения в Lua есть вполне предсказуемый набор операций. Мы можем складывать числа, соединять строки, вставлять пары ключ-значение в таблицы и т. п. Однако мы не можем складывать таблицы, не можем сравнивать функции и не можем вызвать строку. Если только мы не используем метатаблицы.

Метатаблицы позволяют изменить поведение значения в случае, когда мы сталкиваемся с неожиданной операцией. Например, при помощи метатаблиц мы можем определить, как Lua должн вычислить выражение а+b,где а и b- это таблицы. Когда Lua пытается сложить две таблицы, то он проверяет, есть ли хотя бы в одной из них метатаблица и содержит ли эта метатаблица поле__add. Если Lua находит это поле, то он вызывает соответствующее значение - так называемый метаметод, который должен быть функцией, - для вычисления суммы.
zatik

Упражнения

Упражнение 13.1. Определите метаметод__sub, который возвращает разницу двух множеств. (Множество а-b - это множество всех элементов из а, которые не содержатся в b.)

Упражнение 13.2. Определите метаметод__len так, что #s возвращает число элементов в s.

Упражнение 13.3. Дополните реализацию proxy-таблиц в разделе 13.4 метаметодом__ipairs.

Упражнение 13.4. Другим способом реализации таблиц, доступных только для чтения, является использование функции в качестве метаметода__index. Этот подход делает доступ к таблице более дорогим, но создание таких таблиц более дешевом, так как все таблицы, доступные только для чтения, могут иметь одну общую метатаблицу. Перепишите функцию readonly с использованием данного подхода.

ГЛАВА 14 - Окружение

Lua хранит все свои глобальные переменные в обычной таблице, называемой глобальным окружением (global environment). (Точнее, Lua хранит свои «глобальные» переменные в нескольких окружениях, но мы для простоты будем это вначале игнорировать.) Одним из преимуществ этого подхода является то, что он упрощает внутреннюю реализацию Lua, поскольку нет необходимости в специальной структуре данных для хранения глобальных переменных. Другим преимуществом является то, что мы можем работать с этой таблицей так же, как и с любой другой таблицей. Для упрощения такой работы Lua хранит само окружение в глобальной переменной _G. (Да, _G._G равно _G.) Например, следующий код печатает имена всех глобальных переменных, определенных в глобальном окружении:

for n in pairs(_G) do print (n) end

В этой главе мы увидим несколько полезных методов для работы с окружением.  zatik

Упражнения

Упражнение 14.1. Функция getfield, которую мы определили в начале этой главы, обеспечивает слитком мало контроля, так как она допускает такие поля, как math?sin или string! ! !gsub. Перепишите ее так, чтобы она воспринимала в качестве разделителя только одну точку. (Для этого упражнения вам может понадобиться информация из главы 21.)

Упражнение 14.2. Объясните в деталях, что происходит в следующей программе и какой будет ее вывод.

local foo do  local _ENV = _ENV   function foo () print (X) end end X = 13 _ENV = nil foo ()' X = 0

Упражнение 14.3. Объясните в деталях, что происходит в следующей программе и какой будет ее вывод.

local print = print function foo (_ENV, a)   print(a + b) endfoo((b = 14), 12) foo((b = 10), 1)

ГЛАВА 15 - Модули и пакеты

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

Начиная с версии 5.1, Lua определил набор соглашений для модулей и пакетов (пакет - это набор модулей). Эти соглашения не требуют каких-либо дополнительных возможностей от языка; программисты могут их реализовать, используя то, что мы уже в языке видели: таблицы, функции, метатаблицы и окружения. Программисты могут использовать другие соглашения. Однако другие соглашения могут привести к тому, что нельзя будет использовать чужие модули и свои модули не могут быть использованы в чужих программах.

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

Например, все стандартные библиотеки - это модули. Вы можете использовать математическую библиотеку следующим образом:

local m = require "math"print(m.sin(3.14))

Однако отдельный интерпретатор (доступный в виде командной строки) заранее загружает все стандартные библиотеки при помощи кода, эквивалентного следующему:

math = require "math"string = require "string"

Эта загрузка позволяет нам использовать обычную запись math. sin. Очевидным плюсом от использования таблиц для реализации модулей является то, что мы можем работать с модулями так же, как и с таблицами, и использовать для этого всю силу Lua. В большинстве языков модули не являются значениями первого класса (то есть они не могут быть запомнены в переменных, переданы как аргументы функциям и т. п.), поэтому этим языкам нужны специальные механизмы для каждой возможности, которую они хотят предложить для модулей. В Lúa вы получаете эти возможности бесплатно.

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

local mod = require "mod" mod.foo()

Пользователь может установить любое локальное имя для модуля:

local m = require "mod" m.foo ()

Также можно предоставить альтернативные имена для отдельных функций:

local m = require "mod" local f = mod.foo f()

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

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

-- ПЛОХОЙ кодlocal math = require("math", "degree")

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

-- ПЛОХОЙ кодlocal math = require("math", "degree")-- где-то в другом месте той же программыlocal math = require("math", "radians")

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

local mod = require"mod"mod.init(0, 0)

Если инициализирующая функция возвращает сам модуль, то мы можем писать код вроде следующего:

local mod = require"mod".init(0, 0)

Другой вариант - сделать так, чтобы модуль возвращал функцию для инициализации и уже эта функция возвращала бы таблицу модуля:

local mod = require"mod"(0, 0)

В любом случае помните, что модуль загружается всего один раз; модуль сам должен разрешать инициализации с конфликтами.
zatik

Упражнения

Упражнение 15.1. Перепишите код в листинге 13.1 как отдельный модуль.

Упражнение 15.2. Что случится при поиске библиотеки, если путь содержит фиксированную компоненту (то есть компоненту, не содержащую знака вопроса)? Может ли такое поведение быть полезным?

Упражнение 15.3. Напишите искатель, который одновременно ищет файлы на Lúa и библиотеки на С. Например, путь для этого искателя может быть чем-то вроде:

./?.lua;./?.so;/usr/lib/lua5.2/?.so;/usr/share/lua5.2/?.lua

(Подсказка: используйте package. searchpath для поиска соответствующего файла, затем попытайтесь загрузить его, сначала при помощи loadfile, затем при помощи package, loadlib.)

Упражнение 15.4. Что случится, если вы установите метатаблицу для package.preload при помощи метаметода__index? Может ли это быть полезным?

ГЛАВА 16 - Объектно-ориентированное программирование

Таблица в Lua является объектом более чем в одном смысле. Подобно объектам, у таблицы есть состояние. Подобно объектам, у таблицы есть идентичность (self), которая не зависит от ее значений; в частности, две таблицы с одинаковыми значениями являются разными объектами, объект может иметь разные значения в разные моменты времени. Подобно объектам, у таблиц есть жизненный цикл, который не зависит от того, кто их создал или где они были созданы.

У объектов есть свои методы. У таблиц также могут быть свои методы, как показано ниже:

Account = {balance = 0} function Account.withdraw (v)  Account.balance = Account.balance - v ,end

Это определение создает новую функцию и запоминает ее в поле withdraw объекта Account. Затем мы можем позвать ее, как показано ниже:

Account.withdraw(100.00)

Функция подобного типа - это почти то, что мы называем методом. Однако использование глобального имениAccount внутри функции является плохой практикой. Во-первых, эта функция будет работать только для данного конкретного объекта. Во-вторых, даже для этого объекта ровно до тех пор, пока этот объект записан в этой конкретной глобальной переменной. Если мы изменим имя объекта, то withdraw больше не будет работать:

a, Account = Account, nil a.withdraw(100.00) -- ОШИБКА!

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

Более гибким вариантом является использование получателя операции. Для этого нашему методу понадобится дополнительный аргумент со значением получателя. Этот параметр обычно имеет имя self или this.

function Account.withdraw (self, v)   self.balance = self.balance - v end

Теперь, когда мы вызываем метод, мы должны указать, с каким объектом он должен работать:

al = Account; Account = nil. . . al.withdraw(al, 100.00) -- OK

При использовании параметра self мы можем использовать один и тот же метод для многих объектов:

а2 = {balance=0, withdraw = Account.withdraw}. . . a2.withdraw(a2, 260.00)

Это использование параметра self является ключевым в любом объектно-ориентированном языке. В большинстве объектно-ориентированных языков данный механизм частично скрыт от программиста, поэтому этот параметр не нужно явно объявлять (хотя внутри метод по-прежнему можно использовать - self или this). Lua также может скрывать этот параметр при помощи оператора двоеточие. Мы можем переписать предыдущее определение метода следующим образом:

function Account:withdraw (v)   self.balance = self.balance - v end

Тогда вызов метода будет выглядеть следующим образом:

а:withdraw(100.00)

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

Account = { balance=0,            withdraw = function (self, v)              self.balance = self.balance - v end          }function Account:deposit (v)  self.balance = self.balance + vendAccount.deposit(Account, 200.00)Account:withdraw(100.00)

К данному моменту у наших объектов есть идентичность, состояние и операции над этим состоянием. Им не хватает системы классов, наследования и возможности скрыть свои переменные (состояние). Давайте сначала разберемся с первой задачей: как мы можем создать различные объекты с одинаковым поведением? Например, как мы можем создать несколько счетов?
zatik

Упражнения

Упражнение 16.1. Реализуйте класс stack с методами push, pop, top и isempty.
Упражнение 16.2. Реализуйте класс stackQueue как подкласс stack. Кроме унаследованных методов, добавьте к этому классу метод insertbottom, который вставляет элемент в конец стека. (Этот метод позволяет использовать объекты данного класса как очереди.)
Упражнение 16.3. Другой способ обеспечить закрытость для объектов - это реализовать их с использованием прокси (proxy) (см. раздел 13.4). Каждый объект представлен пустой таблицей (прокси). Внутренняя таблица устанавливает соответствие между этими пустыми таблицами и таблицами, несущими состояние объекта. Эта внутренняя таблица не доступна снаружи, но методы используют ее для перевода своего параметра self на реальную таблицу, с которой они работают. Реализуйте пример с классом Account при помощи этого подхода и рассмотрите его плюсы и минусы.
(С этим подходом есть одна маленькая проблема. Постарайтесь найти ее сами или обратитесь к разделу 17.3, где предлагается ее решение.)

Слабые таблицы и финализаторы

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

Использование сборщика мусора означает, что у Lua нет проблем с циклами. Вам не нужно никаких специальных действий при использовании циклических структур данных; они автоматически освобождаются, как и любые другие данные. Однако иногда даже умному сборщику мусора нужна ваша помощь. Ни один сборщик мусора не позволит вам забыть обо всех проблемах об управлении ресурсами, такими как внешние ресурсы.

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

Упражнения

Упражнение 17.1. Напишите код, для того чтобы проверить, действительно ли Lua использует эфемерные таблицы. (Не забудьте вызвать collectgarbage для сборки мусора.) По возможности проверьте ваш код как в Lua 5.1, так и в Lua 5.2. Упражнение 17.2. Рассмотрим первый пример из разделе 17.6, создающий таблицу с финализатором, печатающим сообщение при вызове. Что произойдет, если программа завершится без вызова сборки мусора? Что случится, если программа вызовет os.exit? Что случится, если программа завершит свое выполнение с ошибкой?

Упражнение 17.3. Пусть вам нужно реализовать кэширующую таблицу для функции, получающей строку и возвращающей строку. Использование слабой таблицы не позволит удалять записи, поскольку слабые таблицы не рассматривают строки как удаляемые объекты. Как вы можете реализовать кэширование в этом случае?

Упражнение 17.4. Объясните вывод следующей программы:

local count = Оlocal mt = {_ge = function () count = count - 1 end}local a = {} for i = 1, 10000 do   count = count + 1   a[i] = setmetatable ({}, mt) endcollectgarbage 0print(collectgarbage"count" * 1024, count) a = nilcollectgarbage ()print(collectgarbage"count" * 1024, count) collectgarbage() print(collectgarbage"count" *   1024, count)

ГЛАВА 18  -  Математическая библиотека

В этой и следующих главах, посвященных стандартной библиотеке, моей целью является не дать полную спецификацию каждой функции, а показать, какую функциональность предоставляет каждая библиотека. Я могу опустить некоторые специфические опции или поведение для ясности изложения. Главной целью является зажечь ваше любопытство, которое затем может быть удовлетворено чтением документации по Lua.
Библиотека math содержит стандартный набор математических функций, таких как тригонометрические (sin, cos, tan, asin, acos и т. п.), экспоненцирование и логарифмирование (exp, log, logio), округление (floor, ceil), min, max, функции для генерации псевдослучайных чисел (random, randomseed) и переменные pi и huge (последнее является наибольшим представимым числом, на некоторых платформах может принимать специальное значениеinf).
Все тригонометрические функции работают с радианами. Вы можете использовать функции deg и rad для перевода между градусами и радианами. Если вы хотите работать с градусами, вы можете переопределить тригонометрические функции:

do  local sin, asin, ... = math.sin, math.asin, ...   local deg, rad = math.deg, math.rad   math.sin = function (x) return sin(rad(x)) end   math.asin = function (x) return deg(asin(x)) endend

Функция math, random генерирует псевдослучайные числа. Мы можем вызывать ее тремя разными способами. Когда мы вызываем ее без аргументов, она возвращает вещественное псевдослучайное число в диапазоне [0,1). Когда мы вызываем ее с единственным аргументом, целым п, то она возвращает псевдослучайное целое число х, лежащее между 1 и п. Наконец, мы можем вызвать ее с двумя целочисленными аргументами I и и, тогда она вернет псевдослучайное целое число, лежащее между I и и.
Вы можете задать «затравку» (seed) для генератора псевдослучайных чисел при помощи функции randomseed; ее единственным числовым аргументом является «затравка». Обычно при начале работы программы генератор псевдослучайных чисел инициализируется некоторым фиксированным значением. Это значит, что каждый раз, когда вы запускаете вашу программу, она генерирует одну и ту же последовательность псевдослучайных чисел. Для отладки это оказывается весьма полезным, но в игре все время будете получать одно и то же. Стандартным приемом для борьбы с этим является использование текущего времени в качестве «затравки» при помощи вызоваmath. randomseed (os . time () ). Функция os . time возвращает число, представляющее текущее время, обычно в виде числа секунд, прошедших с определенной даты.
Функция math, random использует функцию rand из стандартной библиотеки языка С. В некоторых реализациях возвращает числа с не очень хорошими статистическими свойствами. Вы можете обратиться к независимым дистрибутивам в поисках более удачного генератора псевдослучайных чисел. (Стандартная поставка Lúa не включает в себя подобного генератора из-за проблем с авторским правом. Она содержит только код, написанный авторами Lua.)

Упражнения

Упражнение 18.1. Напишите функцию для проверки того, является ли заданное число степенью двойки.
Упражнение 18.2. Напишите функцию для расчета объема конуса по его высоте и углу между его образующей и осью.
Упражнение 18.3. Реализуйте другой генератор псевдослучайных чисел для Lua. Поищите хороший алгоритм в Интернете. (Вам может понадобиться библиотека для побитовых операций.)
Упражнение 18.4. Используя функцию math, random, напишите функцию для получения псевдослучайных чисел с гауссовским распределением.
Упражнение 18.5. Напишите функцию для перемешивания заданного списка. Убедитесь, что все варианты равновероятны.

Библиотека для побитовых операций

Источником постоянных жалоб насчет Lua является отсутствие в нем побитовых операций. Это отсутствие вовсе не случайно. Не так легко помирить побитовые операции с числами с плавающей точкой.
Мы можем выразить некоторые побитовые операции как арифметические операции. Например, сдвиги влево соответствуют умножению на степени двух, сдвиги направо соответствуют делению. Однако у побитовых AND и OR нет таких арифметических аналогов. Они определены для двоичных представлений целых чисел. Практически невозможно расширить их на операции с плавающей точкой. Даже некоторые простые операции теряют смысл. Что должно быть дополнением 0.0? Должно ли это быть равно -1? Или OxFFFFFFFF (что в Lua равно 4 294 967 295, что явно не равно -1)? Или может быть 2^64-1 (число, которое нельзя точно представить при помощи значения типа double)?
Для того чтобы избежать подобных проблем, Lua 5.2 вводит побитовые операции при помощи библиотеки, а не как встроенные в язык операции. Это делает ясным, что данные операции не являются «родными» для чисел в Lua, но они используют определенную интерпретацию для работы с этими числами. Более того, другие библиотеки могут предложить иные интерпретации побитовых операций (например, используя более 32 битов).
Для большинства примеров в этой главе я буду использовать шестнадцатеричную запись. Я буду использовать слово мах для обозначения OxFFFFFFFF (то есть 232-1). В примерах я буду использовать следующую дополнительную функцию:

function printx (х)  print(string.format("0x%X", x)) end

Побитовая библиотека в Lua 5.2 называется bit32. Как следует из имени, она работает с 32-битовыми числами. Поскольку and, or и not являются зарезервированными в Lúa словами, то соответствующие функции названы band, bor и bnot. Для последовательности в названиях функция для побитового исключающего ИЛИ названа bхоr:

printx(bit32.band(OxDF, OxFD) )          --> OxDD printx(bit32.bor (OxDO, OxOD))           --> OxDD printx(bit32.bxor(OxDO, OxFF))           --> 0x2F printx(bit32.bnot(0))                    --> OxFFFFFFFF  Функции band, bor и bxor принимают любое количество аргументов:
printx(bit32.bor(OxA, OxAO, OxAOO))      --> OxAAAprintx(bit32.band(OxFFA, OxFAF, OxAFF))  --> OxAAAprintx(bit32.bxor(0, OxAAA, 0))          --> OxAAAprintx(bit32.bor())                      --> 0x0printx (bit32.band())                    --> OxFFFFFFFFprintx(bit32.bxor())                     --> 0x0

(Они все коммутативны и ассоциативны.)
Побитовая библиотека работает с беззнаковыми целыми числами. В ходе работы любое число, переданное как аргумент, приводится к целому числу в диапазоне 0-мах. Во-первых, неуказанные числа округляются неуказанным способом. Во-вторых, числа вне диапазона 0-мах приводятся к нему при помощи операции остатка от деления: целое n становится n % (2^32). Эта операция эквивалентна получению двоичного представления числа и затем взятию его младших 32 бит. Как и ожидается, -1 становится мах. Вы можете использовать следующие операции для нормализации числа (то есть отображения его в диапазон 0-мах):

printx(bit32.bor(2Л32))                     --> 0x0printx (bit32.band(-1))                     --> OxFFFFFFFF

Конечно, в стандартном Lua легче просто выполнить n % (2^32).
Если явно не указано, все функции в библиотеке возвращают результат, который также лежит в 0-мах. Однако вам следует быть осторожными при использовании результатов побитовых операций в качестве обычных чисел. Иногда Lua компилируется, используя другой тип для чисел. В частности, некоторые системы с ограниченными возможностями используют 32-битовые числа в качестве чисел в Lua. В этих системах мах=-1. Более того, некоторые побитовые библиотеки используют различные соглашения для своих результатов. По операции в качестве числа, будьте осторожны. Избегайте сравнений: вместо х(Мы скоро увидим функцию btest.)  Используйте саму побитовую библиотеку для нормализации констант:

if bit32.or(a, b) == bit32.or(-l) then    <какой-то код>

Побитовая библиотека также определяет операции для сдвига и вращения бит: 1shift для сдвига налево; rshift и arshift для сдвига направо; 1rotate для вращения налево и rrrotate для вращения направо. За исключением арифметического сдвига (arshift), все сдвиги заполняют новые биты нулями. Арифметический сдвиг заполняет биты слева копиями своего последнего бита.

printx(bit32.rshift(OxDF, 4))          --> OxD printx(bit32.lshift(OxDF, 4))          --> OxDFOprintx(bit32.rshift(-1, 28))           --> OxFprintx(bit32.arshift(-1, 28))          --> OxFFFFFFFF printx(bit32.lrotate(OxABCDEFOl, 4))   --> OxBCDEFOlA printx(bit32.rrotate(OxABCDEFOl, 4))   --> OxlABCDEFO

Сдвиг или вращение на отрицательное число бит сдвигает (вращает) в противоположную сторону. Например, сдвиг на -1 бит направо эквивалентен сдвигу на 1 бит влево. Результат сдвига на более чем 31 бит равен 0 или мах, поскольку все исходные биты пропали:

printx(bit32.lrotate(OxABCDEFOl, -4))     --> OxlABCDEFOprintx (bit32.lrotate (OxABCDEFO1, -36))  --> OxlABCDEFO printx (bit32.lshift(OxABCDEFOl, -36))    -->0x0printx(bit32.rshift(-1, 34))               --> 0x0printx (bit32.arshift(-1, 34))             --> OxFFFFFFFF

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

print (bit32 .btest (12, 1))                      --> falseprint(bit32.btest(13, 1))                         --> true

Другой распространенной операцией является извлечение заданных битов из числа. Обычно эта операция включает в себя сдвиг и побитовое AND; побитовая библиотека упаковывает все это в одну функцию. Вызов bit32 . extract (х, f, w) возвращает w бит из х, начиная с бита f:

printx(bit32.extract (OxABCDEFOl, 4, 8))          --> OxFOprintx(bit32.extract(OxABCDEFOl, 20, 12))         --> OxABCprintx(bit32.extract(OxABCDEFOl, 0, 12))          --> OxFOl

Эта операция считает биты, начиная с 0 и до 31. Если третий аргумент (w) не задан, то он считается равным единице:

printx(bit32.extract(OxOOOOOOOF, 0))              --> Oxlprintx(bit32.extract(OxFOOOOOOO, 31))             --> Oxl

Обратной к операции extract является операция replace, которая заменяет заданные биты. Первым параметром является исходное число. Второй параметр задает значение, которое надо вставить. Последние два параметра, f и w, имеют тот же смысл, что и в bit32. extract:

printx(bit32.replace(OxABCDEFOl, 0x55, 4, 8))     --> 0xABCDE551 printx(bit32.replace(OxABCDEFOl, 0x0, 4, 8))      --> OxABCDEOOl

Обратите внимание, что для любых допустимых значений х, f и w выполняется следующее равенство:

assert(bit32.replace(х, bit32.extract (х, f, w), f, w) == x)

Упражнения

Упражнение 19.1. Напишите функцию для проверки того, что заданное число является степенью двух.
Упражнение 19.2. Напишите функцию для вычисления чйсла единичных бит в двоичном представлении числа.
Упражнение 19.3. Напишите функцию для проверки того, является ли двоичное представление числа палиндромом.
Упражнение 19.4. Определите операции сдвига и побитовый AND при помощи арифметических операций Lúa.
Упражнение 19.5. Напишите функцию, которая получает строку, закодированную в UTF-8, и возвращает ее первый символ как число. Функция должна вернуть nil, если строка не начинается с допустимой в UTF-8 последовательности.

Библиотека для работы с таблицами

Библиотека table содержит в себе дополнительные функции, позволяющие работать с таблицами как с массивами. Она предоставляет функции для вставки и удаления элементов из списка, для сортировки элементов массива и для конкатенации всех строк в массиве.

20.1. Функции insert и remove

Функция table. insert вставляет элемент в заданное место массива, сдвигая остальные элементы, для того чтобы освободить место. Например, если t - это массив {10, 20, 30}, то после вызова table, insert (t, 1,15) t будет равен{15, 10, 20, 30}. Специальным (и довольно частым) случаем является вызов insert без указания положения, тогда элемент вставляется в самый конец массива и сдвига элементов не происходит. В качестве примера следующий код читает ввод строку за строкой, запоминая все строки в массиве:

t = {}for line in io.lines() do table.insert(t, line)endprint (#t) —> (число прочтенных строк)

В Lua 5.0 этот прием довольно распространен. В более поздних версиях я предпочитаю использовать t [#t+l] =line, для того чтобы добавить строку к массиву.
Функция table. remove удаляет (и возвращает) элемент из заданного места массива, сдвигая при этом следующие элементы массива. Если при вызове положение внутри массива не было указано, то удаляется последний элемент массива.
При помощи этих двух функций довольно легко реализовать стеки, очереди и двойные очереди. Мы можем инициализировать эквивалентна table. insert (t, х); операция удаления элемента эквивалентна table.remove(t).Вызов table.insert (t, 1, x) добавляет элемент в другой конец соответствующей структуры, а вызов table. remove (t, 1) соответственно удаляет элемент из этого конца. Две последние операции не особенно эффективны, так как они должны перемещать все элементы массива в памяти. Однако поскольку в библиотеке table эти функции реализованы на С, то они не являются слишком дорогими и хорошо работают для небольших массивов (до нескольких сот элементов).

20.2. Сортировка

Другой полезной функцией для работы с массивами является table. sort; мы уже видели ее ранее. Она принимает в качестве аргументов массив и опционально функцию для сравнения. Эта функция принимает на вход два аргумента и должна вернуть true, если первый элемент должен идти перед вторым. Если эта функция не указана, то функция сортировки использует стандартный оператор <’.
Типичная путаница происходит, когда программист пытается отсортировать индексы в таблице. В таблице индексы образуют множество, в котором нет никакого упорядочения. Если вы хотите их отсортировать, то вам надо скопировать их в массив и отсортировать этот массив. Давайте рассмотрим пример. Пусть вы прочли входной файл и построили таблицу, которая для каждого имени функции содержит строку, в которой эта функция была определена: что-то вроде следующего:

lines = {  luaH_set = 10,   luaH_get = 24,   luaH_present = 48,}

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

a = {}for n in pairs(lines) do a[#a + 1] = n end table.sort(a)for n in ipairs(a) do print(n) end

Некоторых это смущает. В конце концов, в Lua в массивах нет никакого упорядочения (массивы - это на самом деле таблицы). Поэтому мы навязываем упорядочение при работе с индексами, которые можно упорядочить. Именно поэтому вам лучше обходить массив при помощи ipairs, а не pairs. Первая из этих функций устанавливает порядок ключей 1, 2, 3, ..., в то время как вторая просто использует произвольный порядок из таблицы.
В качестве более продвинутого решении мы можем написать итератор для обхода таблицы, использующей заданный порядок ключей. Необязательный параметр f задает этот порядок. Этот итератор сначала сортирует ключи в отдельный массив, а затем уже обходит этот массив. На каждом шаге он возвращает ключ и соответствующее значение из исходного массива:

function pairsByKeys (t, f)   local a = {}  for n in pairs(t) do a[#a + 1] = n end    table.sort(a, f)   local i = 0  return function () -- итерирующая функция     i = i + 1    return a[i], t[a[i]]   end end

При помощи этого итератора легко напечатать имена функций в алфавитном порядке:

for name, line in pairsByKeys(lines) do   print(name, line) end

20.3. Конкатенация

В разделе 11.6 мы уже видели функцию table. concat. Она берет на вход список строк и возвращает результат конкатенации всех этих строк. Необязательный второй аргумент задает строку-разделитель. Также есть еще два необязательных аргумента, которые задают индексы первой и последней конкатенируимых строк.
Следующая функция является интересным обобщением table, concat. Она может принимать на вход вложенные списки строк:

function rconcat (1)  if type(l) ~= "table" then return 1 end   local res = {)   for i = 1, #1 do    res[i] = rconcat (1 [i])   end  return table.concat(res) end

Для каждого элемента списка rconcat рекурсивно вызывает себя для обработки вложенных списков. Затем она вызывает table, concat для объединения промежуточных результатов.

print (rconcat {{"а", {" nice''}}, " and", {{" long"}, {” list''}}}) --> a nice and long list

Упражнения

Упражнение 20.1. Перепишите функцию rconcat так, чтобы для нее можно было задать строку-разделитель:

print(rconcat({{{"а", ”b"}, ("с"}}, "d", {}, {"е"}}, ";")--> a;b;c;d;e

Упражнение 20.2. Проблемой table.sort является то, что эта сортировка не является устойчивой (stable sort), то есть элементы, которые сортирующая функция считает равными, могут поменять свой порядок в процессе сортировки. Как можно реализовать устойчивую сортировку в Lua?
Упражнение 20.3. Напишите функцию для проверки того, является ли заданная таблица допустимой последовательностью.

Библиотека для работы со строками

Непосредственные возможности работы со строками интерпретатора Lua довольно ограничены. Программа может создавать строки, соединять их и получать длину строки. Но она не может извлекать подстроки или исследовать их содержимое. Подлинная мощь для работы со строками идет из ее библиотеки для работы со строками.
Библиотека для работы со строками доступна как модуль string. Начиная с Lua 5.1, функции также экспортируются как методы строк (используя метатаблицы). Так, перевод строки в заглавные буквы можно записать как string.upper (s) или s:upper(). Выбирайте сами.
zatik

Упражнения

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

t = split("a whole new world", " ")-- t = {"a", "whole", "new", "world"}

Как ваша функция обрабатывает пустые строки? (В частности, является ли пустая строка пустой последовательностью или последовательностью с одной пустой строкой?)
Упражнение 21.2. Шаблоны '%D' и '[^%d]' эквивалентны. А что насчет шаблонов '[^%d%u]' и '[%d%U]'?
Упражнение 21.3. Напишите функцию для транслитерации. Эта функция получает строку и заменяет каждый символ в этой строке другим символом в соответствии с таблицей, заданной вторым аргументом. Если таблица отображает 'а' в 'b', то функция должна заменить каждое вхождение 'а' на 'b'.Если таблица отображает 'а' в false,то функция должна удалить все вхождения символа 'а' из строки.
Упражнение 21.4. Напишите функцию, которая реверсирует строку в UTF-8.
Упражнение 21.5. Напишите функцию транслитерации для UTF-8.

Библиотека ввода/вывода

Библиотека ввода/вывода предоставляет две различные модели для работы с файлами. Простая модель использует текущий входной и текущий выходной файлы, и все ее операции происходят над этими файлами. Полная модель использует явные указатели на файлы; она опирается на объектно-ориентированный подход, который определяет все операции как методы над указателями на файлы.
Простая модель удобная для простых вещей; мы использовали ее на протяжении всей книги. Но ее недостаточно для более гибкой работы с файлами, например для одновременного чтения или одновременной записи сразу в несколько файлов. Для этого нам нужна полная модель.
zatik

Упражнения

Упражнение 22.1. Напишите программу, которая читает текстовый файл и выводит его строки, отсортированные в алфавитном порядке. Если программа была вызвана без аргументов, то она должна брать данные из стандартного ввода и выводить данные в стандартный вывод. Если программа была вызвана с одним аргументом - именем файла, то она должна прочитать все данные из файла и записать вывод в стандартный вывод. При вызове с двумя аргументами она должна читать из первого файла и писать во второй файл.
Упражнение 22.2. Измените предыдущую программу так, чтобы она запрашивала подтверждение, если для вывода задано имя существующего файла.
Упражнение 22.3. Сравните быстродействие программы на Lúa, которая копирует стандартный ввод на стандартный вывод для следующих случаев:
• Копирование осуществляется побайтно.
• Копирование осуществляется построчно.
• Копирование осуществляется блоками по 8К.
• Копируется сразу весь файл.
Для последнего варианта насколько большим может быть входной файл?
Упражнение 22.4. Напишите программу, которая печатает последнюю строку текстового файла. Постарайтесь избежать чтения всего файла, когда файл большой и по нему можно перемещаться.
Упражнение 22.5. Обобщите предыдущую программу так, чтобы она печатала последние п строк текстового файла. Опять постарайтесь избежать чтения всего файла, когда он большой.

Библиотека функций операционной системы

Библиотека функций операционной системы включает в себя функции для работы с файлами (не чтения и записи), получения текущих даты и времени и ряда других возможностей операционной системы. Она определена в таблице os. Переносимость Lua сказалась на этой библиотеке: поскольку Lua написана на чистом ANSI С, то эта библиотека включает в себя только ту функциональность, которая предоставляется ANSI С. Многие возможности операционной системы, такие как работа с каталогами и сокетами, не входят в этот стандарт, и библиотека их не предоставляет. Существуют другие библиотеки, не включенные в основную поставку, которые предоставляют расширенный доступ к операционной системе. Примерами таких библиотек являются posix, предоставляющая функциональность POSI^C.l, luasocket для работы с сетью и LuaFileSystem для работы с каталогами и атрибутами файлов.
Все, что предоставляет данная библиотека для работы с файлами, - это функции os. rename для изменения имени файла и os. remove для удаления файла.   zatik

Упражнения

Упражнение 23.1. Напишите функцию, которая возвращает дату и время спустя ровно месяц от текущей даты (предполагая стандартное кодирование даты как числа).
Упражнение 23.2. Напишите функцию, которая получает дату и время, представленные как число, и возвращает число секунд, прошедших с начала дня.
Упражнение 23.3. Можете ли вы использовать os. execute для того, чтобы изменить текущий каталог вашей программы на Lua? Почему?

ГЛАВА 24  -  Отладочная библиотека

Отладочная библиотека не даст вам отладчик для Lua, но она предложит все те функции, которые необходимы для написания вашего собственного отладчика. Из соображения быстродействия официальный интерфейс к этим функциям - это С API. Отладочная библиотека в Lua - это способ получить доступ к этим функциям прямо из Lua.
В отличие от других библиотек, вы должны пользоваться данными функциями очень осторожно. Во-первых, часть этой функциональности не отличается хорошим быстродействием. Во-вторых, она нарушает некоторые правила языка, например то, что вы не можете обратиться к локальной переменной вне области ее действия. Довольно часто вам не будет хотеться использовать данную библиотеку в окончательной версии вашей программы.
Отладочная библиотека состоит из функций двух типов: функции для доступа к объектам (introspective functions) и ловушки. Функции для доступа к объектам позволяют изучать различные стороны выполняемой программы, такие как стек вызовов, текущая выполняемая строка, значения и имена локальных переменных. Ловушки позволяют нам отслеживать выполнение программы.
Важным понятием в отладочной библиотеке является уровень в стеке. Уровень в стеке - это число, которое относится к конкретной функции, активной в данный момент: у функции, вызвавшей отладочную библиотеку, уровень 1, функция, которая вызвала эту функцию, имеет уровень 2 и т. д.   zatik

Упражнения

Упражнение 24.1. Почему рекурсия в функции getvarvalue (листинг 24.1) обязательно остановится?
Упражнение 24.2. Измените функцию getvarvalue (листинг 24.1) для работы с различными сопрограммами (подобно другим функциям из отладочной библиотеки).
Упражнение 24.3. Напишите функцию setvarvalue.
Упражнение 24.4. На основе функции getvarvalue напишите функцию getallvars, которая возвращает таблицу со всеми переменными, которые видны в заданном месте (возвращаемая таблица не должна включать в себя переменные окружения, вместо этого она должна наследовать их из исходного окружения).
Упражнение 24.5. Напишите улучшенную версию debug, debug, которая выполняет заданные команды, как если бы они были выполнены в области видимости вызывающей функции. (Подсказка: выполняйте команды в пустом окружении
и используйте в качестве метаметода __index функцию
getvarvalue.)
Упражнение 24.6. Измените предыдущий пример для того, чтобы можно было менять переменные.
Упражнение 24.7. Реализуйте некоторые из предложенных улучшений для профилировщика из раздела 24.3.
Упражнение 24.8. Напишите библиотеку для работы с точками останова (breakpoint). Она должна предлагать как минимум две функции:
setbreakpoint(function, line) —> возвращает handle removebreakpoint(handle)
Точка останова задается функцией и строкой внутри функции. Когда выполнение доходит до точки останова, то следует вызвать debug.debug.
(Подсказка: для простейшей реализации используйте ловушку строки и функцию ловушки, которая проверяет, попали ли мы в точку останова; для улучшения быстродействия мы можем включать эту ловушку,только когда мы находимся внутри интересующей нас функции.)

ГЛАВА 25  -  Обзор С API

Lua - это встраиваемый язык. Это значит, что Lua - это не отдельный пакет, а библиотека, которую мы можем прилинковать к другим приложениям для добавления в них возможностей Lua.
Вы можете удивиться: если Lua - это не отдельная программа, но до сих пор в книге мы использовали Lua именно как отдельную программу. Решением этого вопроса является интерпретатор Lua (выполнимый файл lua). Данный интерпретатор - это маленькое приложение (насчитывающее меньше пятисот строк кода), которое использует библиотеку Lua для того, чтобы реализовать отдельный интерпретатор Lua. Эта программа занимается взаимодействием с пользователем, беря файлы и строки и передавая их библиотеке Lua, которая и выполняет основную работу (такую как запуск кода на Lua).
Эта способность использовать библиотеку для того, чтобы расширить возможности приложения, - это именно то, что делает Lua расширяющим языком. В то же самое время программа, которая использует Lua, может зарегистрировать новые функции в окружении Lua, добавляя тем самым возможности, которые не могли бы быть написаны на самой Lua. Это то, что делает Lua расширяемым языком.
Эти два взгляда на Lua (как на расширяющий язык и как на расширяемый язык) соответствуют двум типам взаимодействия между С и Lua. В первом случае С управляет, a Lua - это просто библиотека. Код на С, соответствующий этому типу взаимодействия, мы называем кодом приложения (application code). Во втором случае управление у Lua, а С - это библиотека. В этом случае код на С называется библиотечным кодом. Оба этих типа кода используют один и тот же API для взаимодействия с Lua, так называемый С API.
С API - это просто набор функций, которые позволяют коду на С взаимодействовать с Lua. Он включает в себя функции для чтения и записи глобальных переменных Lua, для вызова функций на Lua, для выполнения фрагментов кода на Lua, для регистрации функций на С, так что они потом могут вызываться из кода на Lua и т. п. Практически все, что код на Lua может сделать, может быть сделано на С при помощи С API,
С API следует стилю языка С, который заметно отличается от стиля языка Lua. Когда мы программируем на С, то мы следим за типами данных, обработкой ошибок, ошибками выделения памяти и рядом других сложных мест. Большинство функций API не проверяет правильность своих аргументов; это ваша задача - убедиться в том, что аргументы корректны, перед вызовом функции. Если вы допустите ошибку, то, скорее всего, получите ошибку вроде «segmentation fault» или что-то вроде нее вместо красивого сообщения об ошибке. Более того, С API делает упор на гибкости и простоте, зачастую за счет легкости использования. Типичные задачи могут потребовать нескольких вызовов API. Это может быть утомительным, но зато дает вам полный контроль над происходящим.
В соответствии с названием целью этой главы является то что необходимо при использовании Lua из С. Не пытайтесь сейчас понять все детали происходящего. Позже мы на этом остановимся. Однако не забывайте, что вы всегда может найти дополнительную информацию в справочном руководстве по Lua. Более того, вы можете найти некоторые примеры использования API в самой поставке Lua. Отдельный интерпретатор Lua (lua. с) дает примеры кода приложения, в то время как стандартные библиотеки (lmathlib.c, lstrlib.c и т. п.) дают примеры библиотечного кода.
С настоящего момента мы выступаем в роли программистов на С. Когда я говорю о «вас», то я имею в виду именно вас, программирующего на С.
Важной компонентой во взаимодействии между Lua и С является постоянно присутствующий виртуальный стек. Почти все функции API работают со значениями на этом стеке. Весь обмен данными между Lua и С происходит через этот стек. Более того, вы также можете использовать этот стек для хранения промежуточных результатов. Этот стек позволяет решить проблемы с принципиальным отличиями между Lua и С: первое отличие заключается в том, что в Lua есть сборка мусора, в то время как С - явное управление памятью; второе отличие заключается в разнице между динамической типизацией в Lua и статической типизацией в С. Мы обсудим стек более подробно в разделе 25.2.
zatik

Упражнения

Упражнение 25.1. Откомпилируйте и запустите простой отдельный интерпретатор Lúa (листинг 25.1).
Упражнение 25.2. Предположим, что стек пустой. Что будет находиться в стеке после следующей последовательности вызовов?

lua_pushnumber(L, 3.5); lua_pushstring(L, "hello"); lua_pushnil(L) ; lua_pushvalue(L, -2) ; lua_remove(L, 1); lua_insert (L, -2);

Упражнение 25.3. Используйте интерпретатор Lua из листинга 25.1 и функцию stackDump (листинг 25.2) для того, чтобы проверить ваш ответ к предыдущему упражнению.

ГЛАВА 26  -  Расширение вашего приложения

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

Упражнения

Упражнение 26.1. Напишите программу на С, которая читает файл на Lúa, определяющий функцию f, которая берет на вход число и возвращает значение функции от этого числа. Ваша программа должна построить график этой функции. (Вам необязательно использовать графику, вполне подойдет обычный текстовый вид, использующий ' *' для графика.)
Упражнение 26.2. Измените функцию call_va (листинг 26.5) для обработки булевых значений.
Упражнение 26.3. Пусть есть программа, которой необходимо следить за несколькими погодными станциями. Внутри* для представления каждой станции, она использует 4-байтовую строку, и есть конфигурационный файл, который сопоставляет каждой такой строке URL соответствующей станции. Конфигурационный файл на Lúa должен выполнять это сопоставление несколькими разными способами:
• набор глобальных переменных, по одной для каждой станции;
• одна таблица, отображающая строки на URL;
• одна функция, для каждой строки возвращающая URL. Обсудите плюсы и минусы каждого варианта, принимая во
внимание общее число станций, типы пользователей, наличие структуры в URL и т. п.

ГЛАВА 27  -  Вызываем С из Lua

Когда мы говорим, что Lua может вызывать С, это не значит, что Lua может вызвать любую функцию на С. Как мы видели в предыдущей главе, когда С вызывает функцию на Lua, необходимо следовать определенному протоколу передачи аргументов и получения результата. Аналогично, чтобы Lua мог вызвать функцию на С, эта функция должна следовать определенному протоколу для получения своих аргументов и возвращению результатов. Более того, чтобы Lua мог вызвать функцию на С, мы должны зарегистрировать эту функцию, то есть должны передать Lua ее адрес определенным способом.
Когда Lua вызывает функцию на С, он использует такой же самый стек, какой С использует для вызова кода на Lua. Функция на С получает свои аргументы со стека и помещает свои результаты на стек.
Важным понятием здесь является то, что стек - это некоторая структура; у каждой функции есть свой собственный локальный стек. Когда Lua вызывает функцию на С, то первый аргумент всегда будет иметь индекс 1 в этом локальном стеке. Даже когда код на С вызывает код на Lua, который вызывает эту же (или другую) функцию, каждый из этих вызовов будет видеть только свой личный стек с первым аргументом по индексу 1.  zatik

Упражнения

Упражнение 27.1. Напишите на С функцию summation, которая считает сумму переменного числа своих числовых аргументов:

print (summation () )                           --> Оprint(summation(2.3, 5.4))                      --> 7.7print(summation(2.3, 5.4, -34))                 --> -26.3print(summation(2.3, 5.4, {}))--> stdinrl: bad argument #3 to 'summation'(number expected, got table)

Упражнение 27.2. Реализуйте функцию, эквивалентную table, pack из стандартной библиотеки.
Упражнение 27.3. Напишите функцию, которая получает произвольное число параметров и возвращает их в обратном порядке:

print(reverse(1, "hello", 20)) --> 20 hello 1

Упражнение 27.4. Напишите функцию foreach, которая получает на вход таблицу и функцию и вызывает эту функцию для каждой пары ключ-значение в таблице:

foreach({x = 10, у = 20}, print)--> х 10--> у 20

Упражнение 27.5. Перепишите функцию foreach из предыдущего упражнения так, чтобы вызываемая функция могла вызвать yield.
Упражнение 27.6. Создайте модуль на С со всеми функциями из предыдущих упражнений.

ГЛАВА 28  -  Приемы написания функций на С

И официальный API, и вспомогательная (auxiliary) библиотека предоставляют несколько механизмов, помогающих писать функции на С. В этой главе мы рассмотрим механизмы для работы с массивами, строками и сохранения значений Lua в С.
zatik

Упражнения

Упражнение 28.1. Напишите на С функцию filter. Она получает на вход список и функцию и возвращает все элементы из заданного списка, для которых функция возвращает истинное значение:

t = filter ({1, 3, 20, -А, 5}, function (х) return х < 5 end) — t = {1, 3, -4}

Упражнение 28.2. Измените функцию l_split (из листинга 28.2) так, чтобы она могла работать со строками, содержащими нулевой байт. (Кроме остальных изменений, она также должна использовать memchr вместоstrchr.)
Упражнение 28.3. Реализуйте функцию transliterate (упражнение 21.3) на С.
Упражнение 28.4. Реализуйте библиотеку с измененной функцией transliterate так, что таблица замены не передается как аргумент, а хранится самой библиотекой. Ваша библиотека должна предоставить следующие функции:

lib.settrans (table) -- задать таблицу заменыlib.gettrans () -- вернуть таблицу заменыlib. tranliterate (s) — перевести 's', используя текущую таблицу

Используйте реестр для хранения таблицы замены.
Упражнение 28.5. Повторите предыдущее упражнение, используя для хранения таблицы связанное значение.
Упражнение 28.6. Считаете ли вы хорошим дизайном хранить таблицу замены как часть состояния библиотеки, а не передавать ее как параметр?

ГЛАВА 29 - Задаваемые пользователем типы в С

В предыдущей главе мы увидели, как расширить Lua при помощи новых функций, написанных на С. Теперь мы увидим, как расширить Lua при помощи новых типов, заданных на С. Мы начнем с небольшого примера; на протяжении всей главы мы будем расширять его при помощи метаметодов и других возможностей.
Наш пример будет довольно простым: массив логических (булевых) значений. Такая простая структура выбрана потому, что с ней не связано каких-либо сложных алгоритмов и мы можем полностью сконцентрироваться наAPI. Тем не менее этот пример все равно является полезным. Конечно, в Lua мы можем использовать таблицы для реализации массивов логических значений. Ио в реализации на С мы будем использовать по одному биту на каждый элемент, то есть нам потребуется всего около 3% от той памяти, которая бы понадобилась для соответствующей таблицы.
Для нашей реализации нам понадобятся следующие определения:

#include #define BITS_PER_WORD (CHÄR_BIT*sizeof (unsigned int))#define I_WORD(i) ((unsigned int) (i) / BITS_PER_WORD)#define I_BIT(i) (1 « ((unsigned int) (i) % BITS_PER_WORD) )

Константа BITS_OER_WORD - это количество бит в беззнаковом целом числе. Макрос I_WORD вычисляет слово, которое содержит бит по заданному индексу, а макрос I_BIT вычисляет битовую маску для соответствующего бита.
Мы будем представлять наши массивы при помощи следующей структуры:

typedef struct NumArray {   int size;  unsigned int values'[l]; /* изменяемая часть */} NumArray;

Мы объявляем массив values с размером в 1, поскольку С89 не позволяет объявлять массивы с размером 0; на самом деле мы будем выделять необходимое число элементов при выделении памяти под наш массив. Следующее выражение вычисляет итоговый размер для нашего битового массива с п элементами:

sizeof(NumArray) + I_W0RD(n - 1)*sizeof(unsigned int)

(Мы вычитаем единицу из п, поскольку в нашей структуре мы уже выделили место под один элемент.)
zatik

Упражнения

Упражнение 29.1. Измените реализацию setarray так, чтобы она принимала на вход только булевы значения.
Упражнение 29.2. Мы можем рассматривать булевский массив как множество целых чисел (индексы, которым соответствуют истинные значения в массиве). Добавьте к реализации булевых массивов функции, которые вычисляют объединение и пересечение двух массивов. Эти функции должны получать на вход два массива и возвращать новый массив, не изменяя входных массивов.
Упражнение 29.3. Измените реализацию метаметода_tostring
так, чтобы он показывал полное содержимое массива каким- либо образом. Используйте буферы (см. раздел 28.2) для создания итоговой строки.
Упражнение 29.4. На основе примера с булевыми массивами реализуйте библиотеку на С для работы с массивами целых чисел

Управление ресурсами

В нашей реализации булевых массивов из предыдущей главы мы не беспокоились об управлении ресурсами. Эти массивы используют только память. Каждый объект типа userdata, представляющий массив, имеет свой блок памяти, которым управляет Lúa. Когда массив становится мусором (то есть никто не хранит на него ссылок), Lúa со временем соберет его и освободит занимаемую память.
Однако жизнь не всегда так легка. Иногда объекту нужны другие ресурсы, кроме памяти, такие как дескрипторы файлов, указатели на окна и т. п. (часто эти ресурсы также являются памятью, но ей управляет другая часть системы). В подобных случаях, когда объект становится мусором, необходимо как-то освободить эти ресурсы.
Как мы уже видели в разделе 17.6, Lúa предоставляет финализато-
ры в виде метаметода__дс. Для того чтобы показать использование
этого метаметода в С, мы реализуем две библиотеки на С, предоставляющие доступ к внешним ресурсам. Первый пример - это другая реализация функции для обхода содержимого каталога. Второй (и более сложный) пример - это использование библиотеки Expat для разбора XML-файлов.
zatik

Упражнения

Упражнение 30.1. Модифицируйте функцию dir iter так, чтобы она закрывала структуру dir, когда она доходит до конца каталога. При этом изменении программе не нужно ждать сборки мусора для освобождения ресурса, который ей больше не нужен.
(Когда вы закрываете каталог, то вы должны установить адрес, записанный в объекте userdata в null, чтобы сообщить фи- нализатору, что каталог уже закрыт. Также функция dir_iter перед использованием каталога должна проверять, что он не закрыт.)
Упражнение 30.2. В примере с библиотекой 1хр обработчик начала элемента получает таблицу с атрибутами элемента. В этой таблице порядок, в котором элементы были заданы внутри элемента, уже потерян. Как вы можете передать эту информацию в обработчик?
Упражнение 30.3. В примере с библиотекой 1хр мы использовали пользовательское значение для того, чтобы связать таблицу обработчиков с соответствующим объектом userdata, представляющим парсер. Этот выбор создал небольшую проблему, поскольку то, что получают обработчики на С, - это структура lxp_userdata, и эта структура не предоставляет прямого доступа к данной таблице. Мы решили эту проблему путем сохранения таблицы обработчиков на заданном месте в стеке во время разбора каждого фрагмента.
Другим решением может быть связать таблицу обработчиков с объектом userdata при помощи ссылок (раздел 28.3): мы создаем ссылку на таблицу обработчиков и запоминаем эту ссылку (целое число) в структуре 1хр_изегйаСа. Реализуйте этот вариант. Не забудьте освободить ссылку при закрытии парсера.

ГЛАВА 31  -  Нити и состояния

Lua не поддерживает настоящую многонитевость, то есть вытесняющие нити, разделяющие общую память. Для этого есть две причины. Первой причиной является то, что такую поддержку не предоставляет ANSI С, и поэтому нет переносимого способа реализовать эту поддержку в Lua. Второй и более серьезной причиной является то, что мы не думаем, что многонитевость - это хорошая идея для Lua.
Многонитевость была разработана для низкоуровневого программирования. Механизмы синхронизации вроде семафоров и мониторов была предложены для операционных систем (и опытных программистов), а не для приложений. Крайне сложно находить и исправлять ошибки, связанные с многонитевостыо, и некоторые из них могут привести к дырам в безопасности. Также многонитевость может вести к серьезным проблемам с быстродействием из-за необходимости синхронизации в ряде критических мест программы, таких как выделение памяти.
Проблемы с многонитевостыо возникают из-за комбинации вытесняющих нитей и разделяемой памяти, поэтому мы можем избежать их, либо не используя вытесняющих нитей, либо не используя разделяемую память. Lua предлагает поддержку и для того, и для другого. Нити Lua (также известные как сопрограммы) не являются вытесняющими и поэтому избегают проблем, связанных с непредсказуемым переключением нитей. Состояния Lua не имеют общей памяти, поэтому образуют хорошую базу для параллельных вычислений. В этой главе мы рассмотрим оба этих варианта. zatik

Упражнения

Упражнение 31.1. Как мы уже видели, если функция вызывает lua_yield (версия без функции-продолжения), управление передается к функции, которая ее вызвала, когда нить снова продолжит свое выполнение. Какие значения вызывающая функция получит как результаты этого вызова?
Упражнение 31.2. Измените библиотеку lproc так, чтобы она могла посылать другие базовые типы, такие как логические значения и числа. (Подсказка: вам нужно только изменить функцию movevalues.)
Упражнение 31.3. Реализуйте в библиотеке lproc неблокирующую функцию send.

ГЛАВА 32  -  Управление памятью

Lua динамически выделяет все свои структуры данных. Все эти структуры растут динамически по мере надобности и со временем уменьшают свой размер или исчезают.
Lua строго следит за своим использованием памяти. Когда мы закрываем состояние Lua, то Lua явно освобождает всю его память. Более того, все объекты внутри Lua подвержены сборке мусора: не только таблицы и строки, но также функции, нити и модули (поскольку на самом деле они являются таблицами).
Способ, которым Lua управляет памятью, удобен для большинства приложений. Однако для некоторых приложений может понадобиться адаптация, например для работы в условиях ограниченного объема памяти или для сведения задержек от работы сборщика мусора к минимуму. Lua позволяет осуществлять подобные адаптации сразу на двух уровнях. На нижнем уровне мы можем задать функцию, используемую для выделения памяти. На более высоком уровне мы можем задать некоторые параметры для управления сборщиком мусора или можем даже получить прямой контроль над сборщиком мусора. В этой главе мы рассмотрим оба этих варианта.
zatik

Упражнения

Упражнение 32.1. Напишите библиотеку, которая позволит скрипту ограничить общий объем памяти, используемый состоянием в Lua. Она может предоставить единственную функцию setlimit для задания этого максимального объема.
Библиотека должна задать свою собственную выделяющую функцию. Эта функция перед вызовом исходной выделяющей функции проверяет общий объем используемой памяти и возвращает NULL, если запрашиваемый объем превысит максимальное значение.
(Подсказка: эта библиотека lua_gc для инициализации своего счетчика памяти при старте. Она также может использовать пользовательские данные, для того чтобы хранить свое состояние: число байт, текущее максимальное ограничение и т. п. Не забудьте использовать исходный указатель на пользовательские данные при вызове исходной выделяющей функции.)
Упражнение 32.2. Для этого упражнения вам понадобится как минимум один скрипт на Lua, использующий очень много памяти. Если у вас такого нет, то напишите его. (Он может быть очень прост - например, цикл, создающий таблицы.)

• Запустите ваш скрипт с различными параметрами сборщика мусора. Насколько они влияют на быстродействие?
• Что случится, если вы установите параметр pause равный нулю? Что случится, если вы установите его равным 1000?
• Что случится, если вы установите параметр stepmul, равным нулю? Что случится, если вы установите его равным 1 000 000?
• Поправьте ваш скрипт, для того чтобы он получил полный контроль над сборщиком мусора. Он должен держать сборщик мусора остановленным и вызывать его время от времени.

Можете ли вы повысить быстродействие вашего скрипта таким образом?

Translate »