Показаны сообщения с ярлыком programming. Показать все сообщения
Показаны сообщения с ярлыком programming. Показать все сообщения

понедельник, августа 08, 2016

Как можно участвовать в ICFPC (2016)

«Рядом с этой сокровищницей мысли, — неторопливо думал Васисуалий, — делаешься чище, как-то духовно растешь»

В этом году мы с товарищами (ForNever, Minoru, Hagane, grouzen) опять взялись участвовать в ICFPC. Если кто не в курсе, что это такое — см мой предыдущий пост или серию постов товарища adept.
В этот раз для разнообразия мы не писали AI для игрушки. Организаторы были из Японии, и поэтому мы весь уик-енд... складывали оригами. Кто-то из кода, а кто-то нет.
Задача состояла в том, чтобы из квадратного «листа бумаги» складывать разные (плоские) фигуры. В качестве задачи даётся описание нужной фигуры, а решение — набор складок на квадрате и описание, какие вершины в какие переходят при складывании. Первая сотня фигур была предложена организаторами, а дальше задачи могли отправлять участники, и за отправку задач тоже давали очки.
При этом, на самом деле, это было «не настоящее оригами». Были допустимы решения, невозможные с бумагой — части листа могли проходить друг через друга при складывании. При отправке решений чужих задач можно было даже «рвать бумагу» (при отправке своих задач — нельзя). Момент про разрешено ли разрезание мы уточняли долго и сложно.
Контест в этот раз проходил с утра пятницы по вечер воскресенья в моём часовом поясе.

В пятницу

с утра я «по-быстрому» написал скачивалку задач с сервера и закачивалку решений (на python, просто потому что там было REST API, а у меня уже был опыт использования для этого питоньей библиотеки requests). Ещё я написал парсер предложенного формата задач и рендерилку, которая рисует фигуры из задач в виде svg (на Haskell). Сразу пригодился хаскельный модуль Data.Ratio. Дело в том, что в задачах использовались координаты в виде рациональных чисел, причём размеры числителя и знаменателя не были ограничены. Так что быстро образовалась строчка type Number = Ratio Integer.
Потом я посмотрел на результаты рендера и увидел, что первые семь задач тривиальные (там просто квадрат, сдвинутый или повёрнутый, но не сложенный), решения для них легко написать вручную. Решил и залил.
Потом мы начали думать над задачей (я уже писал, у меня всегда так — сначала код, потом думать).
Когда нет ограничения на разрезание бумаги, задача сводится к тому, чтобы разрезать квадрат на эн многоугольников и сложить из них нужную фигуру. Насколько я знаю, подобные темы широко и глубоко изучены, но я не знаком с конкретными работами и алгоритмами. (в университете был даже какой-то спецкурс по оригами, как разделу геометрии, но я на него не попал). Поэтому я долго и упорно думал в эту сторону, но ничего не придумал.
В чате довольно быстро образовалось несколько идей:
  • Разрезать требуемую фигуру на треугольники и попытаться из таких треугольников сложить квадрат.
  • Какой-нибудь вариант генетических алгоритмов: разрезаем квадрат как попало и складываем как попало, оцениваем скор в соответствии с правилами организаторов.
  • Быстро придумался простой алгоритм для складывания любого достаточно маленького выпуклого многоугольника: пройти по всем его рёбрам и сложить бумагу вдоль них. Если останутся торчащие «хвосты», то опять пройти по рёбрам и подвернуть вдоль них.
  • Кроме силуэта фигуры, в задачах приводился её «скелет» — набор всех рёбер в положении после складывания. При этом, правда, было указание, что это только подсказка, складывать в соответствии с этим скелетом не обязательно. Так вот, появилась идея брать этот скелет и пытаться разворачивать его до квадрата.
Товарищ grouzen произвёл обширные изыскания существующих научных работ на тему оригами и около, и обеспечил нас пейперами для чтения на всю неделю. К сожалению, применить их мы не смогли.
Потом начался как всегда разброд и шатание, каждый стал заниматься чем-то своим.
Я решил начать с написания какого-никакого «фреймворка» для дальнейшей работы — набора функций для разрезания многоугольников, объединения и т.п. Некоторые из геометрических алгоритмов тривиальны (отражение многоугольника относительно прямой), зато другие зубодробительны (объединение произвольных многоугольников). Для таких алгоритмов я стал гуглить существующие реализации, по возможности на Haskell (т.к. парсер задач уже был на Haskell). Нашёл только clipper. Это биндинги к одноимённой библиотеке на C++. Сразу обнаружилась особенность: Clipper работает с целочисленными координатами (видимо, в основном заточен на экранную графику) размером Int64. А у нас рациональные и просто целые очень большие числа. Так что при попытке засунуть в clipper одну из первых тривиальных задач сразу получили: Literal 1267650600228229401496703205376 is out of the Int64 range -9223372036854775808..9223372036854775807. Так что реализацию сложных алгоритмов отложили на потом (по факту объединение многоугольников у нас в итоге и не использовалось). Minoru взялся писать разрезание многоугольника прямой на два (это в любом случае бы понадобилось для складывания, а в clipper этого нет).
Дальше Minoru, Fornever и hagane начали обсуждать вариант с разворачиванием скелета. Я в эту идею особо не вникал, т.к. думал над представлением «фигуры» в процессе её сворачивания и реализацией «сворачивалки» в виде State ЧтоТо ().
К вечеру пятницы у меня был «солвер», который брал многоугольник и один раз сворачивал квадрат вдоль каждой грани этого многоугольника. И рисовал в svg что получилось. То есть он теоретически мог уже решать часть задач. Но при этом он принимал данные не в том виде, и выводил не в том. При этом там было ещё куча неуловленных багов.
Fornever к этому времени перегенерировал все иллюстрации к задачам, устранив баг в рендерилке.

В субботу

с утра я стал было дописывать алгоритм сворачивания выпуклых полигонов, но наткнулся на баги в методе разрезания многоугольника (где-то какое-то деление на ноль). Попытался разобраться в коде этого метода и не смог. Т.к. Minoru сильно в другом часовом поясе, я решился переписать этот код. Стал гуглить как это делается правильно, и нашёл зубодробительные алгоритмы разрезания произвольных многоугольников. После долгих попыток в них разобраться я внезапно понял, что нам нужно разрезать только выпуклые многоугольники (т.к. многоугольники, на которые бумагу могут разделить складки оригами, могут быть только выпуклыми), и быстро написал сравнительно простой метод разрезания выпуклых многоугольников.
Потом я стал писать форматирование решения в соответствии с форматом, прниимаемым сервером. В это время Fornever с Hagene что-то писали на тему «разворачивания скелетов». Hagane ещё и постил задачи для других участников, придуманные «вручную». Потом, чтобы не тратить клетчатую бумагу, Hagane c Fornever стали писать генератор случайных задач.
grouzen предложил написать графическое интерактивное приложение для разворачивания фигур вручную, но до реализации этой идеи как-то ни у кого не дошли руки.
К вечеру субботы у меня был солвер, который решал некоторую часть задач, и я его пачками натравливал на избранные простые задачи. На многих задачах при этом проявлялся баг в геометрических алгоритмах. Тут же написалась решалка тривиальных задач (которые просто квадрат без складываний). Ещё я написал вариант солвера, который складывает вместо требуемой фигуры её выпуклую оболочку — это, очевидно, даёт приближённые решения, но лучше чем ничего.
Потом оказалось, что у нас имеется три реализации геометрии — Minoru и моя на Haskell, Hagane на Racket и Fornever на F#; и две реализации форматирования вывода, обе на Haskell — моя и Hagane, отличающиеся по-моему только названиями функций. На этом месте я взвыл в чатик — мол, нет на нас ПМ-а или хотя бы тимлида.
До позднего вечера я искал баги в реализации геометрии, Fornever с Hagane писали генератор задач, а Minoru писал разворачивание скелетов.
Потом Hagane решил взять на себя функции лида и отправил меня спать. Правда, совсем перед тем как лечь, я нашёл баг в одной строчке (опечатка при копипасте) в реализации разрезания многоугольника, и поправил его, отчего все «волшебные» баги исчезли.

В воскресенье

с утра у меня образовалось несколько идей:
  • сделать параллельный перенос исходного квадрата к месту, где находится искомая фигура — до этого момента все складывания выполнялись «на месте», на единичном квадрате, так что случаи, когда фигура находится где-то сбоку, не обрабатывались.
  • придумать какие-нибудь эвристики, как определить, на какой угол нужно повернуть квадрат перед складыванием — т.к. фигуру «единичный квадрат повёрнутый на 45 градусов», очевидно, нельзя получить из единичного квадрата одними складываниями без поворотов.
  • доделать складывание выпуклых фигур в солвере. К этому моменту у меня делалось всегда 2 итерации обхода многоугольника со складыванием вдоль рёбер, что было избыточно для одних задач и недостаточно для других. Надо было написать проверку — надо ли делать ещё обход.
  • появилась идея по складыванию невыпуклых фигур. Невыпуклый многоугольник с одной невыпуклой вершиной часто можно получить перегибанием выпуклого многоугольника. Поэтому можно поискать способы «развернуть» такие многоугольники в выпуклые, а выпуклые мы складывать уже умеем. Для более хитрых многоугольников можно процедуру разворачивания повторить несколько раз.
Я нарешал скриптом с исправленным солвером пару сотен простых задач и стал реализовывать перечисленные идеи. В итоге где-то к середине дня я реализовал перенос и для самых простых случаев поворот, доделал сворачивание выпуклых фигур, и оставалось только время от времени запускать скачивалку и решалку (ту, которая складывает выпуклую оболочку для всего подряд). Ещё я оставшееся время пробовал всякие улучшения и оптимизации.
В частности, в какой-то момент я вдруг решил попробовать сделать хоть какой-нибудь вариант генетического алгоритма для решения сложных задач. Для этого нужно иметь функцию оценки, а она в данном случае определяется через площади многоугольников. Какое-то время я боролся с функцией нахождения площади из clipper, но то ли я его не так готовил, то ли есть баги в биндингах, то ли в самом clipper — результаты оценки получались настолько неадекватными, что я на это плюнул. Писать собственный расчёт площади времени не осталось.
У моего солвера оставалась существенная проблема: он часто генерировал решения размером в десятки или сотни килобайт, при ограничении в 5000 байт. Вечером стали думать, как уменьшить размер решений. В итоге сделал:
  • если многоугольник не выпуклый и мы всё равно будем делать приблизительное решение — округлить входные координаты до 10-12 знаков после запятой (уменьшает размер дробей на выходе);
  • складывать не по всем подряд рёбрам многоугольника, а сначала только по чётным, потом только по нечётным. Не очень понимаю почему, но на некоторых задачах это помогло существенно.
Вечером Minoru стал пытаться решать сложную задачу номер 101. Там нужно было сложить классического журавлика, и он его стал складывать. Из бумаги. С целью потом перевести эти складки в цифры. Правда, в итоге безуспешно. Но эту задачу вообще решила только одна команда.
Ночью я передал Minoru скрипты для скачивания задач, решения и заливки решений, оставил его их запускать в оставшееся время контеста, и ушёл спать. Leaderboard к этому времени как раз заморозили.

В итоге

к моменту заморозки leaderboard мы оказались на 44м месте (из 293 команд всего, получивших ненулевое количество очков 201). Что есть необычно высокий результат по сравнению с нашими прошлыми попытками.
По моему личному впечатлению, в этот раз задача была заметно проще, чем в предыдущие (я не удивлюсь, если увижу разочарованные отзывы об этом icfpc от «аксакалов» — мол, слишком легко). Но, возможно, это такой perception bias из-за того, что в классической аналитической геометрии с линейной алгеброй худо-бедно разбираюсь, а во всяких AI почти никак не разбираюсь.
У нас, как всегда, были проблемы с организацией и коммуникацией (каждый писал что-то своё, иногда дублируя работу). По-моему, если бы мы физически находились в одном месте, результаты могли бы быть сильно лучше, хотя бы за счёт взаимной мотивации (говорят, психологически сложнее отлынивать, когда видишь как человек за соседним компом сосредоточенно пишет код) :) Как и в прошлые разы, были проблемы из-за того, что члены команды имели сильно разный опыт работы с выбранным языком.
Надеюсь, в следующем году мы как-нибудь сможем смягчить эти проблемы, и результат будет ещё лучше.

среда, августа 12, 2015

Как не надо участвовать в ICFPC (типа отчёт об ICFPC-2015)

Мы с несколькими товарищами (широко известными в узких кругах - ForNeVer, rexim, Minoru) уже который год подряд пытаемся участвовать в ICFPC. Если кто не в курсе, это такой своеобразный ежегодный контест по программированию, приуроченный к international conference for funcional programming (ICFP). Несмотря на название, программировать можно не только на функциональных языках, а на чём бог на душу положит. Более того, во многих случаях код организаторам передавать необязательно - контест организован так, что нужно отослать ответы на какие-то задачи. Т.е. теоретически можно вообще не программировать, а решить задачи на бумажке и отослать ответы. Просто очень сложно. Контест длится трое суток — в моём часовом поясе это от вечера пятницы до вечера понедельника. Темы заданий выбираются в целом довольно разнообразные, но последние годы заметен уклон в сторону всяческих AI. Например, вот задачи тех контестов, в которых я участвовал:
  • 2014 - надо было написать AI для игры a la Pacman. Даны карты, а надо прислать программы на ассемблере выдуманных процессоров, играющие за основного персонажа и за призраков, набирая побольше очков.
  • 2013 - угадывание формул. Даны значения переменных и результат вычисления, а также некоторая общая информация о выражении. Надо прислать угаданные выражения.
  • 2012 - AI для игры наподобие Supalex.
Результаты у нашей команды обычно... э... ну, лучше чем последнее место, чо. С одной стороны, главное не победа. C другой...
Типичный «путь к успеху» таков. Команда географически сильно распределённая, покрывая несколько часовых поясов. Теоретически это могло бы даже быть преимуществом (непрерывный 24-часовой кодинг, как 24-часовая поддержка, а?). Но как-то не в нашем случае. О контесте вспоминаем за пару дней. Обсуждаем выбор языка программирования, особо не зацикливаясь на фиксации этого выбора. Типичное решение: «давайте попробуем haskell, а там если увидим что чото не то, возьмём C++». При том, что часть команды имеет очень мало опыта с Haskell, а другая часть — с C++. Во время контеста (он длится трое суток) у каждого что-нибудь случается, из разряда «зимой совершенно неожиданно выпал снег» — приезжают родственники, нужно на работу или ещё что-нибудь. Т.е. вещи, которые в принципе можно было предотвратить (отработать в другой день итд), если позаботиться об этом достаточно заранее. В этот раз соригинальничали. О контесте вспомнили аж за неделю. В качестве языка программирования выбрали Scala. При том, что двое из команды (я и Minoru) к этому моменту о ней только по наслышке знали. Я был в принципе не против, т.к. наслышан о языке был давно — почему бы и не изучить. За неделю скачал компилятор, среду разработки (hint: Idea community edition с плагинами для Scala и vim mode) и уехал на дачу. Где через напоминающий старые-добрые времена мобильный GPRS (ни дать ни взять диалап - не верещит на всю комнату, и только) стал изучать гугл на тему туториалов. Нашёл scala by example. Написав hello world, решил потренироваться на кошках и пошёл куда очень давно не ходил — на acm.timus.ru. Кстати, с удивлением обнаружил, что они теперь поддерживают и Scala, и Haskell. Тот факт, что я с институтских времён не занимался спортивным программированием, очень даже сказался — все мои программы упёрлись в time limit :) Не из-за «тормозной явы», конечно, а просто из-за плохих алгоритмов. В данном случае меня это не очень расстроило — в рамках ICFPC производительность нашего кода обычно мало важна. Общее впечатление от Scala, кстати — это скорее «пере-ява», чем «недо-хаскель» :)
У меня есть «особенность стиля разработки», по-моему, не очень удивительная. Я плохо умею придумывать алгоритмы и организацию кода отдельно от самого кода. Поэтому, приступая к чему-то новому, я обычно начинаю с написания сколь угодно плохого алгоритма на том, что под руку подвернётся (прототип в 15 строк, он может даже и вовсе не работать), а дальше уже думаю над алгоритмами и дизайном параллельно с изменением кода. Начало предыдущих ICFPC выглядело поэтому так. Пока народ в чате думает над задачей, над алгоритмами, обсуждает, лучше ли здесь Haskell или C++, я беру и начинаю что-то писать на haskell. К моменту, когда в чате появляются сообщения вида «ну ладно, давайте писать», оказывается, что в репозитории уже лежит мой код на haskell :) (хороший код или плохой — отдельный вопрос). И в итоге остальные, из соображения «экономии кода», чтобы не переписывать то же самое на другом языке, продолжают тоже на haskell. Когда опыта с ним мало, это иногда выливается в многочасовую борьбу с компилятором. Видимо как раз из-за того, что прошлые разы я таким образом «навязывал» остальным Haskell, в этот раз ForNeVer и предложил взять Scala :) Поэтому в этот раз в начале контеста я ждал, пока ForNeVer cоздаст заготовку проекта, а в это время пытался думать над задачей. Без кода думать получалось плохо.
Кстати, о задаче.
В этот раз опять нужно было написать AI для игры. Игра — нечто похожее на тетрис на гексагональной решётке. Основные отличия: вся последовательность фигур известна заранее; фигуры сами по себе вниз не падают, их надо падать отдельными командами; и их можно закреплять не только внизу, но и где-нибудь посередине, если упереть в стенку или в другую фигуру. И ещё: каждая команда может быть закодирована одной из нескольких букв. Поэтому из последовательностей команд можно составлять слова. Имеется некоторое количество «заклинаний», «phrases of power», или попросту чит-кодов. За использование их в последовательности команд дают отдельные очки. Одно заклинание (Ei!) сказали сразу, другие предложили найти «в задании, в твитах, в background leaterature и game artifacts». Бэкграундом были, видимо, произведения Лавкрафта. Организация получения задач и отправки решений такая: дано фиксированное количество «карт» — описаний игровых полей (они могут быть пустыми или на них изначально могут быть какие-то занятые клетки) и фигур, которые там могут выпадать. Надо для каждой карты прислать последовательность команд, которая наберёт по специфическим правилам этого «тетриса» как можно больше очков.
Я нарисовал одну из первых карт (они выдаются в виде json) на бумажке, и обнаружил, что там заполненными клетками написано «Ei!». Ага, значит на других картах могут быть другие заклинания. Только рисовать гексагональную решётку даже на клетчатой бумаге немного утомительно, так что явно нужна какая-то рисовалка карт, чтобы хотя бы на них посмотреть. Примерно к этому времени появилась заготовка проекта, и ForNeVer уже сделал загрузку карт из json. Тогда я написал рисовалку карт в виде ascii-art. Запустив её на имеющихся картах, узнали ещё парочку заклинаний.
В предыдущих ICFPC мы как-то умудрялись обходиться без визуализации действий наших ботов. В этот раз rexim для разнообразия взялся таки сделать визуализацию. Я в AI и родственных алгоритмах мало сведущ — в прошлом году апофигеем стало использование волнового алгоритма. Поэтому я взялся писать то, что особой изобретательности не требует, но при этом явно понадобится — эмулятор этого самого тетриса. Отдельный анекдот вышел вокруг представления игрового поля. Очевидно, нужно что-то наподобие двухмерного массива, с возможностью как-нибудь делать операции «вычёркивание строки» и «перемещение занятых клеток». При этом у меня был, с одной стороны, опыт ещё институтских игрушек, где это был сишный char[][]; с другой — опыт хаскеля предлагал использовать что-нибудь типа [[CellState]]. Опыт со scala не подсказывал пока ничего. Я спросил в чате, и мне предложили не заморачиваясь использовать Array[Array[CellState]] (если кто не в курсе — это мутабельный массив с произвольным доступом, в принципе аналогично сишному char[][]). Ну и ладно. Обернул его в класс, дополнив методами типа «проверить валидность координат», и вперёд. Таким образом, эмулятор вышел «мутабельным»: при обработке команды он изменяет своё состояние. А когда позже ForNeVer стал писать какой-никакой AI, ему понадобились методы типа «посмотреть что получится если выдать такую-то команду», не изменяющие состояния игрового поля. В результате пришлось переделывать эмулятор в частично иммутабельный. Народ потом возмущался — зачем у нас эмулятор мутабельный! в следующий раз никакой мутабельности! Я отвечал, что тогда непонятно, зачем взяли Scala — код без мутабельности естественнее писать на Haskell.
Ещё через некоторое время после начала контеста к нам присоединились grouzen и ulidtko. Ulidtko примерно одновременно с Minoru взялись решать специфическую геометрическую задачку: поворот фигур на гексагональной решётке. Причём каждый из них, кажется, пошёл своим путём. Я решил, что трёх человек на такую простую на первый взгляд задачу будет многовато, и решил этим не заниматься. В итоге большую часть времени я допиливал помаленьку эмулятор и... играл в тетрис. В наш эмулятор с визуализатором. Т.к. первый работоспособный AI у нас появился только где-то к вечеру воскресенья, до этого мы могли набирать очки только играя в наш «тетрис», набирая очки и записывая последовательности команд. А т.к. AI у нас получился бесхитростный, на некоторых задачах мои результаты так и остались лучшими. В воскресенье вечером мы залили всё что этот наш AI нарешал. В понедельник все кроме Minoru ушли на работу. Поэтому в понедельник Minoru ещё что-то допилил в AI и отправил решения по тем задачам, по которым их удалось улучшить.
Собственно, на этом и всё.
«А мораль сей басни... а не будет сегодня никакой морали, дети, идите сами думайте» (с) не помню откуда.

пятница, октября 28, 2011

Введение в прикладное программирование под GNU/Linux

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

Аудитория

Эта статья расчитана на два вида читателей. Во-первых, это люди, имеющие опыт программирования под MS Windows, но не имеющие такого опыта под GNU/Linux. Во-вторых, это люди, не имеющие опыта программирования вовсе. Однако, я предполагаю, что читатель в общем знаком с общепринятой в программировании терминологией, и ему не нужно объяснять, например, что такое «программа», «функция», «компилятор» или «отладка».

Средства разработки

Я буду рассматривать разработку с использованием тех средств, которые являются наиболее «родными» для GNU/Linux. К ним относятся:

  • Язык программирования C

  • Командная оболочка bash

  • Текстовые редакторы Vim и Emacs

  • Компилятор GCC

  • Отладчик GDB

  • Утилита для сборки проекта GNU make

  • Система управления версиями Git

  • Оконная система X11

Выбор именно этих средств не является догмой. Каждое из выше перечисленных средств может быть при желании заменено на другое. Однако, обычно под фразами наподобие «среда разработки Linux» понимается именно этот набор инструментов.

Языки программирования

Наиболее «родным» языком программирования для GNU/Linux является C. Это обусловлено следующими факторами:

  • GNU/Linux заимствует многие идеи (практически, идеологию) операционной системы UNIX;

  • Операционная система UNIX была написана на языке C (собственно, этот язык создавался именно для написания этой ОС);

  • Соответственно, ядро Linux и системное окружение GNU написаны тоже на C.

Ниже я буду рассматривать разработку с использованием языка C. Однако, этот выбор не является догмой. Другими популярными при разработке под GNU/Linux языками являются C++, Python, Perl. Конечно, могут использоваться и любые другие языки.

Среда разработки

В течение последних двух десятилетий очень широкое распространение получили т.н. IDE — интегрированные среды разработки. Такая среда включает в себя текстовый редактор, компилятор, отладчик, средства сборки проекта и мн.др. Такие среды есть и под GNU/Linux (наиболее популярны Eclipse, NetBeans, IDEA, KDevelop, Anjuta). Однако, история разработки под UNIX-подобные системы показывает, что IDE не являются не только единственным, но и наиболее эффективным средством разработки. Практически, правильный ответ на вопрос «какая самая лучшая IDE под GNU/Linux» — это «GNU/Linux это и есть IDE».

Часто можно встретить мнение, что большой проект без IDE разрабатывать невозможно. Это мнение легко опровергается. Первые версии UNIX писались даже не в Vim (его тогда ещё не было), а в Ed. Это так называемый «построчный» текстовый редактор, в котором вы можете редактировать за раз только одну строку текста. Весь файл на экране не отображается. В случае с UNIX по-другому и быть не могло — у разработчиков не было никаких экранов, а общение с системой осуществлялось при помощи телетайпов. Современное ядро Linux пишется в основном в редакторах Emacs и Vim.

Многие утилиты UNIX вызывают «текстовый редактор по умолчанию». Команда, запускающая текстовый редактор по умолчанию, берётся из переменной окружения $EDITOR. Некоторые утилиты смотрят сначала в переменную $VISUAL, и, лишь если она не установлена, в переменную $EDITOR. Это исторически сложившееся поведение: к старым компьютерам зачастую не было подключено никакого дисплея, а только телетайп, поэтому запускать экранный (визуальный) редактор смысла не было. В современных дистрибутивах обычно по умолчанию оказывается EDITOR=vi или EDITOR=nano. Указать использование другого редактора для одной команды можно так:

EDITOR=emacs some-command

Чтобы использовать нужный редактор по умолчанию всегда, нужно добавить в файл ~/.profile строчку типа

export EDITOR=emacs

Исторически сложилось так, что «настоящими» текстовыми редакторами для программистов являются только Vim и Emacs (просто из-за того, что у них самая долгая история развития именно в качестве текстовых редакторов для программистов). Остальные редакторы находятся в положении догоняющих.

Командная оболочка

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

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

ОС семейств DOS и Windows заимствовали некоторые функции командной оболочки из UNIX, однако их авторы пошли на существенные упрощения, из-за чего функционал COMMAND.COM и cmd.exe получился сильно урезанным. PowerShell вполне на уровне, но работает существенно по-другому.

В рамках этой статьи я ограничусь использованием командной оболочки bash (как наиболее распространённой и используемой по умолчанию в большинстве дистрибутивов) для запуска компилятора и других средств разработки. Хороший обзор использования командной оболочки можно найти, например, в известной книге [kernigan_pike].

Документация

Все средства разработки и библиотеки в GNU/Linux обычно довольно хорошо документированы. Традиционно для документации используется специальный формат и утилита для его просмотра — man. Документация в системе делится на несколько разделов:

  1. Команды пользователя (например, ls, gcc или man)

  2. Системные вызовы — API ядра ОС

  3. Библиотечные функции

  4. Драйвера и т.п

  5. Форматы файлов

  6. Игры и т.п

  7. Различные обзоры подсистем

  8. Команды, используемые для системного администрирования

Для вызова раздела документации по имени нужно указать это имя при вызове команды man (например, man ls). Иногда разделы с одинаковым названием есть сразу в нескольких разделах документации документации. Указать конкретный раздел можно при вызове man (например, man 3 printf).

Более подробную информацию о справочной системе man см. в man man.

Утилиты системного окружения GNU часто используют для документации формат info. См., например, info Coreutils.

Компилятор

Сейчас существует много компиляторов языка C, более-менее совместимых с различными стандартами. Тем не менее, пока что в среде GNU/Linux наиболее применимым остаётся компилятор C, входящий в комплект GNU Compilers Collection (GCC). Этот компилятор, кроме стандарта C, поддерживает некоторое количество расширений стандарта. Эти расширения, в частности, широко используются в исходных текстах ядра Linux. В последнее время появляются компиляторы, способные скомпилировать ядро Linux (например, llvm-clang, или EKO).

Компилятор GCC запускается из командной оболочки командой вида

gcc [OPTIONS] program.c

где program.c — имя входного файла. Кроме того, по стандарту POSIX, компилятор может быть запущен командой cc program.c (cc — от "C compiler").

При обычном запуске компилятор пытается создать исполняемый файл. По умолчанию, выходной файл называется a.out (такое название осталось от древних версий UNIX). Другое название можно задать с помощью опции компилятора -o, например,

gcc -o program program.c

При сборке программы из нескольких модулей компилятору можно подавать на вход несколько исходных файлов или файлов объектного кода, например,

gcc -o program main.c module1.o module2.o …

Чтобы только скомпилировать один исходный файл в объектный код (не пытаясь собрать исполняемый файл), нужно дать команду вида

gcc -c module.c

(имя выходного файла по умолчанию будет module.o).

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

Статическая библиотека в UNIX-подобных системах представляет собой архив (старинного формата ar), включающий набор объектных файлов. Такой архив создаётся командой вида

ar r libsomething.a module1.o module2.o …

Имена файлов библиотек традиционно начинаются с префикса lib.

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

gcc -shared -o libsomething.so module1.c module2.c …

Для использования библиотеки при сборке программы её нужно указать компилятору при помощи опции -l, например

gcc -o program -lm program.c

(здесь будет использоваться файл библиотеки libm.so, префикс lib компилятор подставляет по умолчанию). По умолчанию компилятор собирает программу, использующую динамические библиотеки. Если нужно использовать статические версии библиотек, компилятору нужно указать опцию -static.

Подробную информацию об опциях gcc см. в man gcc.

Hello, world!

Считается, что традиция начинать изучение языка программирования с написания программы, выводящей строку "Hello, world!", пошла с книги Кернигана и Ричи "Язык C" [kernigan_richie]. В случае с языком C эта программа выглядит следующим образом:

#include <stdio.h>

int main(int argc, char* argv[]) {
printf("Hello world!\n");
return 0;
}

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

gcc -o hello hello.c

Впрочем, в случае такой простой программы достаточно дать команду

make hello

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

./hello

Порядок сборки

Остановимся несколько подробнее на том, что именно делает компилятор. Порядок действий компилятора C традиционен, и применяется компиляторами некоторых других языков.




На входе компилятор имеет в общем случае набор файлов с исходными текстами. Перед началом собственно компиляции эти файлы обрабатываются т.н. препроцессором (программа cpp). Главная функция этой программы — выполнение директив вида #include . Встретив такую директиву, препроцессор вставляет содержимое указанного файла (в данном случае, stdio.h) на место этой директивы. Препроцессор понимает ещё некоторые директивы, но сейчас на них останавливаться я не буду.

После препроцессора выполняется собственно компиляция. Из исходных файлов на этом этапе получаются т.н. объектные файлы. Это файлы, содержащие исполняемый машинный код, но ещё не готовые для запуска. Главное, чего в них недостаёт — это адреса вызываемых библиотечных функций. Например, код функции printf() содержится в библиотеке libc. А в объектном файле содержится только имя этой функции. Кроме того, объектный файл содержит имена всех объявленных в нём функций.

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

Собственно программа gcc представляет собой так называемый драйвер (driver). Она запускает упомянутые выше программы (или только некоторые из них, в зависимости от опций), чтобы получить исполняемый файл.

Второй пример: решение квадратных уравнений

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

#include <stdio.h>
#include <math.h>

/* solve: calculate roots of square equation.
* a, b, c are coefficients in equation.
* Roots would be stored at x1, x2.
* Return value: count of real roots.
*/
int solve(double a, double b, double c,
double* x1, double* x2) {
double D = b*b - 4*a*c;
double sqrtD;

if (D > 0) {
sqrtD = sqrt(D);
*x1 = (-b - sqrtD)/(2.0 * a);
*x2 = (-b + sqrtD)/(2.0 * a);
return 2;
} else if (D < 0)
return 0;
else {
*x1 = -b/(2.0*a);
return 1;
}
}

int main (int argc, char* argv[]) {
double a,b,c;
double x1, x2;
int roots_count;

// Input coefficients
printf("A: ");
scanf("%lf", &a);
printf("B: ");
scanf("%lf", &b);
printf("C: ");
scanf("%lf", &c);

// Solve the equation
roots_count = solve(a,b,c, &x1, &x2);

// Output results
switch (roots_count) {
case 0:
printf("No (real) roots.\n");
break;
case 1:
printf("One root: %0.4lf\n", x1);
break;
case 2:
printf("Two roots: %0.4lf and %0.4lf\n",
x1, x2);
break;
}

return 0;
}

По аналогии с предыдущим примером, запишем этот текст в файл square.c и попытаемся скомпилировать его командой

gcc -o square square.c

Но на этот раз мы получим ошибку примерно такого вида:

/tmp/cc6RNFIi.o: In function `solve': square.c:(.text+0x6d): undefined reference to `sqrt' collect2: ld returned 1 exit status

В чём здесь дело? Ясно, что компилятору почему-то не понравился вызов функции sqrt(). Причём, он жалуется уже не на файл исходного кода, а на объектный файл (вот этот cc6RNFIi.o). Это означает, что исходный файл благополучно скомпилировался, а проблемы возникли на стадии компоновки (что можно видеть и по упоминанию в тексте ошибки программы ld — это стандартный в GNU/Linux компоновщик). Компоновщик не смог найти функцию sqrt(). В данном случае, это произошло из-за того, что эта функция содержится в библиотеке libm, а мы не просили компилятор использовать её. Чтобы избавиться от этой ошибки, нам нужно изменить команду компиляции на следующую:

gcc -o square -lm square.c

Такая команда должна отработать без ошибок и создать исполняемый файл square.

При сборке любой достаточно сложной программы нам придётся использовать несколько библиотек, и, возможно, понадобится указывать ещё какие-то опции компилятору. Команда может получиться довольно длинная. Что же, каждый раз набирать её вручную? Нет. Один из принципов философии UNIX гласит: «Всё, что может быть автоматизировано, должно быть автоматизировано». Здесь нам пригодится одна из древнейших UNIX-утилит — программа make. Чтобы воспользоваться ею, нужно написать файл с именем Makefile (в той же директории, что и наш исходный файл) со следующим содержимым:

square: square.c         $(CC) -o $@ -lm $<

Теперь собрать исполняемый файл можно просто дав команду make. Как это работает?

Make

Утилита make предназначена для сборки программ (хотя может использоваться для автоматизации многих других похожих задач). Она читает файл с именем Makefile и видит в нём набор правил. Каждое правило определяет три вещи: цель (goal, т.е. то, что нужно собрать), список исходных файлов и набор команд, которые нужно выполнить, чтобы собрать цель из исходных файлов. В примере выше, square — это имя цели, square.c — единственный в данном случае исходный файл (если их несколько, они перечисляются через пробел), а вторая строчка — команда. В команде могут использоваться переменные. Некоторые из переменных имеют специальное значение. В частности, в любом правиле $@ обозначает имя цели, а $< — первый исходный файл. Переменная $(CC) указывает на компилятор C, используемый в системе по умолчанию (в большинстве случаев это gcc, но бывает и что-нибудь другое).

В имени цели и списке исходных файлов может использоваться подстановочный символ %. Например, такое правило:

%.o: %.c   $(CC) -c $<

обозначает, что файлы с именем, заканчивающимся на .o, нужно собирать из соответствующих файлов с суффиксом .c.

Кроме того, make заранее знает некоторое количество правил по умолчанию. Среди них есть упомянутое в последнем примере, а также правило

%: %.c   $(CC) -o $@ $<

Благодаря этому правилу, в примере с «Hello, world!» просто команда make hello запускала cc -o hello hello.c.

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

Более подробную информацию об этой утилите см., например, в man make.

Управление версиями

Для управления версиями исходного кода может использоваться любая VCS. Однако, раз уж мы говорим о GNU/Linux, рассмотрим вкратце систему, используемую для разработки ядра Linux: git. По git существует довольно обширная документация, в т.ч. и на русском языке. См. например, мою статью [vcs_git] или известную серию статей [los-t_git].

Для начала использования git нужно создать репозиторий — хранилище для версий файлов. Это делается командой

git init

Теперь можно добавлять файлы в репозиторий. Но нам не нужно отслеживать версии некоторых файлов, а именно: объектных файлов и исполняемых файлов. Чтобы сразу исключить их из рассмотрения git, напишем файл .gitignore следующего содержания:

*.o square hello

Теперь команда

git add .

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

git commit

По этой команде откроется текстовый редактор по умолчанию. Тут нужно будет написать комментарий к коммиту. В данном случае достаточно строчки типа «Initial commit».

Отладка

Для отладки в Linux используется отладчик gdb. Но сначала, для того, чтобы программу было удобно отлаживать, её нужно скомпилировать с опцией -g. Сейчас нам достаточно изменить Makefile, приведя его к виду

square: square.c         $(CC) -o $@ -lm -g $<

и пересобрать программу.

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

Отладка запускается командой вида

gdb ./square

Если отлаживаемой программе нужно передать какие-то опции командной строки, их можно указать здесь же:

gdb ./some-program -a -b

При запуске отладчика появляется приглашение командной строки вида:

GNU gdb (GDB) 7.2-ubuntu Copyright (C) 2010 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later  This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.  Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". For bug reporting instructions, please see: ... Reading symbols from /home/portnov/LUG/src/square...done. (gdb)

Работа с отладчиком, в общих чертах, напоминает работу с командной оболочкой. Вы вводите команды, отладчик их исполняет. Как и в командной оболочке, работает автодополнение команд по клавише Tab. Кроме того, для краткости можно сокращать команды до первых нескольких букв — лишь бы избежать неоднозначности.

К наиболее часто используемым командам относятся:

list

Напечатать очередной кусок исходника (печатается 10 строк). Можно указать конкретные номера строк после имени команды, например l 10,15.

run

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

break

Установить точку останова. Номер строки, на которой нужно установить точку останова, указывается после имени команды.

next

Выполнить одну строку программы.

print

Вычислить и напечатать выражение. Выражение указывается после команды. Таким образом можно, например, однократно посмотреть значение какой-нибудь переменной.

display

Добавить выражение к списку постоянно отображаемых. Значения этих выражений будут показываться после исполнения каждой команды. Рядом с каждым выражением печатается его номер в списке. Удалить выражение из списка можно командой undisplay с номером выражения.

quit

Выход из отладчика.

Более подробную информацию по GDB см. в man gdb.

Оконная система X11

Исторически в UNIX не было и не могло быть никакой графической среды, потому что не было графических дисплеев. Графическая среда для UNIX появилась примерно тогда, когда появились распространённые графические дисплеи: в 1984. Сначала она называлась W (от Window), затем её усовершенствовали и назвали следующей буквой алфавита — X, следующая версия называлась X2… Сейчас имеем X11.

X11 представляет собой, прежде всего, сетевой протокол поверх TCP/IP и UDP/IP. У протокола есть клиент и есть сервер. Клиент посылает последовательность запросов вида «нарисуй мне окошко», «нарисуй на нём кнопочку», а сервер их исполняет. Один из главных принципов X11 — «определять механизмы, а не политики». Протокол предоставляет возможность, скажем, рисовать окошки, а как именно они будут отображаться — не определяет.

Наиболее распространённым X-сервером сейчас является Xorg (http://x.org); всё ещё жив XFree86; под Windows актуален Xming; выпускаются аппаратные X-серверы — комплекты «монитор + клавиатура + мышка», в которых поддержка серверной функциональности X11 реализована аппаратно — такие комплекты используются в качестве графических терминалов.

Протокол X11, в отличие от, скажем, HTTP, является бинарным, а не текстовым — это сделано из соображений экономии пропускной способности сетевого соединения и простоты разбора запросов сервером. Но это усложняет создание клиентов этого протокола: собирать замысловатые бинарные X11-запросы заведомо сложнее, чем, например, текстовые HTTP-запросы. Поэтому для написания X-клиентов используются специальные библиотеки функций, формирующих и отправляющих серверу X-запросы. Наиболее распространена библиотека libX11. Более современным вариантом является libxcb.

Запросы X11 весьма низкоуровневые. Например, чтобы реализовать функциональность кнопки, нужно нарисовать в окне прямоугольник, написать в нём текст, ждать в цикле нажатия кнопки мыши, и при каждом нажатии проверять, где щёлкнули — внутри прямоугольника или вне него. Поэтому стали появляться так называемые тулкиты — библиотеки, являющиеся высокоуровневыми обёртками над libX11.

Исторически первым тулкитом был Athena3D. Потом были Motif и Tk. Сейчас наиболее распространены GTK+ и Qt (Qt, строго говоря, представляет собой не X11-тулкит, а многоцелевой кроссплатформенный набор библиотек, который может использоваться в качестве X11-тулкита).

Hello, world на GTK+

В качестве примера рассмотрим следующую программу. Она показывает окно с одной кнопкой. При нажатии на эту кнопку появляется сообщение «Hello, world».

#include <gtk/gtk.h>

// This function displays message dialog.
// main_window parameter should be set to parent window of the dialog.
void message_box (GtkWindow* main_window, gchar *message) {
GtkWidget *dialog, *label, *content_area;

// Create a dialog
dialog = gtk_dialog_new_with_buttons ("Message",
main_window,
GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_STOCK_OK,
GTK_RESPONSE_NONE,
NULL);
// Create a label
content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
label = gtk_label_new (message);

// On "response" signal (it's called when user clicks a button in
// the dialog), destroy the dialog.
g_signal_connect_swapped (dialog,
"response",
G_CALLBACK (gtk_widget_destroy),
dialog);

// Add a label
gtk_container_add (GTK_CONTAINER (content_area), label);
// Show the dialog
gtk_widget_show_all (dialog);
}

// Callback for delete-event signal
static gboolean delete_event( GtkWidget *widget,
GdkEvent *event,
gpointer data )
{
// If return TRUE, window will not be closed.
// This may be used to preven closing window in some situations.
return FALSE;
}

// Callback for destroy signal
static void destroy( GtkWidget *widget,
gpointer data )
{
// End main GTK+ event loop
gtk_main_quit ();
}

// Callback for button click
static void hello ( GtkWidget *widget,
gpointer data )
{
// "data" parameter represents main window here
message_box(GTK_WINDOW(data), "Hello, world!");
}

int main( int argc,
char *argv[] )
{
GtkWidget *window;
GtkWidget *button;

// Init GTK+
gtk_init (&argc, &argv);

// Create main window
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

// Set up callbacks for some signals
g_signal_connect (window, "delete-event",
G_CALLBACK (delete_event), NULL);

g_signal_connect (window, "destroy",
G_CALLBACK (destroy), NULL);

// Set window borders width
gtk_container_set_border_width (GTK_CONTAINER (window), 10);

// Create labeled button
button = gtk_button_new_with_label ("Hello World");

// Set up callback for "clicked" signal of the button.
// Pass main window as second parameter.
g_signal_connect (button, "clicked", G_CALLBACK (hello), (gpointer)window);

// Pack the button into window
gtk_container_add (GTK_CONTAINER (window), button);

// Show the button
gtk_widget_show (button);

// Show the window
gtk_widget_show (window);

// Run main GTK+ event loop.
gtk_main ();

return 0;
}

Собирается эта программа командой вида

gcc -o gtk-hello $(pkg-config --cflags gtk+-2.0) $(pkg-config --libs gtk+-2.0) gtk-hello.c

GTK+ — довольно сложно устроенный набор библиотек, поэтому, чтобы не указывать все нужные библиотеки и опции компилятора вручную, мы используем здесь программу pkg-config, которая печатает опции компилятора, нужные для использования GTK+.

Дополнительная литература

[raymond] Реймонд, Эрик С. Искусство программирования для UNIX. — Пер. с англ. — М.: Издательский дом «Вильямс», 2005. — 544с., ил.

[kernigan_pike] Керниган Б., Пайк Р. UNIX. Программное окружене. — Пер с англ. — СПб: Символ-Плюс, 2003. — 416с., ил.

[kernigan_richie] Керниган Б., Ритчи Д. Язык программирования C. — Пер. с англ. — Москва: Вильямс, 2006. — 304 с.

[vcs_git] Портнов И. Системы контроля версий. Git. URL: http://iportnov.blogspot.com/2008/06/git.html

[los-t_git] Los-t. Git guts (серия статей в ЖЖ). URL: http://los-t.livejournal.com/tags/git+guts


вторник, августа 24, 2010

Душа?

Я, кажется, понял, что такое душа :) По крайней мере, с точки зрения кибернетики.

Понятно, что человек — достаточно сложное устройство. Так что будем рассматривать устройство попроще, а именно: чёрный ящик с десятью кнопочками и десятью лампочками. В зависимости от нажатия кнопочек загораются какие-то лампочки. Можно выделить два типа поведения таких ящиков:

  1. Какие лампочки сейчас горят, зависит только от того, какие кнопочки сейчас нажаты, и ни от чего больше.

  2. Включение лампочек зависит не только от того, какие кнопки нажаты, но и от порядка нажатия.

Пусть мы хотим моделировать такие ящики на Haskell. Пусть мы ввели тип X для обозначения множества нажатых в данный момент кнопок, и тип Y — для множества горящих лампочек. Например, это можно сделать так:

import qualified Data.Set as S
type Button = Int
type Lamp = Int
type X = S.Set Button
type Y = S.Set Lamp

Тогда ящик из пункта 1) можно полностью описать функцией с типом

f1 ∷ X -> Y

Это так называемая чистая функция. Заметим, что такая функция не может создавать слишком сложного поведения ящика. Действительно, если f1 очень замысловатая, то, тыкая в кнопки на ящике, мы не сможем разгадать устройства функции f1. Но мы сможем составить табличку из двух колонок: «нажатые кнопочки», «горящие лампочки». Формально говоря, эта таблица — график функции f1. Строк в этой таблице должно быть всего-то навсего 2^10, т.е. 1024. Пользуясь этой таблицей, мы легко сможем предсказывать поведение ящика, сколь бы сложной ни была функция f1.

Ящик же из пункта 2) можно описать функцией с более сложным типом:

import Control.Monad.State
f2 ∷ X -> State St Y

где St — какой-нибудь более или менее замысловатый тип данных. Гипотеза: чем сложнее тип St, тем сложнее может быть поведение функции f2. Возьмём сначала очень простой тип: type St = Bool. Тогда f2 представляет собой конечный автомат с двумя состояниями. Ничего принципиально более сложного, чем двухтактный счётчик, из такого автомата не сделать. Возможно, например, такое поведение: при чётных нажатиях на первую кнопку загорается пятая лампочка, а при нечётных - десятая. Этот алгоритм легко разгадать наблюдениями, никакой загадочности в нём нет. Если type St = Int, то можно придумать уже гораздо более сложное поведение. А уж если, скажем,

data Tree a = Node a | Branch [Tree a]
type St = Tree [Maybe Int]

или что-нибудь ещё такое понавороченнее, то поведение f2 может быть очень сложным и даже загадочным. В процессе нажимания на кнопочки на таком ящике и наблюдения за лампочками нас вполне может посетить мысль, что в ящике сидит какой-то инопланетянин, а кнопочки и лампочки — это у него такой способ общения. Ну, я думаю, вы уже поняли мою гипотезу: душа — это и есть вот это состояние в смысле монады State!

Отсюда следуют некоторые интересные мысли. Например, мы часто говорим о человеке что-нибудь вроде «у него сложный характер» или там «у него настоящая загадочная русская душа»… Оказывается, в указанном выше смысле, понятию сложности души можно придать вполне конкретный смысл — это просто сложность типа данных! :)

Или рассмотрим какую-нибудь очень сложную программу, например банковскую опердень или ОС Windows. Такие программы обладают одновременно двумя свойствами:

  • Их поведение во многих случаях загадочно, далёкие от ИТ пользователи даже часто склонны их одушевлять;

  • Они написаны так, что содержат изменяемое состояние, причём это состояние само по себе очень сложное (ну, например, состояние базы данных).

В свете рассуждений выше мы видим, что одновременность появления этих свойств отнюдь не случайна. «Душа» таких программ по сложности сопоставима с душой человека, что ж удивляться, что она загадочна? Кроме того, оказывается, те далёкие от ИТ пользователи правы в вышеприведённом смысле.

пятница, июля 23, 2010

Вычислимость, λ-исчисление, теория типов, автоматизация доказательств

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

λ-исчисление

λ-исчисление было создано в начале 50-х гг. XX века для формализации понятий вычисления и вычислимости в математике. λ-исчисление оперирует символами и λ-выражениями. Символ — это одиночный абстрактный объект, иногда символы называют переменными. Символы обозначают маленькими латинскими буквами: x,y,z… λ-выражение определяется рекурсивно:

  • Если x — это символ, то x — это λ-выражение;

  • Если x — символ, а E — λ-выражение, то запись λx.E — тоже λ-выражение;

  • Если E — λ-выражение, то (E) — λ-выражение;

  • Если E1 и E2 — λ-выражения, то E1 E2 — тоже λ-выражение.

Выражения вида λx.E называют λ-функциями, или просто функциями. Если в таком выражении символ x встречается в выражении E, он называется связанным. Несвязанные символы, встречающиеся в λ-выражении, называются свободными.

Для любого λ-выражения E можно записать выражение λx.E, где x — свободный символ выражения E (и, таким образом, связать символ x). Эта операция называется λ-абстракцией.

Введём обозначение: записью E[x=T] будем обозначать выражение, полученное из E заменой всех вхождений символа x на T.

Над λ-выражениями можно производить следующие операции:

α-конверсия

E → E[x=y]; т.е. переменные можно переименовывать;

β-редукция

(λx.E1) E2 → E1[x=E2]; это подстановка;

η-редукция

λx.(E x) → E (избавление от лишней абстракции).

Видно, что α-конверсию можно применить к любому выражению, а редукции — только к выражениям определённого вида.

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

Теорема. Если у выражения есть нормальная форма, то она только одна. Любая последовательность редукций приведёт к этой нормальной форме.

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

Example 1: Пример.

ω = (λx.x x) (λx.x x)

ω = (λx.x x) (λy.y y) = (λy.y y) (λy.y y) = (λy.y y) (λz.z z) = (λz.z z) (λz.z z) = …

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

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

Это вполне соответствует ситуации с программами для машины Тьюринга, среди которых есть как завершающиеся, так и не завершающиеся (работающие бесконечно).

Доказывается, что λ-исчисление тьюринг-полно, то есть любой алгоритм, который можно записать для машины Тьюринга, можно записать в виде λ-выражения, и наоборот. В качестве иллюстрации рассмотрим, как вводятся в λ-исчислении некоторые привычные в программировании сущности.

Введём обозначение: λx.λy.λz.λ…E будем записывать как λx y z….E.

Натуральные числа.

За 1 примем выражение λx.x. Для каждого «числа» E следующим числом будем считать выражение λx.E. Таким образом, двойке будет соответствовать выражение λx y.y, тройке — λx y z.z, и т.д. Далее определения действий над числами вводятся как в аксиоматике Пеано. Такая запись чисел называется кодировкой Чёрча.

Выражения «если-то».

За логическую истину примем выражение λx y.x, за логическую ложь — λx y.y. Тогда выражение λc x y.c x y будет соответствовать конструкции «if-then-else». Действительно,

  • if TRUE A B = (λc x y.c x y) (λx y.x) A B = (λx y.x) A B = A;

  • if FALSE A B = (λc x y.c x y) (λx y.y) A B = (λx y.y) A B = B. Кроме того, оказывается, что можно ввести и обычные логические действия — and, or и т.д.

Пары (кортежи из двух элементов).

Пусть

pair = λf.λs.λb.b f s
fst = λp.p TRUE
snd = λp.p FALSE

Тогда выражение pair x y создаёт кортеж (x,y), функция fst возвращает первый элемент кортежа, snd — второй. Действительно,

pair x y = λb.b x y;
fst (pair x y) = (λp.p TRUE) (λb.b x y) = (λb.b x y) TRUE = TRUE x y = x;
snd (pair x y) = (λp.p FALSE) (λb.b x y) = (λb.b x y) FALSE = FALSE x y = y.

Кортежи из трёх и более элементов, очевидно, можно составлять из пар, например pair x (pair y z) — кортеж из трёх элементов.

Проблема останова

Проблема останова ставится следующим образом. Дан алгоритм (записанный в виде программы для машины Тьюринга или в виде λ-выражения). Нужно, не выполняя его, выяснить, завершается ли он или работает бесконечное время.

Теорема. Проблема останова в общем случае неразрешима.

Дальнейшее развитие теории вычислимости шло в направлении выяснения классов алгоритмов, для которых проблема останова разрешима. В случае с теорией машины Тьюринга такая задача не решена до сих пор. В случае с λ-исчислением решением стала теория типов.

Теория типов

Введём в λ-исчисление типизацию. Именно, кроме символов и выражений, теперь в теории будут фигурировать типы — тоже абстрактные объекты. Типы будем обозначать маленькими греческими буквами: τ, σ… Любое λ-выражение должно иметь тип, и при том только один. Это записывается как E : τ. Типы определяются также рекурсивно:

  • Если τ — тип, то (τ) — тип;

  • Если τ и σ — типы, то τ → σ — тип.

Типы без стрелок и других операций называются простыми типами. Все символы имеют простые типы.

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

  • Если x : τ и E : σ, то (λx.E) : τ → σ;

  • Если F : τ → σ и x : τ, то (F x) : σ.

При этом если в выражении F x окажется, что F : τ → σ и x : τ1, причём τ ≠ τ1, то говорят, что выражение неверно типизированное. В типизированном λ-исчислении рассматриваются только верно типизированные выражения.

Тип называется населённым, если существует хотя бы одно λ-выражение, имеющее такой тип.

Example 2: Пример.

Попробуем типизировать выражение λx.x x. Пусть x : τ. Тогда, чтобы к x, стоящему в выражении последним, можно было применить предыдущий x, этот предыдущий x должен иметь тип τ → σ, где σ — какой-то ещё тип. Но (x : τ) и (x : τ → σ) не может выполняться одновременно, т.к. у каждого символа может быть только один тип. Пришли к противоречию. Итак, рассматриваемое выражение неверно типизировано, а значит, неверно типизировано и упомянутое в предыдущем примере выражение ω. Таким образом, оказалось, что невычислимое выражение ω не входит в типизированное λ-исчисление.

Оказывается, что верна следующая

Теорема. Если λ-выражение верно типизировано, то оно вычислимо.

Таким образом, в рамках типизированного λ-исчисления проблема останова решается очень просто: все выразимые в этой системе алгоритмы завершаются.

Неудивительно, что при этом оказывается, что эта система не является тьюринг-полной. Т.е. существуют алгоритмы, которые можно представить в виде программы для машины Тьюринга, но нельзя записать в рамках типизированного λ-исчисления. Из этого можно сделать два замечания:

  • Само по себе типизированное λ-исчисление не несёт большой практической ценности, т.к. не позволяет выразить многие алгоритмы;

  • С другой стороны, т.к. множество типизированных выражений является подмножеством нетипизированных, появляется намёк на решение проблемы останова: чтобы выяснить, вычислимо ли данное выражение, нужно только проверить, является ли оно верно типизированным. Неудивительно, что как раз эта задача (проверить возможность типизации для произвольного выражения) оказывается неразрешимой.

Для возможности практического применения λ-исчисления разработаны системы типов, накладывающие меньше ограничений, чем вышеприведённая. Такие системы:

  • достаточно выразительны, т.к. являются тьюринг-полными;

  • вывод типов в них оказывается разрешимой задачей;

  • но проблема останова, как и в случае нетипизированного λ-исчисления, неразрешима.

На таких "промежуточных" системах типов основаны имеющие практическое применения функциональные языки программирования.

Автоматизация доказательств

Для нужд практического программирования в теории типов обычно добавляют такое правило:

  • Если τ и σ — типы, то τ*σ и τ+σ — тоже типы.

В λ-исчислении можно определить понятие пары (E1,E2) и понятие «одно из двух» E1|E2. При этом оказывается, что:

  • Если E1 : τ, E2 : σ, то (E1,E2) : τ*σ,

  • и E1 | E2 : τ+σ.

Оказывается, что между терминами теории типов и терминами логики высказываний существует естественное соответствие. Именно:

  • Простой тип τ соответствует простому высказыванию;

  • Сумма типов соответствует дизъюнкции;

  • Произведение типов — коньюнкции;

  • Стрелка — импликации.

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

Теорема (изоморфизм Карри-Ховарда). Теорема логики высказываний верна тогда и только тогда, когда соответствующий ей тип населён, т.е. любое λ-выражение, имеющее этот тип, можно рассматривать как доказательство теоремы.

Более сложные, чем вышеприведённая, системы типов позволяют записывать не только теоремы логики высказываний, но и теоремы логики более высоких порядков. При этом изоморфизм Карри-Ховарда остаётся в силе.

Задача «по данному типу найти выражение, имеющее такой тип, или доказать, что таких выражений нет» для многих классов типов решена. Таким образом, задача «по данной теореме найти её доказательство или опровержение» сводится к следующему:

  • Записать данную теорему в виде типа в одной из систем типов;

  • Найти выражение, населяющее этот тип;

  • Записать это выражение на языке предметной области.

четверг, января 01, 2009

Некоторые хитрости в использовании xmonad

Некоторое время назад я публиковал здесь статьи по настройке ion3. Всё течёт, всё меняется, и сейчас я использую другой фреймовый оконный менеджер - xmonad. С русской документацией по нему сейчас дело обстоит лучше, чем обстояло с ion3, когда я начал писать о нём. Именно, есть довольно основательная статья xmonad: функциональный оконный менеджер. Так что с вопросами "что такое xmonad" отсылаю туда. Однако, когда есть одна только вводная документация - этого всё-таки недостаточно. Хочется примеров настройки и всяческих вкусностей. И их есть у меня! ;)

Во вводных статьях по xmonad обычно рассматриваются три стандартные "компоновки" (способа автоматического расположения окон): Full, Tall и Mirror Tall. "Контрибы" xmonad содержат ещё довольно много компоновок, однако даже базовые могут использоваться более чем одним способом. Например, компоновку "Tall 1 (1/100) (2/3)" (что означает: одно мастер-окно, занимающее по ширине 2/3 экрана, за раз ширина его может меняться на 1/100) я использую для чтения документов и книг: основную часть экрана занимает окно документа, а сбоку может быть что-то ещё. Конечно, такую компоновку можно сделать и "на ходу" из стандартной Tall несколькими нажатиями (по умолчанию) mod-l, но если есть уже сделанная заготовка - проще обратиться к ней. Итак, "хитрость" первая: делайте "заготовки" из настроенных компоновок, чтобы потом быстро к ним обращаться.

Для того, чтобы удобнее было обращаться к конкретным компоновкам, есть расширение XMonad.Layout.Named. Делаем

import XMonad.Layout.Named

и потом в определении layoutHook описываем компоновки, давая им имена. Например, вместо tiled пишем named "dwmtiled" tiled, где "dwmtiled" - выбираемое вами имя компоновки.

По умолчанию для переключения компоновок используются сочетания mod-space (следующая компоновка) и mod-shift-space (предыдущая). Однако, когда компоновок больше чем 2-3, это становится неудобно. Удобнее переключаться сразу на нужную компоновку. Я использую для этого сочетания клавиш типа mod+буква. Чтобы такое себе устроить, подправьте в xmonad.hs строку с импортом модуля XMonad: вместо "import XMonad" напишите

import XMonad hiding ( (|||) )

Это мы указали, что не хотим использовать оператор ||| (служащий для перечисления компоновок), определённый в модуле XMonad. Зато мы будем использовать одноимённый оператор, определённый в модуле LayoutCombinators. Итак,

import XMonad.Layout.LayoutCombinators

Оператор ||| из LayoutCombinators "умнее", и позволяет переключаться сразу на нужную компоновку. Теперь описываем сочетания клавиш для этого переключения:

...
, ((modMask, xK_d ), sendMessage $ JumpToLayout "dwmtiled")
, ((modMask, xK_m ), sendMessage $ JumpToLayout "mirror")
...

где "dwmtiled", "mirror" - имена соответствующих компоновок.

Однако переключение на указанную компоновку - только побочная задача модуля LayoutCombinators. Главное его назначение состоит, соответственно названию, в том, чтобы комбинировать компоновки. Этот модуль содержит операторы типа ***||**. Такие операторы разбивают экран на две части, в каждой из которых работает своя компоновка. Количество звёздочек слева и справа показывает, в каком отношении разбивать экран (скажем, упомянутый оператор делит экран в отношении 3:2). Операторы с вертикальными чертами (|) делят экран по вертикали, а с наклонными (например, ***//*) - по горизонтали. Операторы, в которых две черты (вертикальные или наклонные), позволяют во время работы изменять соотношение частей экрана (перетаскивая границу мышкой), а операторы с одной чертой (например, */***) - не позволяют.

Одна проблема с LayoutCombinators состоит в том, что для перемещения окон между разными частями экрана стандартные действия (swapUp, swapDown) не работают. Для этого приходится использовать модуль WindowNavigation, который определяет модификатор компоновки windowNavigation и действие Move (с аргументом U/D/L/R, указывающим, куда двигать окно).

Вот пример использования LayoutCombinators:

-- Разделить экран по вертикали в отношении 3:1
onebig = windowNavigation (tile ***|* coltile)
where
-- компоновка для левой части
-- master-окно занимает 3/4 по высоте
tile = Mirror $ Tall 1 (1/100) (3/4)
-- компоновка для правой части
-- располагает все окна в один столбец
coltile = Tall 0 (1/100) (1/2)

Здесь onebig - это компоновка, дающая одному окну большую часть экрана (3/4 по вертикали и 3/4 по горизонтали), а остальные располагающая снизу и справа от него. Кому легче один раз увидеть, чем десять раз прочитать - вот пример использования этой компоновки (заодно это иллюстрация к предыдущей статье).

Ещё одна "хитрость" касается автоматического назначения свойств окнам (manageHook). xmonad по умолчанию делает диалоги "плавающими" (float), и это правильно. Только вот по умолчанию распознаются не все диалоги. В частности, по умолчанию xmonad не считает диалогами всплывающие окна Gimp-а (например, диалог кривых и пр). Однако это можно победить. Для таких окон приложения обычно выставляют свойство окна _NET_WM_WINDOW_TYPE в значение _NET_WM_WINDOW_TYPE_DIALOG. Можно заставить xmonad проверять это свойство:

-- подключаем библиотеки X11
import Graphics.X11.Xlib.Extras
import Foreign.C.Types (CLong)
-- Взять значение свойства окна
getProp :: Atom -> Window -> X (Maybe [CLong])
getProp a w = withDisplay $ \dpy -> io $ getWindowProperty32 dpy a w
-- Эта функция проверяет, выставлено ли свойство окна name в значение value
checkAtom name value = ask >>= \w -> liftX $ do
a <- getAtom name
val <- getAtom value
mbr <- getProp a w
case mbr of
Just [r] -> return $ elem (fromIntegral r) [val]
_ -> return False
-- Эта функция проверяет, является ли окно диалогом
checkDialog = checkAtom "_NET_WM_WINDOW_TYPE" "_NET_WM_WINDOW_TYPE_DIALOG"

Другой "пунктик" - надо ещё распознавать "отрывающиеся" (tear-off) меню. Это тоже можно сделать проверкой значения атома:

checkMenu = checkAtom "_NET_WM_WINDOW_TYPE" "_NET_WM_WINDOW_TYPE_MENU"

Объявляем соответствующие manageHook-и и добавляем их к остальным:

-- Сделать меню плавающими
manageMenus = checkMenu --> doFloat
-- Сделать диалоги плавающими
manageDialogs = checkDialog --> doFloat
-- Добавляем наши функции к остальным
myManageHook = ... <+> manageMenus <+> manageDialogs

Xmonad реализует концепцию виртуальных десктопов (здесь они называются workspaces), как, вобщем, и большинство других оконных менеджеров. Однако известна также другая концепция - теги для окон. Теги используются, например, в dwm и awesome. Их можно использовать и в xmonad. У меня сейчас используются обе концепции параллельно.

Чтобы использовать теги в xmonad, нужно подключить соответствующий модуль:

import XMonad.Actions.TagWindows

Я объявляю несколько функций, для пущей читабельности:

-- переместить окна, помеченные тегом name, на текущий workspace
showtag name = withTaggedGlobalP name shiftHere
-- вкл/выкл тег name для текущего окна
toggletag name = withFocused $ \w -> hasTag name w >>=
(\b -> if b then delTag name w else addTag name w)
-- снять тег name
remtag name = withFocused (delTag name)
-- перейти к следующему окну, помеченному тегом name
nexttagged name = focusDownTaggedGlobal name
-- переместить окна с тегом name с текущего workspace на "misc"
shiftoff name = withTaggedP name (W.shiftWin "misc")

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

-- Объявить сочетания клавиш для тега tag с клавишей tag
tagkeys mask key tag = [
((mod1Mask, key), showtag tag),
((mask, key), toggletag tag),
((mod3Mask, key), nexttagged tag),
((mask .|. controlMask, key), shiftoff tag)
]

(mod3 у меня находится слева от цифрового ряда клавиатуры). Ну и добавляем эти сочетания к остальным:

...
-- Пометить текущее окно произвольным тегом
, ((modMask, xK_t), tagPrompt defaultXPConfig (\s -> withFocused (addTag s)))
-- Снять произвольный тег
, ((modMask .|. controlMask, xK_t), tagDelPrompt defaultXPConfig)
-- Переместить окна, помеченные произвольным тегом, на текущий workspace
, ((mod1Mask, xK_t), tagPrompt defaultXPConfig (\s -> withTaggedGlobalP s shiftHere))
-- Вкл/выкл тег "mark"
, ((modMask, xK_grave), toggletag "mark")
-- Перейти к следующему окну, помеченному "mark"
, ((mod3Mask, xK_grave), focusDownTaggedGlobal "mark")
...
]
++ (tagkeys modMask xK_exclam "web")
++ (tagkeys modMask xK_numbersign "text")
++ (tagkeys modMask xK_slash "gfx")
++ (tagkeys modMask xK_semicolon "office")
++ (tagkeys modMask xK_colon "docs")
++ (tagkeys modMask xK_question "math")
++ (tagkeys modMask xK_asterisk "files")
++ (tagkeys modMask xK_percent "im")

(у меня typewriter-like раскладка клавиатуры, так что при нажатии цифровых клавиш без шифта получаются знаки препинания).

Для полного счастья надо, чтобы некоторым окнам (отбираемым, например, по классу или заголовку) сразу назначались правильные теги. Стандартного manageHook-а для этого нет, так что приходится изобретать свой. Чтобы было покороче, я просто приведу куски своего xmonad.hs:

import XMonad.Hooks.XPropManage
...
myManageHook = ignoresome <+> (xPropManageHook xPropMatches) <+> manageMenus <+> manageDialogs
ignoresome = composeAll
[ className =? "trayer" --> doIgnore
, className =? "fbpanel" --> doIgnore
, className =? "Plasma" --> doIgnore]
xPropMatches :: [XPropMatch]
xPropMatches = tagclasses ["Epiphany-browser", "Kontact", "Liferea-bin"] "web"
++ tagclasses ["gimp", "f-spot", "Inkscape", "Eog"] "gfx"
++ tagclasses ["gnome-terminal"] "term"
++ tagclasses ["Gedit", "Leafpad", "Gvim"] "text"
++ tagclasses ["Evince"] "docs"
++ tagclasses ["Nautilus"] "files"
++ tagclasses ["Amarok", "Rhythmbox", "Totem"] "media"
++ tagclasses ["Wxmaxima"] "math"
++ moveclasses ["Pidgin"] "im"
++ floatclasses ["Qwerty.py"]
where
ckClass cls = [(wM_CLASS, any (cls==))]
-- добавить тег окну
tag name = pmX (addTag name)
-- добавить тег и переместить окно
moveAndTag name = (\w -> addTag name w >> return (W.shift name))
mkfloat = pmX float
-- пометить тегом все окна с данным классом
tagclasses clss name = [ (ckClass cls, tag name) | cls <- clss ]
-- переместить окна с данным классом на воркспейс ws
moveclasses clss ws = [ (ckClass cls, moveAndTag ws) | cls <- clss ]
floatclasses clss = [ (ckClass cls, mkfloat) | cls <- clss ]

Это, конечно, далеко не все "фишки" xmonad. Однако, я надеюсь, кому-то это может послужить стартовой площадкой :)