Kodomo

Пользователь

Типы переменных, функции, видимость переменных (Lua)

План занятия

Выбор учебного проекта

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

Можно объединяться в группы до 5 человек, однако вклад каждого должен быть виден. Код учебного проекта будет опубликован с пометками об авторстве.

Темы можно (и нужно) придумывать самостоятельно. Если ничего не пришло в голову, подкину пару тем:

Примеры учебных проектов:

Программирование крупного проекта (теоретическая вводная)

Основное требование к любому коду - понятность. Это требование проистекает из необходимости изменять код: исправлять в нём ошибки и добавлять новые возможности. Чем понятней код, тем меньше времени тратит программист, чтобы понять код и приступить к непосредственной его правке. А время программиста - очень ценный ресурс!

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

Итак, перед нами стоит задача реализовать некий код (программа или библиотека) или его часть. Мы предполагаем, что в этом коде можно выделить низкоуровневую часть, но пока не знаем, стоит ли это делать и что именно туда вынести.

В данном случае можно применить два подхода:

  1. (вглубь) сначала написать низкоуровневый слой (на С), потом привязать к нему скриптовый язык. Данный подход часто применяется, когда код на C уже есть, а Lua встраивается внутрь. В данном случае Lua используется в качестве встраиваемого языка (embedded) внутри программы, написанной на C. Как следствие, полноценно выполняться Lua-код может только в составе данной программы.

  2. (вширь) сначала всю программу написать на скриптовом языке, а потом найти медленные места и вынести их в C. В данном случае основным языком остаётся Lua, из которого вызывают код на C. Lua-код в данном случае не привязан к одной C-программе и может исполняться различными интерпретаторами. Этот подход всегда используется для Lua-библиотек.

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

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

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

В рамках факультатива мы разберём оба подхода и освоим сопутствующие инструменты.

Операции и операторы

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

Оператор (англ. statement) - это запись в коде, управляющая исполнением. Пример: присваивание, ветвление, циклы. Операторы не относятся к выражениям.

Важно различать эти вещи. К сожалению, в среде отечественных программистов даже весьма заслуженные товарищи время от времени путаются и переводят "operator" неправильно.

В некоторых языках одна и та же запись может быть одновременно и операцией, и оператором (например, присваивание в C). Это не должно сбивать с толку. В функциональных языках всё является выражением (то есть, имеет результат).

Локальные переменные и видимость переменных

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

Пример:

local a = 1
print(a) -- prints "1"

if true then
  local b
  print(b) -- prints "nil"
else
  local a = 2
  print(a) -- prints "2"
end

print(a) -- prints "1"

Переменные, имена которых пишутся после for, являются локальными, хотя их и не объявляли с помощью local.

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

Важно разделять понятия объявление локальной переменной и присваивание значения локальной переменной. Запись "local x" означает объявление локальной переменной x, запись "x = 1" (при условии, что x уже объявлена как локальная переменная) означает присваивание локальной переменной. Запись "local x = 1" означает объявление и присваивание. Если переменную объявили, но не присвоили ей значение, то её значение - nil.

Можно за раз объявить несколько локальных переменных: "local x, y". Можно сразу же присвоить им значения: "local x, y = 1, 2". В этом примере x равен 1, и y 2.

При исполнении команд в интерактивном режиме область видимости равна одной вводимой команде, поэтому если в одной команде объявить локальную переменную, а в другой команде попытаться использовать эту локальную переменную, то ничего не выйдет: так как локальная переменная "ушла" в предыдущей команде, её имя теперь соответствует несуществующей глобальной переменной:

> local x = 1
> print(x)
nil

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

Область видимости

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

Пример:

-- (1, начало)
if true then
  -- (2, начало)
  for i = 1, 100 do
    -- (3)
  end
  -- (2, конец)
end
-- (1, конец)

Область видимости (1) охватывает все строчки этого примера, область видимости (2) охватывает все строчки внутри if, а область видимости (3) охватывает только одну строку кода.

Локальные переменные всегда привязаны к области видимости и они исчезают, когда исполнение выходит из их области видимости. К примеру, если мы объявим локальную переменную в области видимости 2, то она исчезнет на предпоследней строчке примера.

Можно искусственно создавать области видимости с помощью операторов do и end:

-- (1, начало)
do
  -- (2, начало)
  do
    -- (3)
  end
  -- (2, конец)
end
-- (1, конец)

Функции

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

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

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

f = function(x)
  print(x * 2)
end

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

f(100) -- prints "200"

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

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

(function(x)
  print(x * 2)
end)(100)

Функции могут возвращать результат (кстати, функции в функциональном программировании только это и могут):

local f = function(x)
  return x * 2
end

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

print(f(100))

Если исполнение функции завершилось без return, то считается, что функция вернула nil.

Важное правило: в области видимости после return нельзя ничего делать!

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

function f(x)
  return x * 2
end

local function g(x)
  return x * 2
end

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

Глобальные функции (например, print) являются глобальными переменными. Их можно присваивать точно так же, как любые глобальные переменные. К примеру, с помощью "print = nil" можно удалить функцию print, запретив тем самым вывод сообщений на экран с помощью этой функции.

Функции могут возвращать несколько значений:

local f = function(x)
  return x * 2, x * 3
end

local x2, x3 = f(100)
print(x2, x3) -- prints "200    300"

local x2 = f(100)
print(x2) -- prints "200"

Как видно в этом примере, функция не обидится, если часть её результатов никуда не записать. Эти значения пропадают.

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

И последнее про функции на сегодня: они могут принимать произвольное число аргументов. За это отвечает обозначение "..." (три точки). Чтобы получить эти аргументы как таблицу, надо заключить их в фигурные скобки. Чтобы вернуть все члены таблицы-списка из функции, используется функция unpack (Lua <= 5.1) или table.unpack (Lua >= 5.2). Если вызов функции, возвращающей много аргументов, является последним аргументом другого вызова функции, то будут переданы все аргументы, в противном случае - только первый.

function f()
  return 1, 2, 3
end

function g(func)
  return func()
end

function h(...)
  return ...
end

print(0, h(g(f))) -- prints "0  1  2  3"
print(h(g(f)), 0) -- prints "1  0"

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

Истинность

Истинны все объекты, кроме nil и false. Обратите внимание, что 0, пустая строка и путая таблица истинны!

Домашнее задание

  1. Выбрать задачу для учебного проекта.

  2. Зарегистрировать бесплатный аккаунт в сервисе https://github.com и прислать имя пользователя на bnagaev@gmail.com. Этот сервис будет использоваться для публикации и обмена кодом.

  3. Прорешать тест lua2hw на http://kodomoquiz.tk хотя бы на 95% от максимального балла. Прорешать или перерешать незачтённые тесты, названия которых начинаются с "lua".

  4. Написать функцию, решующую квадратное уравнение. Уравнение ax^2 + bx + c = 0. Функция принимает аргументы a, b, c и должна вернуть корни уравнения x1 и x2 (если корни разные) или x (если корень один). Если корней нет, функция ничего не возвращает (или возвращает nil, что то же самое). Включить функцию в состав программы, которая печатает общий вид квадратного уравнения, спрашивает у пользователя значения a, b, c, подаёт их в функцию, которую вы написали, и печатает корни. Обратите внимание, что если результат вызова функции - последний аргумент в списке аргументов print, то print распечатает все результаты функции (так устроены все функции в Lua, а не только print). Будет здорово, если у вас в коде будет функция, считающая дискриминант.
  5. (*) Поработаем с функциями, как с объектами.
    1. напишите функцию f, которая возвращает функцию print. Воспользуйтесь функцией: f()('hello', 'world') должен печатать "hello world"
    2. напишите функцию g, которая принимает функцию (назовём её f) и произвольное число аргументов. Эта функция g вызывает функцию f со всеми оставшимися своими аргументами и возвращает её результат. Пример использования: g(print, 'hello', 'world')
    3. напишите функцию h, которая принимает функцию (назовём её f) и возвращает функцию, которая вызывает функцию f дважды. Пример использования: h(print)('hello', 'world') должно печатать дважды строчку "hello world".
    4. (**) напишите функцию bind, которая принимает функцию (назовём её f) и базовые аргументы и возвращает функцию, которая принимает добавочные аргументы и вызывает f с совокупностью базовых и добавочных аргументов и возвращает её результат. Пример: bind(print, 'hello', 'my')('endless', 'world') должно печатать "hello my endless world"