kstep 27.02.2011 04:48

CodingВведение в Lua для программистов — часть 2.2

text(textarea):В прошлой части я рассказал про таблицы, а в этой хочу поделиться своими знаниями о функциях в Lua.


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

Следующие два кусочка кода являются идентичными:

1
2
3
4
5
6
7
function out(x)
print(x)
end

out = function (x)
print(x)
end



То есть запись function name(args) ... end — это всего навсего синтаксический сахар, а реально это операция присваивания переменной name значения в виде лямбда-функции. Ниже я буду применять синтаксис с присваиванием переменной значения типа function, чтобы вы привыкли к этой мысли, но обычно всё же лучше писать в «сахарном» виде: так удобнее и понятнее.

Процедур в Lua не существует: это по сути те же функции, которые возвращают nil, а это происходит, если в функции нет оператора return (ну или он есть, но в нём не задано значение для возврата, или явно задано значение nil — вне функции разницы не будет никакой).

Естественно, передавать функции можно несколько значений. Естественно, возвращать можно тоже несколько значений. И конечно же есть синтаксис для передачи произвольного числа параметров. Но — по порядку.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- функция принимает два аргумента, возвращает один
sumof2 = function (a, b)
return a + b
end

-- функция принимает один аргумент, возвращает два
double2 = function (a)
return a + a, a * a
end

-- функция принимает два аргумента, возвращает тоже два
plusminus = function (a, b)
return a + b, a - b
end

-- вызовы можно комбинировать
print(sumof2(plusminus(double2(10))))
-- цепочка вызовов получается такая:
-- double2(10) -> 20, 100
-- plusminus(20, 100) -> 120, -80
-- sumof2(120, -80) -> 40
-- print(40)

-- принимаем два значения
sum, dif = plusminus(10, 20)
-- покажет 30 и -10
print(sum, dif)



Все аргументы функций являются необязательными: если функции передано меньше аргументов, чем у неё объявлено формальных параметров, то все параметры, для которых значений не нашлось, получат значение nil. Более того, можно передавать функции и больше аргументов, чем у неё объявлено параметров в прототипе: «лишние» аргументы будут просто проигнорированы. Следить за корректностью и существованием входных параметров предстоит программисту. Значений по-умолчанию для формальных параметров тоже не предусмотрено, так что выставляйте их сами. В общем ситуация чем-то похожа на ту, что в Perl-е. Примеры:

1
2
3
4
5
6
7
8
9
$ lua
Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio
> print2 = function (a, b) print(a, b) end
> print2(1, 2)
1 2
> print2(1, 2, 3)
1 2
> print2(1)
1 nil



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

1
2
3
4
5
6
test_func = function (a, b, c)
if not a then return end -- a - обязательный параметр
b = b or 10 -- b по-умолчанию равен 10
if not c then c = 20 end -- с по-умолчанию равен 20 (аналогично предыдущей строчке, но длиннее)
return a + b * c
end



А если мы хотим функцию наподобие С-шной printf(format, ...)? Легко!

1
2
3
4
5
6
7
8
9
printf = function(format, ...)
print(format)
for i = 1, arg.n do
print(i, arg<i>)
end
end

printf("format string", "one", nil, "two", 3, nil)
</i>



Результат:

format string
1 one
2 nil
3 two
4 3
5 nil


(Ясно, что сейчас это далеко не printf(), но её мы напишем попозже, а сейчас просто разберём этот пример.)

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

Думаю не надо объяснять зачем передаётся число аргументов, а для итерации по этим аргументам рекомендуется использовать цикл for i = 1, arg.n do..., а не for k, v in ipairs(arg) do...? Ведь среди переданных аргументов могут быть nil-ы, и итератор ipairs() споткнётся на первом же из них. Если же в качестве верхней границы в цикле со счётчиком использовать #arg, то цикл тоже срежется совсем не там где надо, особенно если nil будет передан в последних рядах. (За подробностями прошу в предыдущую статью.)

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

 1
2
3
4
5
6
7
8
9
10
&gt; ellipsis = function(...) if ... then print(...) end end
&gt; ellipsis(1, 2, 3)
1 2 3
&gt; ellipsis(nil, 2, 3)
&gt; ellipsis(1, nil, 3)
1 nil 3
&gt; ellipsis(1, nil, nil)
1 nil nil
&gt; ellipsis(false, 2, 3)
&gt;



Функция print() принимает любое число аргументов, поэтому ей просто напрямую будут переданы все аргументы, указанные в нашей тестовой функции, а вот оператор if ожидает только одно булево значение, так что для него многоточие отдаст только первый аргумент, поэтому если первым аргументом будет значение «ложь», то проверка пройдена не будет. (Можете ещё для проверки попробовать выполнить код if 1, 2, 3 then print("yes") end и получить синтаксическую ошибку, чтобы убедиться, что многоточие в случае с if-ом не будет разворачиваться в полный список.)

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

Achtung! Ниже идёт более глубокий материал, который требует чуть лучшей подготовки и может быть не всеми сразу понят. Если вы запнулись на первом же примере после этого абзаца, можете пропустить часть статьи до самых итогов, а сюда вернуться попозже, когда прокачаете навык «функциональное программирование» чуть лучше.

Классический пример хвостовой рекурсии:

1
2
3
4
5
factorial = function (n, m)
if n &lt; 0 then return end
if n == 0 then return m or 1 end
return factorial(n - 1, (m or 1) * n)
end



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

1
2
3
4
5
factorial = function (n)
if n &lt; 0 then return end
if n == 0 then return 1 end
return n * factorial(n - 1)
end



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

Пример функции, которая возвращает другую:

1
2
3
4
5
square_eq = function (a, b, c)
return function (x)
return a*x*x + b*x + c
end
end



Кроме того здесь же динамически создаётся замыкание, в которое попадают значения a, b и c: после выхода из функции square_eq() они перестают существовать, т.к. являются локальными для этой функции, но функция, которая была динамически создана и возвращена, получает свои собственные копии этих переменных, они продолжают жить в этой клетушке, которая называется замыканием. Обратиться к этим копиям извне замыкания, естественно, невозможно.

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

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

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


Тэги: lua luaintro программирование
+ 14 -
Похожие Поделиться

doit 27.02.2011 07:48 #
Очень интересно конечно, но уж простите, но так учить ЯП lua всё равно, что сексу по порнографии. Соберите одно толковое "lua для чайников", а потом уж мы заценим в едином порыве. Я бы с удовольствием почитал бы. Или хотя бы замутите оглавление в постах.
inst 27.02.2011 10:52 #
хм, а мне, например, нравится.

Я думаю, для начала всё же стоит написать материал, обсудить его с адептами, а потом уже можно было бы и PDF-ку какую-нибудь замутить. С оглавлением и пр. прелестями книги.
mhspace 27.02.2011 14:28 #
PDF для цифровых документов - зло. Изначально PDF создавался для гарантии сохранности документа перед печатью и до сих пор это единственная его обязанность, с которой он справляется идеально. Все остальные плюшки вроде навигации и текстовых оверлеев это удовлетворение его извращённых пользователей. На практике, в случаях, когда эти плюшки всё же нужны, всегда есть какой-то другой формат, который подойдёт для данной задачи лучше.
inst 27.02.2011 20:16 #
Спасибо за развёрнутый ответ, но мой эпитет "какую-нибудь" означал как раз то, что PDF указан просто для примера и скорее всего найдётся более подходящий формат.
kstep 27.02.2011 13:05 #
В первой же статье я описал свою ЦА: люди, которые уже знают, что такое цикл, условие, функция... Я расчитываю, что мне придётся расжёвывать, что это такое, что достаточно показать: «а вот цикл в луа записывается так», а не разжёвывать «цикл, ребятки, это когда вот эти инструкции повторяются какое-то количество раз...» Если человек не знает таких элементарных вещей, то тут надо учить не конкретному языку, а программированию в целом, а это слишком большая задача для меня сейчас. Тут одинаково сложно будет давать и Лого-черепашку, и Луа (знаю, намучился с пятиклашками)...

Ещё я предполагаю собеседника человеком любопытным и внимательным, который 1) пробует все примеры на своей шкуре, 2) при встрече незнакомых слов, способен об их значении либо догадаться по контексту (который я изо всех сил пытаюсь предоставить), либо, заинтересовавшись, пойдёт в вики какую (ведь очень часто человек просто не знает, что такая концепция существует). Может, я выставляю слишком высокие требования для своей ЦА?

Да, я кидаю своих читателей в воду, и надеюсь, что он сможет выплыть. Знаете, не все ученики и учителя подходят друг к другу. Кому-то нравится такой подход, кому-то не очень, для них есть другие учебники. Кстати, я старательно избегаю слов «урок», «учитель» и «ученик»: я пишу эти статьи для своих коллег-программистов, знающих столько же, сколько я (для них постаточно примеров с короткими пояснениями), меньше чем я (для них более я пишу подробные разъяснения), либо больше меня (для них достаточно опсания отличий Lua от других языков, чтобы начать на нём писать).
vvorth 28.02.2011 09:36 #
Всё отлично написано - понятно будет любому кто хоть как то знаком с программированием, а не со значением этого слова. Да и стиль изложения хорош. Так что "все правильно сделал" (с)
inst 27.02.2011 10:55 #
Скажите, как быстро развивается язык Lua?
Как-то в цикле особенно не был рассмотрен этот вопрос.
kstep 27.02.2011 13:25 #
Увы, не так быстро, как хотелось бы, за то достаточно уверенными шагами. Сейчас доступна альфа Lua 5.2, выпущенная в конце прошлого года. Есть приличное количество сторонних библиотек и несколько проектов на гуглокоде — в основном игровой направленности. Есть несколько разных диалектов Lua. Следует признать, что если вы хотите зарабатывать на Lua, то в 90% вам дорога в гейм-дев: язык лучше всего поддерживается именно разработчиками игр. Либо можете его заюзать в своём проекте как язык конфигурации, благо интегрировать его с Си достаточно просто.

В общем язык достаточно старый (1993) и доведён до довольно стабильного состояния, меняется очень неохотно. Занял свою основную отраслевую нишу и в ней успешно живёт.
kstep 27.02.2011 13:33 #
Забыл, видимо, добавить ещё один дисклеймер: этот цикл — по сути альфа-версия введения в луа (про ЦА я уже сказал отдельно), потому что вот он пишется и тутже выдаётся. Я готов в определённых пределах дополнять и изменять материал. Пределы — установленная ЦА и конструктивная критика (вроде «я совсем не понял третий абзац сверху» — это сойдёт за конструктивную критику, а вопрос вроде «я попробовал заменить «~=» на «=» и получил ошибку, что я сделал не так?» — вообще бальзам на душу). Ещё моя душа всегда радуется указаниям на ошибки в примерах и опечатки: значит люди не просто читают, но пытаются понять. Первая статья уже немного доработана по таким замечаниям, и я очень благодарен комментаторам.

Кроме того недавно был запрос на примеры реально работающего кода. После обещанной статьи 2.3, завершающей раздел 2, я хочу сделать интерлюдию с приведением и разбором такой реальной программы. В связи с чем у меня большая просьба: подумайте и скажите, что бы вы хотели увидеть?
GalS 02.03.2011 11:08 #
Не описано как принимать из функции несколько параметров.
(аналогично предыдущей строчки, но длиннее)

в которое попадают значение a, b и c

узнали как, можно передавать
запятая ???
kstep 02.03.2011 20:24 #
Добавил пример с множественным присвоением, подчистил ошибки. Спасибо!