Подключение модулей в Lua. Создание собственных модулей. Обзор стандартной библиотеки Lua. Репозиторий LuaRocks и одноимённая утилита
План занятия
- Пояснение про io.read("*n")
- Пояснения по ДЗ 2
- Контрольная работа по материалу занятия 2
- Разбор домашних заданий
- Утверждение учебных проектов
- Первые шаги в git и github
- Классы языков программирования
- Замыкания - экземпляры функции; upvalues - внешние локальные переменные
- состояние функции и чистые функции
- Вызов методов объектов с помощью двоеточия
- Обзор стандартной библиотеки Lua
- Создание собственных модулей в 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:
сообщить гиту своё имя и email, которые будут входить во все ваши последующие коммиты: git config --global user.email "you@example.com"; git config --global user.name "Your Name"
- создание нового репозитория в текущей папке: git init
- начать отслеживать файл в репозитории: git add имя_файла
- сделать коммит, включить в него изменения в определенных файлах: git commit список файлов. Эта команда откроет тектовый редактор, в котором вы напишете текстовое описание коммита: первая строка - котороткое описание изменений, не больше 50 символов. Вторая строка пусткая. Начиная с третьей строки развёрнутое описание коммита (необязательно).
- сделать копию репозитория (файлы + история): git clone пусть-к-исходному-репозиторию
- отправить коммиты в другой гит (например на гитхаб): git push
- загрузить коммиты из другого гита (например, с гитхаба): git pull
Статьи о 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
Встроенные функции - это глобальные переменные.
Полный список встроенных функций
По данной ссылке прочитайте описания нижеперечисленных функций и переменных.
Здесь я приведу краткое описание самых важных глобальных функций и переменных в форме шпоргалки. Это значит, что список и описание функций неполные и могут кое-где грешить против истины ради упрощения и краткости.
Ещё более короткая версия, для распечатывания и повторения
Глобальные переменные:
_G – таблица, в которой содержатся все глобальные переменные
_VERSION – версия Lua
Глобальные функции:
assert – проверяет истинность аргумента, если аргумент ложный, то производит ошибку
dofile – принимает имя файла, выполняет его и возвращает результат (то, что код файла вернул с помощью return)
error – принимает строку, производит ошибку с этой строкой в качестве ошибки
ipairs – принимает таблицу-список и возвращает итератор, возвращающий индекс и элемент
pairs – принимает таблицу-словарь и возвращает итератор, возвращающий ключ и элемент
loadstring – принимает строку с кодом на Lua, возвращает (но не выполняет!) функцию
pcall – принимает функцию и список аргументов, запускает её с аргументами в защищённом режиме (перехватывает ошибки). Возвращает статус выполненеия (истина - успех, ложь - ошибка) и список результатов (в случае успеха) или сообщение об ошибке
print – печатает свои аргументы на экран
require – подключает внешний модуль
tonumber – переводит свой аргумент в число
tostring – переводит свой аргумент в строку
type – возвращает тип своего аргумента как строку (например, "number")
unpack – принимает таблицу-список и возвращает все её члены как множественные результаты
Функции ввода и вывода:
io.open – принимает имя файла и режим работы с ним ("r", "w"), открывает файл и возвращает файловый объект. Методы работы с этим объектом – смотри ниже.
io.flush – синхронирует стандартный поток вывода
io.lines – возвращает итератор для обхода строк файла, после завершения обхода закрывает файл
io.popen – принимает команду и режим работы с ней ("r", "w"), запускает её и присоединяется к её входному или выходному потоку, в зависимости от режима. Удобно для чтения выдачи какой-то внешней программы прямо в процессе генерации этой самой выдачи
io.stderr – стандартный поток ошибок (объект, а не функция)
io.stdin – стандартный поток ввода (объект, а не функция)
io.stdout – стандартный поток вывода (объект, а не функция)
io.tmpfile – создаёт временный файл и возвращает его как файловый объект
io.type – возвращает тип файлового объекта ("file", "closed file", nil)
Методы файлового объекта:
file:close – закрывает файл
file:flush – синхронизирует данные с жестким диском
file:lines – возвращает итератор для обхода строк файла
file:read – читает из файла (см. описание функции, она может читать весь файл, одну строку, число или произвольное число байт из файла в зависимости от аргументов)
file:seek – перейти к определенной позиции в файле
file:setvbuf – настраивает буфер записи (см. описание)
file:write – принимает строку и пишет её в файл как список байт (которым строка в 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
Системные функции:
os.clock – число секунд, которое "съел" текущий процесс
os.time – метка времени, число секунд с начала эпохи Unix (1 января 1970 г.)
os.date – текущая дата как строка
os.difftime – принимает две метки времени (выдачу os.time) и возвращает разницу между ними
os.execute – принимает команду и выполняет её
os.exit – завершает текущий процесс
os.getenv – принимает имя переменной окружения и возвращает её значение
os.remove – удаляет файл
os.rename – переименовывает файл
os.setlocale – выставляет языковой стандарт
os.tmpname – создаёт временный файл и возвращает его имя
Функции для работы с модулями:
package.cpath – строка со списком мест, где ищутся сишные модули
package.loaded – словарь, сопоставляющий имя модуля его значению (тому, что возвращает require)
package.path – строка со списком мест, где ищутся луашные модули
Функции для работы со строками. Могут применяться как методы, например: ("aaa"):upper() вернёт "AAA".
string.byte – возвращает числовые значения символов строки
string.char – принимает числовые значения символов и возвращает по ним строку
string.dump – принимает функцию, возвращает её строковую запись (байт-код)
string.find – ищет в строке
string.format – подставляет в строку-шаблон переменные
string.gmatch – возвращает итератор для обхода всех находок паттерна в строке
string.gsub – поиск и замена
string.len – длина строки
string.lower – перевод в нижний регистр
string.match – проверка соответствия строки паттерну
string.rep – возторяет строку несколько раз
string.reverse – разворячивает порядок символов в строке
string.sub – вырезает часть строки (срез)
string.upper – перевод в заглавный регистр
Функции для работы с таблицами-списками:
table.concat – объединяет элементы списка, соединяя их через разделитель
table.insert – вставляет элемент в список
table.remove – удаляет элемент списка
table.sort – сортирует список
Подключение модулей в 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
Чтобы не перегружать это занятие, пока не будем вдаваться в детали данного пакета.
Домашнее задание
Создать репозиторий с кодом предыдущих домашних заданий на github, сделать в нем хотя бы один коммит, прислать ссылку на репозиторий на bnagaev@gmail.com. Все последующие домашние задания выкладывайте на github
Выучите, что делают встроенные функции Lua (хотя бы короткая шпоргалка, а лучше весь список выше)
Выполните домашнее задание lua3hw на http://kodomoquiz.tk
Напишите функцию skips, которая принимает таблицу и возвращает таблицу таблиц: первая таблица совпадает с входной таблицей, вторая включает каждый второй элемент входной таблицы, третья - каждый третий и т.д. Всего должно получиться столько таблиц, сколько элементов было в исходной таблице. Пример: skips({2, 3, 5, 7}) = {{2, 3, 5, 7}, {3, 7}, {5}, {7}}
Напишите функцию localMax, которая принимает список чисел и возвращает список локальных максимов. Локальный максимум - это элемент таблицы, больший своих левого и правого соседей. Первый и последний элементы списка не могут быть локальными максимами. Пример: localMax({1, 3, 2, 7, 2, 9}) = {3, 7}
- (*) выясните имена остальных слушателей факультатива на github, подпишитесь на обновления от них
- (*) ключи SSH (см. выше в разделе про гитхаб)
(*) установите себе на машину LuaRocks, разберитесь, как он работает. Установите пакет luasocket, разберитесь с ним (с помощью google) и напишите программу, узнающую айпишник компьютера через сайт ip4.me
(*) Как отсортировать список строк по длине, чтобы сначала шли короткие строки, а потом длинные? Подсказка: прочитайте описание функции table.sort