Kodomo

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

Подключение модулей в Lua. Создание собственных модулей. Обзор стандартной библиотеки Lua. Репозиторий LuaRocks и одноимённая утилита

План занятия

Пояснение про io.read("*n")

На прошлом занятии возник вопрос про странное поведение io.read("*n") в случае, если пользователь ввёл не число. При последующем вызове io.read("*n") сразу возвращает nil и не ждёт, пока пользователь что-то введёт. Оказалось, что io.read запоминает ввод до тех пор, пока его не "заберут", используя подходящий формат. Если после неудачного вызова io.read("*n") вызвать io.read(), то будет сразу возвращена строка, которую ввели в прошлый раз и которую не удалось превратить в число. См. переписку по этому вопросу.

Пояснения по ДЗ 2

Разберем задания, в которых многие ошиблись.

Что возвращает следующее выражение?
function() return 1 end

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

Как удалить локальную переменную с именем myvar?

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

Как присвоить значение 1 локальной переменной x?

Правильный ответ: x = 1. Ответ "local x = 1" не принимался, так как спрашивалось, как присвоить значение переменной, а не как объявить переменную с таким-то значением.

Под сколькими именами может быть известна одна и та же функция в Lua?

Правильный ответ: [0, +∞). У функции может быть сколько угодно имён (если её присвоили многим переменным) или не быть ни одного (если её объект никакой переменной не присвоили, а просто создали и, например, сразу же вызвали).

Как удалить глобальную переменную с именем myvar?

Правильный ответ: myvar = nil

Что распечатает следующий код на Lua?

local x = 0
if x then
    print('x')
else
    print('not x')
end

Правильный ответ: x. Все значения, кроме nil и false в Lua считаются истинными, в том числе 0.

Что будет распечатано, если в интерактивном режиме набрать следующие команды?

> f = function(x) return x + 1 end
> local x = f(100)
> print(x)

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

Что нужно сделать, чтобы print при запуске в любом месте кода перед выдачей в той же строке печатал слово hello?

Правильный ответ:

local original_print = print
print = function(...)
    original_print('hello', ...)
end

В данном коде мы сохранили изначальный вариант print'а в локальную переменную original_print и после этого переопределили значение глобальной переменной print. В неё мы положили замыкание, которое использует original_print, причём добавляет строку "hello" в начало списка аргументов.

Что распечатает следующий код на Lua?

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

local n = 100
local f1 = f(n)
f1(50)
print(n)

Правильный ответ: 100. Переменная n не изменяется.

Код проекта А разделён на два слоя: низкоуровневый и высокоуровневый. Вася решил связать проект А с проектом Б. В какой из этих двух слоев проекта А придётся вносить код, работающий с кодом проекта Б? (При условии, что проекты А и Б написаны как следует.)

Правильный ответ: преимущественно в высокоуровневый. Низкоуровный код (реализация) проекта А "имеет право" знать только о коде внутри своего же проекта А. Следовательно, привязка к проекту Б может происходить только на высокоуровневом слое. Разумеется, "плоды" этой привязки (к примеру, данные, которые выдают функции проекта Б) могут передаваться из высокого уровня проекта А в его низкий уровень, однако получать их от проекта Б должен именно высокоуровневый код проекта А. Иногда в целях оптимизации это правило нарушается и соединяют низкоуровневый код проекта А с другими проектами и часто это ухудшает код проекта А.

Команда из 10 разработчиков должна написать код за 1 месяц. Какая причина может побудить их начать писать с верхнего уровня?

это сэкономит время и усилия команды

Первые шаги в Git и GitHub

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

В множестве серьёзных проектов используется Git, в том числе ядро Linux. Умение пользоваться git можно отнести к основным умениям, составляющую компьютерную грамотность.

Git на Microsoft Windows может быть громоздким. Если есть возможность, рекомендую использовать Linux или Mac или подключаться к kodomo по ssh и работать с гитом там. В компьютерном классе установлен Git в том числе для Windows. В домашних условиях можно установить пакет Cygwin (не забудьте поставить галочку напротив git в списке устанавливаемых компонентов). Есть и другие варианты. Как бы то ни было, для выполнения любого действия с гитом понадобится вводить команду git в командной строке.

Основные действия при работе с git:

Статьи о Git: Волшебство Git, Всё о Git, Использование Git и GitHub, Ежедневная работа с Git.

GitHub

Гитхаб - это сервис, бесплатно обслуживающий git-репозитории проектов с открытым исходным кодом и представляющий элементы социальной сети для программистов. GitHub является удобной средой для совместной работы над кодом. На гитхабе очень просто исправлять ошибки в чужом коде, если вы их нашли. Многие биоинформатики публикуют код на гитхабе.

В прошлый раз мы завели аккаунты на GitHub. Посмотрим, что ещё мы можем сделать. В меню найдите кнопку "создать репозиторий". Разберитесь, что есть что: куда вписывать имя и описание и т.п. Теперь, когда вы сделали репозиторий, сделайте его локальную копию при помощи команды git clone (путь отображается в правой части страницы репозитория). Добавьте какие-нибудь файлы, сделайте первый коммит, отправьте его в github при помощи git push. Теперь у вас есть первый коммит на гитхабе!

На GitHub опубликовано множество репозиториев. Адреса репозиториев формируются очень просто github.com/username/reponame, где username - имя хозяина репозитория, а reponame - имя репозитория. Каждый пользователь GitHub может сделать свою копию чужого репозитория, это действие называется Fork (форкнуть). Найдите какой-нибудь репозиторий и сделайте его форк. Позже мы поговорим о том, что ещё можно сделать с форком.

Бонусное задание: разберитесь, что такое ключи SSH и как добавить публичный ключ SSH в гитхаб. Это избавит вас от необходимости вводить свой пароль каждый раз при запуске команды git push. Информацию о ключах SSH можно найти в сети и в руководствах, ссылки на которые были выше.

Как говорилось выше, гитхаб - это социальная сеть. А это значит, что ко всему, что есть на гитхабе (репозиториям, коммитам) можно писать комментарии, которые могут перерастать в обсуждения. Кроме того, можно собирать "избранное" путем проставления "звёздочек" пользователям и репозиториям. В качестве тренировки выясните github-имена друг друга и подпишитесь на знакомых.

Есть в github и аналог "групп", которые есть в обычных социальных сетях. В гитхабе группы называются организациями. В организации можно создавать команды и приглашать участников. Те, кто зарегистрировался в github и сообщил свой логин, приклашены в организацию LuaAndC, в команду students. Учебные проекты мы планируем публиковать в этой организации. По умолчанию членство в команде не афишируется и видно только другим участникам организации. Каждый участник, если захочет, может изменить тип членства на "публичное" в настройках команды.

Классы языков программирования (теория)

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

Замыкания - экземпляры функции; upvalues - внешние локальные переменные

Функцию можно начать писать в любом месте. Определение функции - это это выражение, возвращающее функцию. Результат выполнения такого выражения - экземпляр функции - называется замыканием (closure). Из функции можно использовать локальные переменные, объявленные до неё во внешних по отношению к ней областях видимости. В узком смысле замыканиями являются только те функции, которые пользуются внешними локальными переменными. Если замыкание пользуется локальными переменными, объявленными снаружи от него, то они называются внешними локальными переменными (upvalue). Первая часть названия ("up") помогает запомнить, что они должны быть объявлены выше функции-замыкания.

Пример:

local a = 123

local function f()
    print(a)
end

f() -- prints 123

Если локальная переменная объявлена ниже функции, то функция её не видит:

local function f()
    -- a is a global variable here
    print(a)
end

local a = 123

f() -- prints nil, because a is a global variable inside f()

Если исполнение вышло из области видимости, в которой были объявлены upvalue, они не удаляются, пока "живо" замыкание.

Пример:

local function getSwitch()
    local value = 0
    return function()
        if value == 0 then
            value = 1
        else
            value = 0
        end
        return value
    end
end

local f = getSwitch()
print(f()) -- prints 1
print(f()) -- prints 0
print(f()) -- prints 1
print(f()) -- prints 0

В этом примере функция getSwitch создает замыкание, использующее локальную переменную value функции getSwitch. Мы вызываем функцию getSwitch и сохраняем результат (замыкание) в переменную f. К этому моменту облать видимости внутри функции getSwitch закрылась, однако локальная переменная value остается "живой", так как она привязана к замыканию f, которое живо.

После этого мы вызываем несколько раз функцию-замыкание f. Она меняет значение переменной value с 0 на 1 и обратно и возвращает его, а мы его печатаем.

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

Ещё один пример (из документации к Lua)

     local a = {}
     local x = 20
     for i = 1, 10 do
       local y = 0
       a[i] = function()
         y = y+1
         return x + y
       end
     end

Этот код создаёт 10 замыканий (экземпляров анонимной функции). Каждое из этих замыканий использует свою переменную y, но все они используют общую переменную x.

Локальные переменные замыкания полезны, чтобы "спрятать" данные в функцию. Функция, обладающая состоянием, называется функтором. Функтор обладает одновременно свойствами объекта (внутреннее состояние) и функции (его можно вызывать как функцию). В занятии 9 мы обсудим другой способ создавать функторы - с помощью метатаблиц.

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

Чистые функции

Таким образом, входной информацией для функции являются не только аргументы, но и внешние переменные и глобальные переменные. Результат функции зависит от аргументов, внешних переменных и глобальных переменных. Если результат функции зависит только от её аргументов, её аргументы содержат неизменяемое значение и не содержат ссылок на изменяемые объекты, то такая функция называется чистой. Чистые функции просты в написании, отладке и тестировании. Стоит стремиться писать чистые функции, когда это возможно. В функциональных языках функции чисты по умолчанию.

Пример чистой функции:

local function f(x, y)
    return x ^ y
end

Отметим, что эта функция чиста при условии, что x и y - числа. Числа не могут изменяться и не содержат ссылок на данные, которые могут меняться, поэтому результат функции зависит только от её аргументов и всегда одинаков для одних и тех же аргументов.

Примеры нечистых функций:

local function changeSmth(o)
    o.x = o.x + 1
    return o.x ^ 2
end

local function sin(x)
    return math.sin(x)
end

Очевидно, что функция changeSmth не является чистой - она изменяет таблицу, которую ей подают в качестве аргумента. С функцией sin интереснее. Она использует поле sin глобального объекта math для вычисления синуса числа. (Эта функция относится к стандартной библиотеке Lua.) Следовательно, результат функции зависит не только от её аргумента, но и от состояния глобального объекта math, а значит, функция не является чистой. Если мы подменим функцию math.sin на другую функцию, то функция sin будет возвращать другое значение:

local function sin(x)
    return math.sin(x)
end

print(sin(0)) -- prints 0
math.sin = math.cos
print(sin(0)) -- prints 1

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

Вызов методов объектов с помощью двоеточия

Метод - это функция, являющаяся члено объекта. Пример:

local o = {
 x = 2,

 getX = function(self)
   return self.x
 end,
}

В объекте o есть метод getX. Вызовем этот метод:

print(o.getX(o)) -- prints 2

Методы объекта обычно используют сам объект, поэтому его принято передавать первым аргуметом. Чтобы избежать дублирований этой переменной, введена сокращённая форма записи для этого случая (получить метод как член объекта и сразу же вызвать его, передавая этот объект как первый аргумент):

print(o:getX()) -- prints 2

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

Обзор стандартной библиотеки Lua

Встроенные функции - это глобальные переменные.

Полный список встроенных функций

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

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

Ещё более короткая версия, для распечатывания и повторения

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

Глобальные функции:

Функции ввода и вывода:

Методы файлового объекта:

Математические функции, без комментариев: math.abs math.acos math.asin math.atan math.atan2 math.ceil math.cos math.cosh math.deg math.exp math.floor math.fmod math.frexp math.huge math.ldexp math.log math.log10 math.max math.min math.modf math.pi math.pow math.rad math.random math.randomseed math.sin math.sinh math.sqrt math.tan math.tanh

Системные функции:

Функции для работы с модулями:

Функции для работы со строками. Могут применяться как методы, например: ("aaa"):upper() вернёт "AAA".

Функции для работы с таблицами-списками:

Подключение модулей в Lua и создание собственных модулей в Lua

Каждый Lua-файл является Lua-модулем. Чтобы подключить Lua-модуль, используется функция require. Эта функция возвращает объект, который возвращает код модуля:

-- file aaa.lua
return "test"

-- Lua interpreter
local aaa = require "aaa"
print(aaa) -- prints "test"

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

-- file aaa.lua
local function f()
        return 1
end

local function g(x)
        return x * x
end

return {f=f, g=g}

-- Lua interpreter
local aaa = require "aaa"
local x = aaa.f()
print(aaa.g(x)) -- prints 1

Точки в названии модуля заменяются на разделитель частей пути к файлу (например, "/"). К примеру, модуль "aaa.bbb.ccc" будет загружен из файла "aaa/bbb/ccc.lua". Файлы ищутся в нескольких местах. Эти места контролируются переменной окружения LUA_PATH или переменной Lua package.path. В ней содержится список мест для поиска, разделенных точкой с запятой. Каждое место для поиска преставляет собой путь к файлу, в котором на место процента "%" подставляется имя модуля, в котором точки заменены на разделители.

Установка модулей из LuaRocks

Сообщество Lua накопило достаточно много модулей. Их выкладывают в репозитории luarocks, откуда пользователи могут устанавливать их с помощью программы luarocks.

Пример. Установим пакет luasocket, служащий для работы с сетью:

luarocks install luasocket

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

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