неділя, 18 грудня 2011 р.

lua

Поміж скриптових мов, що плодяться останнім часом як кролики, крім всіх відомих монстрів типу Perl, Python, PHP, існує одна маленька, майже крихітна, при тому зручна та потужна мова -- Lua. Звичайно, вона не призначена замінити, скажімо, Python. Її призначення скромніше. Це може бути написання скриптів для задання поведінки об'єктів програми, наприклад юнітів у іграх. Або конфігурування програми. Можливість задавати конфігурацію не просто числовими параметрами та стрічками, але й за допомогою повноцінної мови програмування - дуже потужна. Поки повіримо на слово, приклади будуть далі :-)
Трішки історії та реклами:
  • Офіційний сайт: http://www.lua.org.
  • Неофіційний сайт із документацією: http://lua-users.org/wiki/.
  • Існування мови розпочалося в 1993, створена командою розробників з католицького університету Ріо-де-Жанейро, Роберто Єрусалимським (Roberto Ierusalimschy), Вольдемаром Целесом (Waldemar Celes) і Луісом Енріке (Luiz Henrique). 
  • "Стандартний" інтерпретатор реалізовано на C, хоча існують реалізації як іншими мовами, так і просто альтернативні, великий список тут: http://lua-users.org/wiki/LuaImplementations.  
  • Ліцензія, починаючи з версії 5.0 - MIT License. В загальних рисах, вона дозволяє робити з кодом майже все що завгодно, поки при його використанні вказується копірайт, а автори не несуть жодної відповідальності. Неофіційний переклад ліцензії українською тут.  
  • Актуальна зараз версія - 5.2, але так як вийшла вона всього кілька днів тому, все нижче стосується версії 5.1.
  • Назва Lua португальською мовою означає Місяць. 
Переваги мови перерахуємо, слідуючи за її авторами:
  • Луа активно використовується в багатьох програмах, починаючи від Adobe's Photoshop Lightroom, та ігор і закінчуючи вбудованими системами. На англомовній вікіпедії для таких ігор навіть створено окрему категорію: "Category:Lua-scripted video games".
  • Луа швидка, одна із найшвидших скриптових мов!
  • Луа портабельна, легко переноситься практично на будь-яку платформу. 
  • Луа "ембедабельна" (embeddable) і може легко бути включена в програму, написану на C, C++, та на багатьох інших мовах (Java, C#, Smalltalk, Fortran, Ada, Erlang, Perl, Ruby). 
  • Луа дуже потужна! Але при тому - крихітна, інтерпретатор із всіма стандартними бібліотеками займає всього кількасот кілобайт.
  • Включає доволі хороший очи́щувач па́м’яті  (garbage collector).
  • Про ліцензію вже згадувалося :-) -- дуже відкрита.
Давно хотів щось про неї написати, так як активно використовую в своїх обчислювальних програмах. А нещодавно і потреба виникла. Постів заплановано декілька - загальний огляд мови, деякі особливості використання, типові ідіоми та трюки, binding (прив'язку) до C/C++ та імпорт сутностей, оголошених в C/C++ програмі (функцій, змінних, класів) у Lua, як стандартним API, так і за допомогою сторонніх засобів (Diluculum, swig). Як воно вийде - побачимо :-)

Що ж це за мова така?

1. Заведення її у себе на машині

Бінарні збірки для вашої платформи, готові для вживання можна взяти тут: Lua Binaries. Зокрема, збірка для Windows: http://luabinaries.sourceforge.net/. Заслуговує уваги і LuaForWindows, що містить стандартний інтерпретатор, та багато-багато корисних бібліотек.  Для любителів мінімалізму можна порекомендувати MurgaLua, яка крім стандартного інтерпретатора, містить прив'язки до fltk, та дозволяє розробляти повноцінні GUI програми за допомогою середовища розміром менше мегабайта, що включає навіть RAD-інструмент FLUID. Для Linux -- звертаєтеся до розповсюджувачів ваших дистрибутивів ;-) В більшості з них Луа присутня в стандартних репозиторіях. Існують також комерційні IDE, наприклад Decoda (shareware, 30 днів, 50$ для малих компаній) та OpenSource плагін для IntelliJ IDE, який працює і з безкоштовним його варіантом -- Lua For IDEA (рекомендовано Murga, автором згаданої вище збірки).

Якщо є бажання скомпілювати самостійно -- теж все просто. Беруться джерельні тексти, розархівовуються та компілюються простою послідовністю команд. Деякі помилки, виявлені з часу останнього релізу, 5.1.4, виправляє наступний патч. Наприклад, з використанням MinGW-MSYS:

    make mingw
    make install INSTALL_TOP=<path-to-install>
    cp src/lua51.dll <path-to-install>/bin

(На жаль, скрипт у версії 5.1.4 "забуває" скопіювати власне dll).

Під іншими платформами - аналогічно, сходу підтримуються:  aix ansi bsd freebsd generic linux macosx mingw posix solaris. Можна скомпілювати і вручну, деталі див. файл INSTALL в пакеті джерельних текстів.
 
2. Перші програми

Луа -- інтерпретована мова. Програма перед виконанням транслюється у байт-код, який потім виконується у віртуальній машині. Найчастіше інтерпретатор так і називається - lua. Якщо його запустити без аргументів командного рядка, він працюватиме в інтерактивному режимі:

та очікуватиме команд користувача. Доступні стандартні команди редагування та історія попередніх команд. 
Якщо ж йому передати ім'я скрипта, наприклад так: lua some_program.lua, то скрипт буде виконано, а інтерпретатор завершить своє виконання (опція -i змусить його після виконання перейти в інтерактивний режим).

Роботу типової програми "Hello world!" ви можете бачити на знімку екрану вище. 

Шматок коду, що виконується інтерпретатором, називають chunk. Дослівно так і перекладається шматок, в принципі. Однак так мені не відомо офіційного перекладу цього терміну для Луа, щоб не було плутанини, цей термін я так і "перекладатиму" - чанк. Це може бути як одна команда, введена в інтерпретаторі, так і цілий файл з програмою. 

Команди в Луа розділяються пробілами. Дозволено додавати крапку з комою в кінці команди, але її присутність не вимагається. Якщо кілька команд знаходяться в одному рядку, краще все ж ";" ставити, для спрощення читання програми та уникнення неоднозначностей. 

Все ж так:
a = 5;  c = 3
гарніше ніж так:
a = 5  c = 3

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

> function pow2(x)
>> return x*x
>> end

>

та зразу і скористатися означеною функцією:

> print(pow2(5))
25
>


Приклади, що наводяться разом із результатом їх роботи, часто зображені так, як вони виглядали б в інтерпретаторі -- із символом ">" перед введеними командами.

3. Основи синтаксису

Цей підрозділ відносно нудний, але без нього - нікуди. Якщо нецікаво, можна спершу глянути в наступні пости -- про C API, та приклади використання, повертаючись сюди в міру необхідності.

Про шматки-чанки та роздільники ми вже говорили вище.

3.1 Ідентифікатори

Ім'я ідентифікатор в Луа може містити великі та малі літери англійського алфавіту, цифри та знак підкреслення, однак не може починатися з цифри. Використовувати ідентифікатори, що починаються з підкреслення чи великої літери не бажано -- вони зарезервовані для внутрішнього використання інтерпретатором. (Зауваження -- допустимі символи залежать від поточної локалі, тому ідентифікатори із іменем "змінна" теж іноді можна використовувати, але не рекомендується!).

Великі та малі літери - різні. Мова чутлива до регістру.

Деякі ідентифікатори, традиційно зарезервовано:

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

3.2 Коментарі 

Вся стрічка після "--" ігнорується. Якщо потрібно закоментувати декілька рядків, використовується пара "--[[" та "]]". Для зручного керування такими коментарями існує зручна ідіома:
--[[
  print("Some text")
--]]
Поставивши "--" перед "]]" ми можемо легко "ввімкнути"/"вимкнути" коментар із того рядка, де він починається. Справді, якщо додати ще один дефіз до "--[[" то і цей рядок і рядок із квадратними дужками, що закінчують коментар, стануть звичайними однорядковими коментарями, а print нормально виконуватиметься.

3.3 Типи даних

Луа -- мова із динамічною типізацією даних. Тобто, змінні не мають типу, але значення, що в них зберігаються -- мають. Існують наступні типи даних:
  • nil -- спеціальний тип, що вказує на відсутність значення. Змінні, яким не присвоювалося значення (тобто такі, що фактично не існують), при звертанні повертають саме його. Присвоєння nil глобальній змінній знищує її.
  • Booleans -- булівський тип, може мати два значення, true або false. Існує приведення інших значень до булівського типу. nil вважається false, будь-яке інше значення - true. Обережно із цим!
  • Numbers -- числа. Всі числа в Луа -- з плаваючою крапкою. Не існує окремих цілих чисел. (Хоча така версія інтерпретатора, де всі числа - цілі, існує). Само по собі це не створює проблем -- арифметичні операції над такими числами на більшості сучасних процесорів такі ж швидкі, як і над цілими, а представлення цілих як числа з плаваючою крапкою не вносить похибок заокруглення, принаймні поки їх розмір залишається в розумних межах (10^14 для типу double)
  • Strings -- стрічки. Саме вони -- послідовності символів :-)  Детальніше про стрічки нижче.
  • Tables -- таблиці, асоціативні масиви. Можуть індексуватися практично будь-якими значеннями. Детальніше про них пізніше.
  • Functions -- функції. Можуть присвоюватися змінним, як і значення інших типів. Можуть повертати декілька значень. Детальніше теж нижче. 
  • Userdata -- об'єкти, про які Луа нічого не знає. Наприклад, передані з С. Типовий приклад -- file handler (згідно http://e2u.org.ua/ -- опрацьо́вувач фа́йлів). З точки зору програміста схожі на таблиці.
  • Thread -- мають відношення до співпрограм (копрограм, coroutines).
Величини будь якого типу можна довільно присвоювати змінним, передавати як аргумент функції, та повертати з них.

Тип змінної можна визначити за допомогою функції type. В зв'язку з особливістю приведення типув Луа, його робота має ряд нюансів:
> a="1"
> print(a,type(a))
1       string
> a=a+7
> print(a,type(a))
8       number

Приклади роботи із різноманітними типами можна побачити тут: Lua Types Tutorial.

3.4 Стрічки (strings)

Можуть містити будь-які восьмибітні символи, включаючи "вбудовані" нулі. (На відміну від ANSI-стрічок). Пам'ять їх керується автоматично.

Обмежуються одинарними або подвійними лапками: "string1", 'string2'. Лапки протилежного типу в стрічці можна використовувати довільно. Допустимими є типові escape-послідовності C: \n, \t, \v, \\, \", \', тощо та коди символів виду \ddd, де d -- десяткові цифри.


> str1="abc"
> print(str1)
abc
> str2='def'

> print(str2)
def
> str3='ab"cd'
> print(str3)
ab"cd
> str4="ef'gh"
> print(str4)
ef'gh
> str5="line1\nline2\tafter tab"
> print(str5)
line1
line2   after tab


Так як стрічки константні, просто так їх змінювати не можна. Можна лише присвоїти нове значення, навіть якщо воно буде модифікованим старим:

> str1="String1"
> str2=string.gsub(str1,"1","2")
> print(str1, str2)
String1 String2


Перетворення стрічок у числа і назад відбувається автоматично (coercion).

> print(3+"5")
8
> print("1"+"4")
5
 

Тому для конкатенації не можна використовувати оператор "+": 

> print(1+"hello")
stdin:1: attempt to perform arithmetic on a string value
stack traceback:
        stdin:1: in main chunk
        [C]: ?

Для  цього служить спеціальний оператор конкатенації "..":

> print(123 .. "hello")
123hello


Пробіли навколо нього обов'язкові, інакше може бути сприйнятий як десяткова крапка.

Довгі літерали можна задати за допомогою конструкції з пари [[ та ]], подібно до коментарів:

> sometext=[[
>> 1
>> 2 3
>> 4 5 6
>> 7 8 9 10
>> ,$%^&
>> <tag>
>> ]]
> print (sometext)
1
2 3
4 5 6
7 8 9 10
,$%^&
<tag>

>


Зверніть увагу на додатковий перевід рядка в кінці виводу -- адже ]] ми поставили з нового рядка. Якщо в літералі трапляються [[, ]], починаючи з Луа 5.1 можна користуватися конструкціями виду: [==[, ]==], де кількість знаків дорівнює має співпадати на початку та в кінці рядка. Цей же трюк можна використовувати і для коментарів.

В Луа 5.1 довжину стрічки можна отримати за допомогою оператора #:

> str="abc"
> print(#str)
3  

Зауваження щодо порівняння: 5 == "5" буде мати значення false! Адже ліворуч число, праворуч стрічка. Автоматичне перетворення тут не спрацьовує. Для повноцінного порівняння потрібно або одну сторону перетворити на число, функцією tonumber, або іншу на стрічку, функцією tostring чи конкатенацією з порожньою стрічкою.

> print(5 == "5")
false
> print(tostring(5) == "5")
true
> print(5 == tonumber("5") )
true
> print(5 .. "" == "5")
true

Стандартна бібліотека string містить багато інших цікавих функцій по роботі із стрічками, ми їх розглянемо пізніше. 

3.5 Таблиці (tables)

Таблиці -- основні (та й єдині) структури даних в Луа. Індексуються чим завгодно, крім nil, за потреби ростуть, пам'ять керується автоматично.

Щоб створити таблицю, використовуємо оператор {}:

> a={}
> a[0]=1
> a[1]=2
> a.field="value"
> a[200]=3
> print(a)
table: 003FC310

Сама змінна, що посилається на таблицю, як бачимо, містить щось подібне до вказівника. Звертатися до елементів таблиці можна за індексами (зокрема, відтворюючи поведінку масиву), в тому числі і такими, що не йдуть послідовно; стрічками; довільними числами, утворюючи асоціативні масиви; іншими таблицями, даючи можливість вкладених структур та багатомірних масивів; тощо.

Ще одна форма оператора {}:

> b={val1=5, val2="value", [5]=7}
> print(b.val1)
5
> print(b.val2)
value
> print(c[5])
7.7

дозволяє зразу ініціалізувати вказані поля.

Таблиця -- динамічний об'єкт, змінні містять лише посилання на них. При цьому неявного копіювання не відбувається.

> a={}
> b=a
> a.fld=1
> print(b.fld);
1

Для зручності a.b та a.["b"] -- синоніми. Зверніть увагу на лапки. a.[b] вказуватиме на елемент з індексом, що зберігається в b. Однак, зрозуміло що всі індекси з 1, "1", "01","+1" вказуватимуть на різні елементи.

Хоча числові індекси в таблицях нічим не обмежені, багато які елементи Луа вважають, що перший елемент масиву має індекс 1 (а не нуль, як в С/С++!).

Для таблиць теж означено оператор розміру #, однак поводитися з ним слід обережно. Формально кажучи, діючи на таблицю t, він повертає число n, таке, що t[n] -- не nil, а t[n+1] -- nil. Зрозуміло, що для звичайного масиву, який починається з індексу 1, буде повернуто рівно кількість елементів. Однак для масивів з початковими індексами, відмінними від 1, та масивів із "дірками", результат цього оператора, взагалі кажучи, не визначений. Хоча у поточній реалізації він просто стартує з індексу 1 та рухається в напрямку його збільшення, поки не зустріне nil. Тому маємо наступну поведінку:

> t1={[1]=5,[2]=6,[3]=7}
> print(#t1)
3

як і очікувалося;

> t1={[0]=0,[1]=5,[2]=6,[3]=7}
> print(#t1)
розрахунок почався з першого індексу, елемент t1[0] не пораховано.

> t1={[-2]=5,[-1]=6,[0]=0}
> print(#t1)
0
Очікуваних індексів просто немає, тому нуль.

> t1={[1]=5,[2]=6,[3]=7, [11]=222}
> print(#t1)
3
За дірки не заглядаємо.

Деякі ідіоми роботи із коректними, з точки зору Луа, масивами:
  • t[#t] звертається до останнього елементу,
  • t[#t]=nil видаляє його (типу pop_back в C++)
  • t[#t+1]="new_val" додає ще один елемент в кінці (типу push_back в C++)
Якщо ви хочете знати максимальний індекс в таблиці -- скористайтеся функцією maxn з стандартної бібліотеки table:

> t2={[1]=5,[2]=6,[3]=7, [200]=555}
> print(table.maxn(t1) )
200

В таблицях можна зберігати і функції, створюючи такі собі простори імен (namespaces):

> our_math={}
> our_math.pow2 = function(x)
>> return x*x;
>> end
> our_math.pow3 = function(x)
>> return x*x*x
>> end
> print(our_math.pow2(5))
25
> print(our_math.pow3(5))
125

Пройтися по всіх елементах таблиці можна за допомогою функції pairs(t):

> t={val1="red", val2="green", val3="blue", [0]=11, [5]=12}
> for k,v in pairs(t) do print(k,v) end
0       11
val1    red
val3    blue
val2    green
5       12
як саме воно працює обговорюватиметься пізніше. Варто лише зауважити, що порядок обходу не є фіксованим, навіть для числових індексів, а додавання елементів під час нього приводить до невизначеної поведінки. Видаляти елементи, присвоюючи їм nil можна безпечно.

Для обходу масивів у звичному порядку, від 1 до n, можна використовувати функцію ipairs:

> t={[1]="val1",[2]=15,[3]=0.16}
> for i,v in ipairs(t) do print(i,v) end
1       val1
2       15
3       0.16

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

3.6 Глобальні та локальні змінні

Глобальні змінні створюються під час присвоєння їм якогось значення, знищуються присвоєнням nil.

Локальні змінні існують лише в блоці, де вони оголошуються. Блок це оператори управління потоком, тіла функцій, чанки коду. Для їх створення використовуємо наступну конструкцію:

local i = 1

Наприклад:

> j=11
> do
>> local j=5
>> local j2=1
>> print(j)
>> print(j2)
>> end
5
1
> print (j)
11
> print(j2)
nil

4. Вирази (Expressions)

З роботою оператора присвоєння ми вже стикалися. Однак в Луа він вміє більше, ніж просто присвоювати змінні ліворуч те, що праворуч:

> a,b = 5,7
> print(a,b);
5       7

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

> a,b=b,a
> print(a,b);
7       5

Кількість аргументів ліворуч та праворуч може не співпадати. Зайві праворуч просто відкидаються, змінним ліворуч, яким не дісталося свого значення, присвоюється nil.

> print(a,b,c)
1       2       3
> a,b,c=5,6
> print(a,b,c)
5       6       nil

Арифметичні оператори теж менш-більш звичні. Це +, -, *, /; піднесення до степеню ^; остача %; зміна знаку числа (унарний мінус) -. Працюють вони над числами з плаваючою крапкою, стрічки намагаються приводити до чисел (coercion).

> print(5+1,7*2, 11/5,11-2, 11%5, -(-5), 2^3,5*"2")
6       14      2.2     9       1       5       8       10

Оператори порівняння:
  • == -- рівність,
  • ~= -- нерівність,
  • < -- менше,
  • > -- більше,
  • <= -- менше або рівно,
  • >= -- більше або рівно.
> print(1==1,1==0,2>1, 2>2,2>=2)
true    false   true    false   true

Приведення типів під час порівняння не працює, змінні різного типу вважають різними, як і різні таблиці. З іншого боку, різні посилання на ту ж таблицю -- рівні.

> print(1=="1")
false
> a={val=1}
> b={val=1}
> print(a==b)
false
> c=b
> print(c==b)
true

Логічні оператори -- or,and, not. Як вже згадувалося, nil та false означають false (хоча і не рівні між собою), все решта -- true. Зокрема, типовий для С нуль вважається true!

> if 0 then
>>  print("True")
>> else
>>  print("False")
>> end 
True

Важливе зауваження -- оператори and i or не обов'язково повертають величину булівського типу. Повертається насправді той аргумент оператора, що остаточно визначає його значення в процесі виконання:

> print(nil and "test1", false and "test2", true and "test3")
nil     false   test3
> print(nil or "test1", false or "test2", true or "test3")
test1   test2   true

Оператор not завжди повертає булівське значення.
З цих  властивостей можна побудувати декілька корисних ідіом.

  • x = x or other_x, що еквівалентно if not x then x = other_x end,
  • (a and b) or c, що еквівалентно С-шному a ? b:c,

Пріоритет операторів наступний:
  1. ^
  2. not # - (unary)
  3. * / %
  4. + -
  5. ..
  6. < > <= >= ~= ==
  7. and
  8. or
5. Оператори (statements)

Про присвоєння, конкатенацію та локальні змінні вже говорилося. Наступний важливий елемент мови -- керівні структури (control structures).

Найважливіша із них -- if. Вживається менш-більш традиційно. Просто if expr then block end:

> if 1 < 2 then
>>
>>   print("Then block")
>> end
Then block
>
> if 1 > 2 then
>>   print("Then block")
>> end
>

З альтернативою if expr then block1 else block2 end:

> if 1 > 2 then
>> print("Then block")
>> else
>> print("Else block")
>> end
Else block

Також є зручна конструкція elseif (особливо незамінна через відсутність switch :-)

> number = 3
> if number < 1 then
>>   value = "smaller than one"
>> elseif number==1 then
>>   value = "one"
>> elseif number==2 then
>>   value = "two"
>> elseif number==3 then
>>   value = "three"
>> else
>>   value = "bigger than three"
>> end
> print(value)
three

(Приклад взято тут: Control Structure Tutorial)

Так як цикл for в Луа не такий простий, почнемо, слідуючи традиційним підручникам Луа, з while

5.1 while

> i=5; j=9
> while i<j do
>> print(i)
>> i=i+1
>> end
5
6
7
8

Як бачимо, його структура типова -- while exp do block end.

5.2 repeat-until

Майже те ж, що і while, але один раз виконуватиметься точно -- перевірка умови відбувається після виконання тіла циклу. Загальний вигляд: repeat block until exp

> i=3
> repeat
>> print(i)
>> i=i-1
>> until i==0
3
2
1

5.3 for - числовий
Виглядає він ось так: for var=exp1,exp2,exp3 do expr end, де exp1 -- початкове значення змінної циклу var, exp2 -- її кінцеве значення, exp3 -- крок інкременту (може бути від'ємним). По замовчуванню вважається що exp3=1.
> for i=1,5 do
>> print(i)
>> end
1
2
3
4
5

> for i=5,1,-1 do
>> print(i)
>> end
5
4
3
2

Всі три вирази, exp1, exp2, exp3, Обчислюються лише раз, до першого виконання тіла циклу.

Змінна циклу локальна для його тіла! Тому невидима після його закінчення.

Змінювати змінну циклу в тілі заборонено -- результат буде непередбачуваним.

5.4 for - загальний

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

tbl={sx="ml", hg="2",[1]=11,[2]=22,[3]=33,wk="test"}

Виконаємо такий цикл:

> for k,v in pairs(tbl) do
>> print(k,v);
>> end
wk      test
2       22
sx      ml
hg      2
3       33
1       11

Проглянули всю таблицю, але порядок, зокрема і числових індексів, менш-більш довільний. При цьому k присвоювався ключ, а v -- значення.
Якщо ми хочемо пройтися лише по елементах з числовим індексом, зате впорядковано, можна скористатися ітератором ipairs (таблиця та ж):

> for k,v in ipairs(tbl) do
>> print(k,v);
>> end
1       11
2       22
3       33

Обхід ipairs починає з елемента номер 1 і продовжує доти, поки елемент з індексом n існує, а з n+1 - ні, рівний nil


Ще пара стандартних ітераторів стосуються роботи з файлами: io.lines() та file:lines(). Різниця між ними в тому, що io.lines() сам відкриватиме файл, а file:lines() користуватиметься вже відкритим, що дає додаткову гнучкість, зокрема і в обробці помилок. Розглянемо їх роботу на прикладі io.lines(), взятому з "For Tutorial". Спочатку створимо файл:

> io.output(io.open("my.txt","w"))
> io.write("This is\nsome sample text\nfor Lua.")
> io.close()

А тепер прочитаємо його по рядках:

> for line in io.lines("my.txt") do print(line) end
This is
some sample text
for Lua.

5.5 Ітератори

Тепер, уявляючи як функціонує загальний for, придивимося уважніше до ітераторів.
Логіку роботи цього циклу, згідно офіційної докуметації, можна виразити так:

for var_1, ···, var_n in explist do block end

відповідає:

do
   local f, state, var = explist
   while true do
      local var_1, ···, var_n = f(state, var)
      var = var_1
      if var == nil then break end
      block
   end
end

Тобто, ітератор повинен повернути:
  1. функцію для отримання наступного значення індексу циклу, 
  2. "стан" ітератора (для pairs чи ipairs це таблиця, з якою працюємо, але може бути і щось зовсім інше, наприклад максимальний номер ітерації, див. приклад далі), 
  3. початкове значення змінної циклу.
Функція f має в ролі аргументу приймати стан ітератора та поточне значення змінної циклу, повертаючи нове значення змінної циклу та довільну кількість інших змінних. Якщо вона повертає nil -- цикл завершується.

Зокрема, pairs, що згадувався вище, повертає функцію next, яка першим аргументом отримує таблицю, другим -- попередній індекс (nil вважається таким, що йде перед першим елементом таблиці) в ній, а повертає наступне значення індексу та значення, що зберігається за цим індексом-ключем. На прикладі використаної вище таблиці tbl:

> print( next(tbl) )
wk      test

> i1 = next(tbl)
> print( next(tbl,i1) )
2       22

Порівняйте з виводом раніше :-)
Як видно, замість for k,v in pairs(tbl) do можна написати  for k,v in next,tbl,nil do:

> for k,v in next,tbl,nil do
>> print(k,v);
>> end
wk      test
2       22
sx      ml
hg      2
3       33
1       11

Напишемо самостійно простий ітератор, який послідовно надає факторіали перших n чисел. Зразу скажу -- реалізація цього ітератора неефективна, так як на кожному кроці факторіал розраховується заново, що дуже нераціонально, однак проста в розумінні.

Функція обчислення факторіалу:

factorial = function (n)
  assert(n>=0, "n should be >= 0")
  assert(math.modf (n)==n, "n should be integer")
  local res = 1
  for i=1,n do
    res = res*i
  end   
  return res
end


Наш ітератор:

function first_n_factorials(n)
    local next_val_fn = function (state,val)
        if state>val
        then
            val = val + 1
            local res=factorial(val)
            return val,res
        else
            return nil
        end
    end
    return next_val_fn,n,0
end

Його аргумент - найбільше число, для якого знаходимо факторіал. Станом ітератора і є це число - n. Функцію, що повертатиме значення наступної ітерації, оголошено всередині:  next_val_fn. Вона перевіряє, чи не досягнули ми ще найбільшого значення. Якщо так, повертає nil, закінчуючи виконання циклу. Якщо ж ні -- обчислює факторіал другого аргументу, повертаючи його після наступного числа, для якого рахуватимемо факторіал. Ітератор повертає цю функцію, стан - максимальне число, та початкове значення -- 0. Зверніть увагу, що виклик  next_val_fn відбудеться до першого виконання тіла циклу.

Подивимося на його роботу:
> for idx,factr in first_n_factorials(5) do
>>      print(idx,factr)
>> end
1       1
2       2
3       6
4       24
5       120

Це приклад так-званого ітератора без стану (stateless iterator). Взагалі, ітератори -- інструмент потужний, але складний, заслуговує окремої розмови. Для розуміння загального for наведеного вище мало б бути достатньо.

5.6 break

Іноді виникає потреба перервати цикл передчасно. Для цього існує спеціальний оператор -- break. Він негайно завершує виконання того, найбільш внутрішнього, циклу, в якому був викликаний. Використання очевидне:

> tbl3={[0]=5,[1]=7,[5]=11,[7]=12}
> for k,v in pairs(tbl3) do
>> if k==5 then break end
>> print(k,v)
>> end
0       5


break мусить бути останнім виразом в блоці. Автори Луа стверджують що по синтаксичним причинам. Часто це виходить природним чином, адже команди після нього не виконуватимуться. Якщо все ж потрібно щоб він знаходився в середні поточного блоку, можна створити для нього окремий блок виду do break end (див. наступний розділ).

5.7 do - end

Іноді потрібно просто окреслити область видимості, скажімо щоб обмежити час життя локальних змінних. Для цього можна використати вираз do expr end. Він вже використовувався вище.

> i=5
> do
>> local i=7
>> print(i)
>> end
7
> print(i)
5

6. Функції

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

Виклик здійснюється типово -- ім'я, після нього, в дужках, через кому, аргументи:

> print("test",5);
test    5

Є один виняток -- якщо функція приймає єдиний аргумент, і він є стрічкою або конструктором таблиці, можна обійтися без дужок:

> print "Test"
Test
> print( type ({a=5}) )
table
> print( type {a=5} )
table


Оголошуються функції теж відносно звично:

> function dupl(x)  return 2*x end
> print(dupl(13))
26

За ключовим словом  function йде її ім'я, потім, в дужках, список аргументів, список команд тіла функції та ключове слово end. Функції є повноцінними  значеннями, їх можна присвоювати змінним, передавати іншим функціям, тощо. Зокрема, запис вище еквівалентний наступному:

dupl = function(x)  return 2*x end

Можна, також, використовувати анонімні функції:
> print( (function(x) return 2*x end)(13)  )
26

При тому, значення функцій є посиланнями, при копіюванні копіюється не код, а саме це  посилання:

> print(dupl,dupl_copy)
function: 003FCB60      function: 003FCB60


Функції можуть бути написаними як на Луа, так і на C, з точки зору Луа-програми відмінності між ними немає.

6.1 Аргументи функцій

Кількість аргументів, в принципі, майже довільна. Список, що є в оголошенні, лише іменує певну кількість перших із них. Іменовані параметри стають локальними змінними, присвоєння яким  списку фактичних аргументів здійснюється так, як звичайне множинне присвоєння (див. вище). Аргументам, що не задані, присвоюється nil, зайві передані параметри відкидаються.

> function triple_args(x,y,z) print(x,y,z) end
> triple_args(1,2,3,4)
1       2       3
> triple_args(1,2)
1       2       nil
> triple_args(1)
1       nil     nil
> triple_args()
nil     nil     nil

Завдяки такій поведінці існує цікава ідіома, реалізація аргументів по замовчуванню:

> function default_arg(x)
x = x or 10
print(2*x)
end
> default_arg(4)
8
> default_arg()
20

Ще одна ідіома, яка напряму не підтримується, але легко реалізується наявними засобами -- іменовані аргументи. Якщо домовитися, що єдиним аргументом функції може бути таблиця, поля якої мають відповідні імена, то завдяки розширеному синтаксису виклику функцій, можна робити ось так:

> function widget(param) 
print("width=", param.width, "height=", param.height) 
end
> widget {width=5,height=11}
width=  5       height= 11

Зверніть увагу -- замість круглих дужок при виклиці функції використано фігурні, які задають конструктор таблиці, з іменованими полями.

Мова підтримує функції із довільною (чи змінною) кількістю аргументів. Увага, описаний нижче механізм стосується лише Луа 5.1! Для 5.0 існують певні відмінності.

Функція, що хоче отримувати змінну, невідому наперед, кількість аргументів, вказує це за допомогою трокрапки (...), після всіх фіксованих аргументів. Доступ до цих змінних здійснюється за допомогою оператора ..., який повертає список із всіма аргументами функції. Зокрема, з них можна зробити таблицю:

local all_varargs={...}

але можна скористатися лише частиною, наприклад першими двома:

local vararg1,vararg2=...
Приклад функції, що підсумовує всі свої аргументи (взято із книги Єрусалимського, автора Луа):

>function add (...)
>> local s = 0 
>>  for i, v in ipairs{...} do
>>    s = s + v
>>  end 
>> return s
>>end
>
> add(1,2,3,4,5)
> print ( add(1,2,3,4,5) )
15
 
За їх допомогою можна, скажімо, організувати журналювання викликів функцій:

> function suspicious_function(x) return x+3 end
> function suspicious_function_trace(...)
>> print("suspicious_function called with: ", ...)
>> suspicious_function(...)
>> end
> suspicious_function_trace(11)
suspicious_function called with:        11
> suspicious_function_trace(11, "Bla-bla-bla")
suspicious_function called with:        11      Bla-bla-bla

У випадку, якщо поміж аргументів можуть траплятися nil, корисною є функція select. Викликана з числом, вона повертає аргументи, з номерами, що слідують після вказаного числа, а з параметром '#' -- кількість аргументів. Якщо нам потрібен тільки один аргумент, просто присвоюємо select(n,...) змінній, решта списку буде відкинуто.

> function test_select(fixed,...)
>> print("fixed arg=",fixed);
>> print( "varargs number=", select('#',...) )
>> local arg1=select(1,...)
>> print( "arg 1=", arg1)
>> local arg2=select(2,...)
>> print( "arg 2=", arg2)
>> end
>
>
> test_select("a")
fixed arg=      a
varargs number= 0
arg 1=  nil
arg 2=  nil
> test_select("a","b")
fixed arg=      a
varargs number= 1
arg 1=  b
arg 2=  nil
> test_select("a","b","c")
fixed arg=      a
varargs number= 2
arg 1=  b
arg 2=  c
> test_select("a","b","c","d")
fixed arg=      a
varargs number= 3
arg 1=  b
arg 2=  c
> test_select("a","b","c","d","e")
fixed arg=      a
varargs number= 4
arg 1=  b
arg 2=  c

 
Луа 5.0 замість ... використовує локальну змінну arg, що створюється автоматично. В ролі select('#',...) можна використовувати arg.n.

6.2 Повернення результатів функції

Функція може повертати як одне, так і декілька значень. Власне, все, перераховане після return, буде повернуто із функції як список. Тобто, якщо ліворуч більше змінних, ніж повернула функція, "зайвим" буде присвоєно nil, якщо менше (вироджений випадок -- жодної) -- надлишок буде відкинуто. Однак так відбувається, якщо функція є єдиним, або останнім елементом списку праворуч від "=". Інакше вона повертає один і тільки один результат.

> function ret0() end
> function ret1() return 1 end
> function ret2() return 1,2 end
> function ret3() return 1,2,3 end
>
> a,b=ret0(); print(a,b)
nil     nil
> a,b=ret1(); print(a,b)
1       nil
> a,b=ret2(); print(a,b)
1       2
> a,b=ret3(); print(a,b)
1       2
>
> a,b,c="a",ret3(); print(a,b,c)
a       1       2
> a,b,c=ret3(),"a"; print(a,b,c)
1       a       nil
> a,b,c="a",ret3(),"b"; print(a,b,c)
a       1       b
> a,b,c="a",ret0(),"b"; print(a,b,c)
a       nil     b

Оператор виду return f() повертає всі результати виклику функції f.
Дужки навколо виклику функції вказують повертати лише один аргумент:

> print((ret0()))
nil
> print((ret1()))
1
> print((ret2()))
1
> print((ret3()))
1
6.3 Елементи функціонального програмування

Як вже говорилося, функції є повноцінними значеннями, їх можна передавати як аргументи. Завдяки цьому існують функції вищих порядків, які отримують в ролі аргумента функції. Приклад такої функції -- sort із стандартної бібліотеки, яка отримує як аргумент, функцію порядку, що має по парі аргументів визначити, котрий перший, а котрий другий. 

> ts={5,1,4,2,7}
> for k,v in pairs(ts) do print(v) end
5
1
4
2
7
> table.sort(ts, function(x,y) return x<y end)
> for k,v in pairs(ts) do print(v) end
1
2
4
5
7
> ts={5,1,4,2,7}
> table.sort(ts, function(x,y) return x>y end)
> for k,v in pairs(ts) do print(v) end
7
5
4
2
1

Крім того, завдяки lexical scoping (як то перекласти?), функції доступні змінні, з блоку, в якому вона оголошена. Що важливо, Луа реалізує концепцію замикання (Closure), тобто, поки існує функція, існує і її контекст, потрібні їй змінні з блоку, в якому вона була оголошена, навіть якщо сам цей блок вже не існує. Такі змінні називаються нелокальними, на противагу локальним та глобальним. Приклад -- своєрідна емуляція статичних локальних змінних C (приклад взято з книги Єрусалимського):

> function newCounter ()
>>     local i = 0
>>     return function () -- анонімна функція
>>         i = i + 1
>>         return i
>>     end
>> end
> c1 = newCounter()
> print(c1())
1
> print(c1())
2
Підтримується також і оптимізована хвостова рекурсія.
За більш детальним оглядом -- див. літературу.

6.4 Об'єктно орієнтовані виклики функцій

Для підтримки об'єктно-орієнтованого програмування Луа надає зручний синтаксис виклику методів. Вираз object:method(arg) еквівалентний object.method(object,arg). (Нагадаємо, що таблиці можуть містити і функції).  При тому посилання на таблицю-об'єкт передається з іменем self:

> our_value = { value = 0 }
> function our_value:add(x) self.value=self.value+x end
> our_value:add(5)
> print(our_value.value)
5

При чому еквівалентність ця повна, і різні стилі можна довільно змішувати. В продовження попереднього прикладу: 

> our_value.add(our_value,3)
> print(our_value.value)
8

Взагалі, хоча прямої підтримки об'єктно-орієнтованого програмування в Луа немає, однак всі його елементи легко емулюються її засобами. Див. наприклад Object Orientation Tutorial

7. Дуже коротко про метаметоди та метатаблиці

Метатаблиці -- засіб змінити поведінку Луа при виконанні звичних операцій, таких як додавання чисел. Таблиці та userdata мають свої індивідуальні метатаблиці (хоча, декілька різних таблиць можуть користуватися спільною метатаблицею). Всі інші типи користуються спільними для всіх величин даного типу (ця можливість з'явилася в Луа 5.1). На жаль, маніпулювати засобами Луа можна лише метатаблицями для таблиць, для всіх інших типів доведеться звертатися до C API.

Спробуємо пояснити, що ж вони таке на класичному прикладі комплексних чисел. 

> Complex = {}
>
> Complex_metatable = {}
>
> function Complex.New(x,y)
>>     local res = {}
>>     setmetatable(res, Complex_metatable)
>>     res.Re = x or 0
>>     res.Im = y or 0
>>     return res;
>> end
>

Створюючи кожна комплексне число, ми присвоюємо йому одну і ту ж метатаблицю -- Complex_metatable. (Зверніть також увагу на згадувану раніше ідіому з ініціалізацією значенням по замовчуванню).

Нехай додавання двох комплексних чисел здійснюється так:

> function Complex.Add(x,y)
>>     local res = Complex.New(x.Re,x.Im)
>>     res.Re = res.Re + y.Re
>>     res.Im = res.Im + y.Im
>>     return res;
>> end
>
Тоді, визначивши в метатаблиці метаметод __add:

> Complex_metatable.__add=Complex.Add
>
> x = Complex.New(1,5)
> y = Complex.New(3,1.7)
>
Ми зможемо робити не тільки так:
> z1 = Complex.Add(x,y); print(z1.Re, z1.Im)
4       6.7
Але і так:

> z2 = x+y ; print(z2.Re, z2.Im)
4       6.7
що значно зручніше :-)

Наперед визначені арифметичні метаметоди це: __add, __mul, __sub, __div, __unm (унарний мінус), __mod, __pow, а також __concat для оператора конкатенації. Множина метаметодів для операторів порівняння: __eq (дорівнює), __lt (менше), and __le (менше або рівно). Інші оператори порівняння своїх метаметодів не мають, бо насправді Луа використовує not(a==b) замість a~=b, b<a замість a>b і b<=a замість a>=b.
Арифметичні метаметоди застосовуються так: якщо перший аргумент оператора має визначений метаметод, використовується саме він, якщо ні -- використовується метаметод другого операнда, якщо і він не існує -- виникає помилка. Метаметоди порівняння працюють лише для змінних з одним набором метаметодів, відтворюючи поведінку Луа для простих типів -- до прикладу, стрічки не можна порівнювати з числами. Але замість помилки просто автоматично повертають false. 

Бібліотеки часто визначають свої додаткові метаметоди. Наприклад, функція print для своїх аргументів завжди використовує функцію tostring(), яка у свою чергу, перевіряє чи метатаблиці аргументів мають метаметод __tostring, і якщо мають -- використовує його.

Ще одним важливим метаметодом є __index. Якщо він не існує, і певний індекс не знайдено в таблиці, повертається nil. Якщо ж __index існує, він повертатиме результат.  Типове використання __index -- реалізація (об'єктно-орієнтованого) наслідування. Найбільш лаконічно це можна записати так:
setmetatable(derived_class, {__index = base_class})
Тепер, якщо якесь поле не знайдене в derived_class, його шукатимуть в base_class. Може __index бути і функцією, що отримує індекс, повертаючи відповідне йому значення.

Аналогічно, __newindex, використовується при створенні нового індекса, дозволяючи, наприклад, обмежити тип індекса лише числом, чи лише стрічкою або взагалі, записувати у журнал всі маніпуляції з таблицею. (Але існує функція rawset(table,key,value), що його не використовує, напряму модифікуючи таблицю).

Нарешті, __metatable служить для захисту таблиці від змін. Якщо помістити туди якесь значення, getmetatable буде повертати саме його, а setmetatable завершуватиметься із помилкою "cannot change protected metatable". 

Взагалі, метатаблиці та метаметоди -- надзвичайно потужний засіб Луа, за більш детальною інформацією про нього варто звернутися до відповідної літератури.

8. Література

Мушу зізнатися, в тексті вище дуже мало оригінального матеріалу. Практично весь він -- короткий переказ кількох основних джерел:
  1. Дуже хорошим підручником Луа є написана її автором книга: Roberto Ierusalimschy, "Programming in Lua", 2nd ed., Rio de Janeiro, 2006. ISBN 85-903798-2-5. Зокрема, вказане 2-ге видання описує Lua версії 5.1, про яку в основному і йшла мова вище. Слід зауважити, що на відміну від книги Страуструпа, читається легко. Хоча, справа, напевне, в тому, що Луа значно простіша від С++. Наскільки я знаю, на російську чи українську мову не перекладалася. Знайти можна на торентах (увага -- великого поширення в Інтернеті набула перша редакція, в тому числі із назвою другої!). Також, на офіційному сайті доступно перше видання, що стосувалося Lua 5.0, 2003-го року: http://www.lua.org/pil/.
  2. Офіційна документація: http://www.lua.org/manual/5.1/manual.html. Так як її задача -- точний і повний опис мови, доволі занудна. Однак в ролі довідника -- саме те.
  3. Матеріали сайту lua-users.org, зокрема "Tutorial Directory" та "Lua Directory".
  4. Англомовна Вікіпедія: http://en.wikipedia.org/wiki/Lua_(programming_language). (На жаль, статті українською та російською зовсім короткі, майже без деталей. Треба б прикластися при нагоді...)
 Існцє ще декілька книжок, що стосуються Луа, вони дещо простіші, але й рівень їх значно нижчий.
  1. Beginning Lua Programming, by Kurt Jung and Aaron Brown. Wrox, February 2007, ISBN 0470069171.
  2. Game Programming with Python, Lua, and Ruby, by Tom Gutschmidt. Course Technology PTR, December 2003, ISBN 1592000770.
  3. Список на офіційному сайті.
Заключне слово

Про те, що таке Луа та про її синтаксис розказано вище. Однак цікавих тем залишається безліч. Можна сказати навіть, що ми тільки закінчили з нецікавим. Так що, при мінімальному везінні, далі буде. :-)

Зразу кажу, значки запрошення інтерпретатора ">"  в прикладах коду залишено свідомо. Щоб копіпаста не була зовсім вже бездумною. Хоча, як то кажуть, захист від ідіота не допомагає від розумних чи кмітливих ідіотів.

Якщо є зауваження чи побажання -- пишіть!

1 коментар: