советы.блогспот.ком

Линукс, Vim, LaTeX, полезные скрипты, визуализация данных, численные расчёты, немного ФП

20091124

Скрипт для создания статического значка Flickr

Делюсь маленьким и несовершенным скриптом, который, надеюсь, кому-то всё же окажется полезен. Скрипт генерирует картинку-бейджик с последними фотками на Flickr. Зачем он нужен? Официальные виджеты Flickr основаны на Javascript и на флэше, их не везде можно вставлять. А статическую картинку можно куда угодно вставлять, хоть в ЖЖ, хоть на форумах...

В общем, скрипт состоит, на самом деле, из двух. Первый, вспомогательный, на питоне, flickrlatest.py находит и печатает ссылки на миниатюры N последних фоток заданного пользователя. Для работы нужно иметь ключ API, который скрипт считывает из файла ~/.flickr.apikey.

Второй скрипт, основной, обычный скрипт на bash, скачивает нужные картинки и объединяет их Graphics Magick-ом (при желании легко заменить на Image Magick, но, как я недавно узнал, GM быстрее, используется и на самом фликере, так что решил переходить потихоньку на GM).

Использование второго скрипта:

mkflickrbadge.sh пользователь геометрия файл-куда-сохранить.png


Несколько примеров.

$ mkflickrbadge.sh arboreus 3x2 arboreus-3x2.png


Flickr badge 3×2


$ mkflickrbadge.sh dobrych 5x1 dobrych-5x1.png


Flickr badge 5×1


Можно использовать NSID вместо имени пользователя:

$ mkflickrbadge.sh 7333287@N07 2x2 marjina-2x2.png


Flickr badge 2×2


Надеюсь, идея понятна. Скачать скрипты можно c битбакета (архив.zip). Распаковать, сделать скрипты исполняемыми, положить где-нибудь в PATH. Как добавить в crontab — умолчу.

P.S. Отмечу в качестве альтернативы скрипту — вебсервис http://www.flickriver.com/badge/create.

20091028

Переименование переменных и слияние изменений в Darcs

Ныне к традиционным холиварам, вроде vi против emacs, прибавился ещё hg (Mercurial) против git. И то, и другое — распределённые системы управления версиями (DVCS). В чём их преимущество перед старыми централизованными системами и как пользоваться новыми давно уже написано. Впрочем, выбор этими двумя системами не ограничивается, отдельные маньяки успешно пользуются и другими системами. А среди альтернативных систем совершенно особняком стоит darcs.

Почитал я тут руководство по darcs, и обнаружил что есть у него одна удивительная возможность, которой, насколько мне известно, у его более популярных собратьев нет. А именно, поддержка замен в управляемых файлах. Например, можно переименовать в одной ветке функцию или переменную, в другой ветке делать другие изменения, затрагивающие эти же строки, а потом совершенно волшебным способом автоматически объединить изменения обеих веток. И ручное слияние изменений не потребуется. Возможность настолько необычная, что захотелось поделиться.

Основное отличие darcs от собратьев: он отслеживает не состояние каталога с файлами (и историю его изменений), а хранит сами изменения — патчи. А уж состояние рабочего каталога определяется просто как результат применения всех накопленных изменений-патчей. Всякое такое изменение обратимо, а некоторые можно безболезненно переставлять местами (и это очень облегчает слияния).

В случае обычных DVCS, каждое изменение определяется разницей двух состояний каталога. Чтобы объединить такие изменения, нужен их общий «предок», к которому изменения можно применить. В darcs изменение не обязательно должно определяться разницей между двумя состояниями каталога. Это позвляет создавать разные типы изменений, и автор может определить что именно изменение делает (семантически). В том числе есть и такой вид изменений: замена слов в файле (token replace patch).

Покажу, как это работает, а вы уж сами судите, насколько это круто :-)

Завязка



Итак, создадим вначале исходный репозиторий и поместим в него простую программку. Тут отличия между darcs и hg или git минимальны:
$ mkdir repo-0
$ cd repo-0
repo-0$ darcs init
repo-0$ cat > hello.py
def hello(what):
print "Hello %s" % what

hello("World")
^D

repo-0$ darcs add hello.py
repo-0$ darcs record -m 'initial commit' hello.py
Recording changes in "hello.py":

addfile ./hello.py
Shall I record this change? (1/2) [ynWsfvplxdaqjk], or ? for help: y
hunk ./hello.py 1
+def hello(what):
+ print "Hello %s" % what
+
+hello("World")
Shall I record this change? (2/2) [ynWsfvplxdaqjk], or ? for help: y
Finished recording patch 'initial commit'


А теперь создадим две ветки. В каждой ветке сделаем свои изменения. В одной (A) изменим название переменной what на name, а в другой (B) переименуем и перепишем функцию hello().

Внезапно!



Клонируем исходный репозиторий:
repo-0$ cd ..
$ darcs get repo-0 repo-A
Copying patches, to get lazy repository hit ctrl-C...
Finished getting.

И переименовываем в этой ветке переменную. Только хитрость, мы хотим не просто сделать замену слов в файле, а мы хотим явно указать darcs-у, что это именно замена слов. Поэтому вместо текстового редактора выполняем такую вот команду:
$ cd repo-A
repo-A$ darcs replace what name hello.py

Убеждаемся, что программка изменилась:
repo-A$ cat hello.py
def hello(name):
print "Hello %s" % name

hello("World")

И записываем изменения в репозиторий:
repo-A$ darcs record -m 'renamed: what to name' hello.py
Recording changes in "hello.py":

replace ./hello.py [A-Za-z_0-9] what name
Shall I record this change? (1/1) [ynWsfvplxdaqjk], or ? for help: y
Finished recording patch 'renamed: what to name'


Тем временем...



Параллельно создаём другую ветку и как-нибудь меняем функцию hello:
repo-A$ cd ..
$ darcs get repo-0 repo-B
Copying patches, to get lazy repository hit ctrl-C...
Finished getting.
$ cd repo-B
repo-B$ cat > hello.py
def hello(what):
if len(what) > 6:
print "Hello %s" % what
else:
print "Hi %s" % what

hello("World")
^D

Изменения настолько серьёзны, что старое имя функции уже не подходит. Переименовываем её с помощью darcs replace:
repo-B$ darcs replace hello greet hello.py

И записываем изменения:
repo-B$ darcs record -m 'changed hello and renamed to greet' hello.py
Recording changes in "hello.py":

hunk ./hello.py 2
- print "Hello %s" % what
+ if len(what) > 6:
+ print "Hello %s" % what
+ else:
+ print "Hi %s" % what
Shall I record this change? (1/2) [ynWsfvplxdaqjk], or ? for help: y
replace ./hello.py [A-Za-z_0-9] hello greet
Shall I record this change? (2/2) [ynWsfvplxdaqjk], or ? for help: y
Finished recording patch 'changed hello and renamed to greet'


Кровавый финал



А теперь возвращаемся в исходный репозиторий и объединяем изменения:
repo-B$ cd ../repo-0
repo-0$ darcs pull ../repo-A ../repo-B
Wed Oct 28 17:21:41 CET 2009 me@example.com
* changed hello and renamed to greet
Shall I pull this patch? (1/2) [ynWsfvplxdaqjk], or ? for help: y
Wed Oct 28 17:12:12 CET 2009 me@example.com
* renamed: what to name
Shall I pull this patch? (2/2) [ynWsfvplxdaqjk], or ? for help: y
Finished pulling and applying.

И что же мы видим?
repo-0$ cat hello.py
def greet(name):
if len(name) > 6:
print "Hello %s" % name
else:
print "Hi %s" % name

greet("World")

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

Я впечатлён.

20091016

Микросоветы

Всё чаще в твиттер
одной строкой пост целый
пишу на память.

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

Приёмы работы
LaTeX и вёрстка
Программирование
Находки (всякие программки)

1. Приёмы работы:

  • Чтобы не закрывать Firefox, когда закрывается последняя вкладка по Ctrl-W: идём в about:config, находим browser.tabs.closeWindowWithLastTab, ставим false. Проверено на FF 3.5.
  • OpenOffice: чтобы запретить разрыв слова (т.е. запретить перенос), вставляем нечитаемый символ U+2060 (Zero-width WORD JOINER). Символ можно найти, запустив gucharmap. Надо в .XCompose добавить...
  • Чтобы использовать новый, сжимающий раза в два лучше, видео-кодировщик Theora 1.1, нужно взять саму новую библиотечку (уже есть в Debian unstable), и, главное, ffmpeg2theora версии не ниже 0.25. На сайте разработчиков есть и бинарные сборки.
  • Принудительное отключение подсветки ЖК-дисплея: xset dpms force off. Отсюда.
  • Банальность. Удаление пустых строк sed-ом: sed '/^\s*$/d'.
  • Редактируя диаграммы Graphviz в Vim, быстрый просмотр по :make можно сделать так: :set makeprg=dot\ -Tpng\ %\ \\\|display\ png:- errorformat='' autowrite. Подставить название используемой программы (dot, neato, fdp, ...).
  • Создание паролей (если нет KeePassX): cat /dev/urandom | tr -d -c 'a-zA-Z0-9' | fold -w 8 | head -1
  • Поиск и удаление дубликатов файлов: fdupes в командной строке, fslint — утилита с графическим интерфейсом.
  • В Debian можно заменить файл пакета, не пересобирая пакет. Поможет dpkg-divert.
  • sudo -i имитирует логин под рутом (даёт #). Бывает полезно (раньше sudo su - иногда пользовался).
  • Как создавать картинки предварительного просмотра видеофайлов:
    ffmpeg -itsoffset -1 -i видеофайл.avi -vcodec mjpeg -vframes 1 -an -y -f rawvideo -s 320x240 картинка.jpg ; done
    Как создавать картинки из PDF:
    convert -thumbnail 300x300 документ.pdf[0] -gravity center -extent 300x300 картинка.png


2. LaTeX и вёрстка:

  • Рекомендуемая минимальная ширина полей, чтобы документ можно было печатать и на A4, и на Letter — А4, слева и справа 20 мм, сверху и снизу 33 мм. RFC 2346.
  • Чтобы избежать разрыва страницы в LaTeX, можно поместить фрагмент текста в окружение samepage. Это частый вопрос.
  • Отступы элементов списка в LaTeX можно настроить, если использовать окружение list вместо itemize. Пример.
  • Чтобы добавить межабзацный пробел, \setlength{\parskip}{10pt plus 1pt minus 1pt}. Особенно полезно в наборе без абзацного отступа. Отсюда.
  • Чтобы выравнять картинку и текст справа от неё по вертикали, по середине, повозившись, сделал себе макрос \sidebyside{}{}:
    \newsavebox{\leftbox}\newlength{\leftboxheight}\newcommand{\sidebyside}[2]{\sbox{\leftbox}{#1}\settoheight{\leftboxheight}{\usebox{\leftbox}}\usebox{\leftbox}\raisebox{0.5\leftboxheight}{#2}}
    Смотрите пример использования.
  • Чтобы автоматически закрывать окружения LaTeX, пользователи Vim могут поставить плагин tex_autoclose. Использование: в режиме вставки Ctrl+\, затем c.
  • Разрезать на страницы и «склеивать» PDF-документы можно с помощью pdftk. Объединить два файла в один:
    pdftk первый.pdf второй.pdf cat output новый.pdf


3. Программирование:

  • В Python, примитивное транспонирование списка пар в пару списков:
    unzip = lambda pairs: zip(*pairs)
    @vlasovskikh подсказал, что для больших списков izip будет быстрее (проверили, так).
  • Занятное и доходчивое объяснение «что такое продолжения» на 11-й минуте видеопрезентации Swarm-dpl.
  • Быстро создавать графический интерфейс для научных программок позволяет библиотечка TraitsUI (Python). Пока не пробовал, но прочитал урок по TraitsUI.
  • Говорят, Intel готовит Concurrent Collections и для Хаскеля.
  • Я же пока проснулся и прочитал про со-процедуры на Си и устройство Даффа. Впечатлился.
  • Хотите полюбоваться, как можно добавлять побочные вычисления «наследованием» типов? Вот, пожалуйста, в этом примере (на Хаскеле). Хотя это, конечно не Java.
  • Учился использвать монадные трансформеры (бррр!) — оказалось несложно. В результате получился такой пример использования StateT поверх IO. Может кому пригодится.
  • Мелкое копирование словарей в Python — грабли.


4. Находки (всякие программки):

  • Atrack — анонимный открытый битторент трекер для Google App Engine. Всего 246 строк кода.
  • Sweet Home 3D — программа для планирования интерьера. Можно рисовать планы комнат, расставлять мебель, крутить по всякому. Сделана красиво.
  • fuse-zip — файловая система FUSE для монтирования zip-архивов. Быстрая, легко собирается по make, умеет писать в архив. Использование:
    fuse-zip архив.zip /точка/монтирования
    Есть также avfs, которая монтирует любые архивы, но не пишет и не такая удобная. Её использовать так:
    mountavfs ; ls ~/.avfs/полный/путь/к/архиву.zip#/файл/в/архиве
    В Debian нужно предварительно добавить пользователя в группу fuse.
  • Python(x,y) — дистрибутив Python для научных работников, для Windows и Ubuntu. Все инструменты и библиотечки «из коробки».
  • В дополнение к своему однострочнику antiodt нашёл ещё хороший конвертер ODT в Markdown odt2txt.py.
  • Дружественный к Гному вариант Xmonad — Bluetile. Раз попробовал, и две недели им пользовался.
  • Попробовал gitit. Самая простая вики для совместной работы над математическими текстами (вместе с jsMath из коробки). Хранилище — git или darcs.
  • TxtSushi — утилитки, позволяющие выполнять SQL-запросы по простому текстовому (CSV, TSV) файлу.


Ух-ты, а немало получилось.

20090922

Как нарисовать стрелку в Inkscape

Очень люблю Inkscape, и часто в нём рисую разные схемы. А для того, чтобы рисовать схемы, нужны стрелки. Готового инструмента «стрелка» в Инкскейпе нет*, поэтому творю из подручных материалов сообразно вкусу и потребностям. В общем-то, минутное дело, умеючи...

Я подумал, что кому-то, возможно, будет интересно увидеть, как можно самому нарисовать практически любую стрелку. Предлагаю небольшой видеоурок, где показываю как нарисовать стрелку попроще и стрелку позатейливей:



Это мой первый скринкаст. Прошу строго не судить. Кино немое, просто с титрами.

* В инкскейпе есть маркеры конца и начала линии. Красить их нельзя, они всегда чёрные. Впрочем, подсказывают из зала, можно — создав стрелку, надо «оконтурить обводку», получить два контура (древко и острие), вручную их подправить-подравнять, и получится полноценная контурная стрелка. Я пока остаюсь приверженцем своего способа создания контурных стрелок. Впрочем, смотрите на другой способ сами (подал идею и сделал видео freedomfidaj).

20090917

(Новичковые) ужасы Хаскеля

Я — начинающий программист на Хаскеле, и пока я ещё помню всё, чем он страшен. И это хочу записать. Сразу скажу, когда я приступал к Хаскелю, я ещё не знал практически ничего о функциональном программировании, поэтому одновременно с языком, нужно было осваивать новые идеи и образ мысли. И вообще-то это было здорово. А у страха, как известно, глаза велики. В общем, я думаю, эта заметка может быть полезна и другим начинающим. В ней я укажу на пять непонятных мне вначале, но относительно несложных вещей, поняв которые хотя бы приблизительно, освоить язык мне было гораздо легче.

1. Ламбда-функции


— Но мы называем его лембас или путевой хлеб, он подкрепляет лучше, чем любая пища людей, и он гораздо вкуснее.


Вообще-то, именно поэтому я и выучил Хаскель. Мне было просто любопытно, что означают все эти лямбды. Очень помогла в самом начале статья Пола Худака Conception, evolution, and application of functional programming languages (PDF также здесь). Возможно, есть введения и получше, но я начинал с него.

Ламбда-выражение — самая суть «функций» — это выражение вида
\lambda x \;.\; \text{expression with $x$}
Значением этого выражения является пока ещё безымянная функция одного аргумента (x), что-то с ним вычисляющая (а именно, выражение справа от точки). С лямбда-функциями связана серьёзная математическая теория, но с точки зрения программирования можно считать \lambda ключевым словом для определения функций. Действительно, когда я и до этого уже пользовался лямбдами в Питоне (и почти во всех других современных языках они тоже есть). В Питоне они выглядят вот так:
lambda x: expression with x
Ими было очень удобно пользоваться в filter() и reduce(). И вообще, почти везде, где в качестве аргумента требуется имя функции. Однако у лямбда-функций нет имён, и именно поэтому их ещё называют анонимными (безымянными) функциями. В пайтоне иногда я давал им имена прямо на лету:
add_42 = lambda x: x + 42
Теперь имя add_42 указывает на функцию. Точно такой же результат можно было получить, записав определение функции как обычно:
def add_42(x):
return x+42
А что же насчёт Хаскеля? Да почти то же самое. Символ \ заменяет \lambda, -> служит вместо точки. Всё вместе записывается так:
\x -> выражение с x
И мы даже можем давать имена таким безымянным функциям, так же как и в Питоне:
add_42 = \x -> x + 42
Согласитесь, очень похоже.

Однако тут есть одна тонкость. Как только я начал читать о Хаскеле, я увидел лямбда-выражения, которые поначалу казались немного странными:
\x -> \y -> выражение с x и y
Что означают все эти «стрелочки»? Ответ оказался очень прост и очень полезен в дальнейшем освоении языка.

В Хаскеле все функции являются функциями одного аргумента. Поначалу это может показаться ограничением, но на деле это очень удобная и практичная идея. Любую функцию n аргументов можно представить как функцию одного аргумента, возвращающую другую функцию n–1 аргементов. И по науке это азывается каррированием. Эта идея, в частности, позволяет передавать функции только часть аргументов.

Узнав об этом, мы теперь можем читать любые выражение с множеством «стрелок»:
\x -> (\y -> выражение с x и y)
Значением такого выражения будет фукнция, берущая один аргумент и производящая другую функцию, которая берёт ещё один аргумент. Такое выражение в целом ведёт себя как функция двух аргументов. Например, мы можем вычислить такую функцию двух аргументов в интерпретаторе Хаскеля ghci:
ghci> (\x -> \y -> x + y ) 3 4
7
Конечно, есть более краткий способ записи функций двух аргументов (обратите внимание, что список аргументов брать в скобки совсем не нужно):
ghci> (\x y -> x + y) 3 4
7
Однако знать, что на самом деле все функции нескольких аргументов являются функциями одного аргумента очень полезно. Например, это помогает читать описания типов функций. Например, тип функции map выглядит так:
map :: (a -> b) -> [a] -> [b]
Я обычно читаю это следующим образом: «функция map принимает два аргумента, первый — функцию преобразующую a в b, второй — список элементов типа a, а возвращает список элементов типа b». Но иногда гораздо естественнее записать тот же самый тип так:
map :: (a -> b) -> ([a] -> [b])
«Функция, которая берёт функцию, преобразующую a в b, и возвращает функцию, преобразующую список a в список b».

Даже эти простые понятия о лямбда-функиях были уже достаточны, чтобы начать пользоваться Хаскелем и понять большинство примеров и объяснений.

2. Знак равенства


— Ну что, если тут нет смысла, — сказал Король, — тогда у нас гора с плеч: нам незачем пытаться его найти! Сэкономим кучу работы! И все же...


По-моему, знак равенства (=) — самый важный символ в Хаскеле. Понять его важно для понимания языка. И мне кажется, смысл равенства недостаточно подчёркивается во всевозможных учебниках. Например, это единственное ключевое слово, отсутствующее в списке ключевых слов Хаскеля в его вики.

В отличии от большинства императивных языков, где = означает присваивание (то есть действие), в Хаскеле он означает, что левая часть равна правой (то есть описывает свойство).

«Равна» — не значит «становится». Это означает, что нечто равно чему-то ещё. Всегда. Как в математике. a = b в Хаскеле означает, что a равно b по определению, a эквивалентно b.

Таким образом, = в Хаскеле служит для записи определений. «Равно» может определять самые разные вещи, но определяет их статично. Оно не зависит от порядка выполнения операций. На него можно положиться.

Пользователям функциональных языком это покажется слишком уж очевидным, но именно в смысле знака равенства самое важное изменение для тех, кто раньше пользовался императивными языками. Теперь, кстати, мы можем давать имена нашим безымянным функциям:
add = \x -> \y -> x + y
Признаю, что читается это плохо, поэтому в большинстве случаев функции в Хаскеле определяются так:
add x y = x + y
Но и это по-прежнему определение функции add.

3. Классы типов


Significant benefits arise from sharing a common type system, a common toolset, and so forth. These technical advantages translate into important practical benefits such as enabling groups with moderately differing needs to share a language rather than having to apply a number of specialized languages. — приписывается Б. Страуструпу


Система типов в Хаскеле просто прекрасна. В ней очень легко и естественно выражаются многие идеи. И возможно, именно классы типов — это наименее чуждая концепция для тех, кто приходит в Хаскель из процедурного и объектно-ориентированного мира. Во всяком случае, мне так показалось. Однако классы типов — это совсем не то же самое, что классы в Си++ или в Джаве. Гораздо больше они похожи на абстрактные шаблоны классов в Си++, потому что классы типов
  • определяют только абстрактный интерфейс (не имеют реализации по-умолчанию)
  • позволяют создавать несколько независимых реализаций интерфейса (таким образом, любой класс может стать экземпляром класса типов, если предоставит реализацию его методов)
  • полиморфны по своей природе и поддерживают наследование
  • не могут иметь переменных состояния

Как только мы привыкнем, что типы классов — это не классы Си++, а абстрактные интерфейсы, и экземпляры классов это не «объекты», а конкретные реализации абстрактных интерфейсов, Хаскель сразу станет привычным и уютным.

Я очень советую почитать вики-статью OOP vs type classes, которая гораздо более детально сравнивает объектно-ориентированный подход и классово-типовой.


4. Монады


И так как всякое настоящее состояние простой субстанции, естественно, есть следствие ее предыдущего состояния, то настоящее ее чревато будущим, — Лейбниц, «Монадология»


Не важно, насколько мягкое введение в Хаскель, рано или поздно его читатель упрётся лбом в крепкую стеную из монад. Да, это вам не плюшки тырить, это вам серьёзная математика. Где-то за этой стеной.

Но вот что я понял: изучать абстрактную математику совсем не обязательно, чтобы монады использовать, а они и правда очень изящная программистская техника. Вначале они казались мне немного странными, но понять раз и навсегда монады гораздо легче, чем запоминать (и правильно применять!) бесчисленные шаблоны ОО-проектирования. Монады логичней.

Поскольку тьюториалов по монадом огромное множество, я не буду их здесь повторять и ожидаю, что вы их уже прочитал парочку. Что же не так с монадами? Для человека, привыкшему к императивным языка, испорченному годами объектно-ориентированного мышления монады кажутся странными. Они выглядят как абстрактный класс-контейнер с загадочным методом >>=:
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
fail :: String -> m a
Хорошо, если return — конструктор, то почему такое чудное имя? Если это класс-контейнер, то как из него что-либо извлечь? И какой смысл применять функцию внутри контейнера (а именно это делает метод >>=, называемый также операцией связывания), если мы не можем вытащить результат из этого контейнера?

Отвечу вначале на последний вопрос. Зачем нужно связывание (>>=)? Монады являются и одновременно не являются контейнерами. Они — обёртки, упаковки для вычислений, а не для значений (return). Однако они обёртывают вычисления не для того, чтобы их было удобнее хранить в монадных коробочках, а чтобы их можно было удобнее соединять друг с другом. Вместо «коробочек» представьте обыкновенные кирпичи, которые ровно кладутся друг к другу. Это, кстати, похоже на шаблон Adapter в ОО-проектировании. Каждая монада определяет какой-то способ передавать результат от одного вычисления к другому и реализует стандартный интерфейс, чтобы этот способ использовать (>>=). И что бы ни случилось, результат всегда останется в той же монаде (даже, если произойдёт сбой, fail).

Самая простая программистская аналогия монадам, которую я придумал, это конвееры (pipes) в командной оболочке Unix. Монады обеспечивают однонаправленный конвеер для вычислений. То же самое делают и конвееры в Unix. Например:
$ seq 1 5 | awk '{print $1 " " $1*$1*$1 ;}'
1 1
2 8
3 27
4 64
5 125
seq создаёт список целых чисел. awk вычислят куб каждого из них. Что здесь замечательного? У нас есть две слабо связанные друг с другом программы, которым мы можем легко указать работать вместе. Поток текста создаваемый программой слева попадает по конвееру в программу справа, которая может читать этот поток, что-то с ним делать и создавать уже новый текстовый поток. Текстовый поток — общий формат для результата вычислений, | — операция, связывающая их воедино.

Монады очень похожи. >>= берёт внутреннее вычисление из монады слева и подставляет его в вычисление справа, которое всегда должно создавать ещё одну монаду того же типа.

Как вы уже, наверное, знаете, списки и тип Maybe в Хаскеле — монады. Например, пусть у нас есть простое вычисление, которое возвращает пару из числа и его куба обратно в монаду (return):
\x -> return (x, x^3)
тогда мы можем взять список и направить его «по конвееру» в это вычисление:
ghci> [1,2,3,4,5] >>= \x -> return (x,x^3)
[(1,1),(2,8),(3,27),(4,64),(5,125)]
Заметьте, что мы получили список пар. Это та же самая исходная монада (то есть список). Однако если мы возмьём значение Maybe и направим его в то же вычисление, на выходе у нас будет та же сама монада Maybe:
ghci> Just 5 >>= \x -> return (x,x^3)
Just (5,125)
Таким образом, мы можем создать конвеер из двух вычислений, и поведение этого конвеера зависит от контекста (т.е. от того, какая монада используется), а не от самих вычислений. В отличии от юниксовых конвееров, монады строго типизованы, и сама система типов заботится о том, чтобы выход одной монады был совместим с входом другой. И в отличии от юниксовых конвееров, мы можем задавать наши собственные правила связывания (>>=). Например, такие: «не делать более 42 вычислений подряд» или «посмотреть на входное значение, сделать то или это». Классы монад содержат в себе подобные правила, как соединять вычисления.
Дополнение: в комментариях подсказывают, что гораздо более подробно и более строго аналогия между юниксовым конвеером и монадами разобрана в статье Monadic i/o and UNIX shell programming.
Теперь, я надеюсь, вы понимаете монады не хуже меня (не обязательно полностью). Хочу обсудить несколько мнемонических правил. Почему return называется return?

В большинстве языков return возвращает результат вычисления из функции. В Хаскеле же он конструктор для монад. Это очень странно. Однако посмотрим как работает >>=: эта операция извлекает значение из монады слева, а затем связывает его с аргументом функции справа (отсюда, кстати, и другое название метода — bind). А функция справа должна вернуть значение обратно в монаду, чтобы можно было передать эстафетную палочку дальше следующей операции >>=. Это первое мнемоническое правило: return — возвращает вычисленное значения обратно в монаду.

Вторая мнемоника. Функция верхнего уровня любой программы на Хаскеле выполняется в монаде IO (тип функции mainIO ()). Эта монада позволяет выполнять ввод-вывод и вообще любые последовательные действия. Таким образом, монадный код выполняется на самом верхнем уровне программы, и именно он вызывает любой «чистый» код по мере необходимости, а не наоборот. Таким образом, любое «чистое» значение, если не отбрасывается, то рано или поздно возвращается в монаду её вызвавшую.

Надеюсь, что после этих объяснений имя return для монадного конструктора больше не кажется таким уж странным. Я, однако, не утверждаю, что мои объяснения 100% технически верны.

Следующий вопрос бывшего ОО-программиста, как вытащить значение вычисления из монады?. Хорошо, монады преднамеренно спроектированы именно так, что это не всегда возможно. Например, нельзя извлечь чистое значение из монады IO. Если дана такая «односторонняя» монада, то всё, что можно с ней делать — передавать внутреннее значение по монадному конвееру дальше. К счастью, в Хаскеле разработан специальный синтаксис с ключевым словом do, которые делает такую многократную передачу монады по конвееру очень похожей на последовательную императивную программу. Следующие две программы делают одно и то же. Первая записана с do-нотацией:
main :: IO ()
main = do
name <- getLine
putStrLn ("Hi, " ++ name)
а вторая явно использует >>=:
main :: IO ()
main = getLine >>= \name -> putStrLn ("Hi, " ++ name)
Эквивалентная программа на Питоне:
from sys import stdin, stdout
if __name__ == "__main__":
name = stdin.readline()
stdout.write("Hi, " + name)
Однако иногда вытащить чистое значение из монадного вычисления можно. Это не предусмотрено общим монадным интерфейсом, поэтому разработчик монады должен специально предусмотреть возможность извлекать значения наружу. Например, можно извлекать значения из монады Maybe, используя функцию fromMaybe:
ghci> fromMaybe 0 $ Just 3
3
ghci> fromMaybe 0 $ Nothing
0


Заключение по монадам

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

5. Страшные слова


Я знаю только то, что ничего не знаю.


Даже спустя месяцы после того, как я начал учить Хаскель, умея написать какие-то полезные программы на нём, я вижу вокруг, в мире Хаскеля, ещё много понятий, о которых не знаю ничего или имею только очень смутное представление. Я называю такие понятия «страшными словами». И я вижу, что есть люди, которые создают и используют библиотеки, воплощающие эти понятия в жизнь.

Надо принзнать, Хаскель остаётся испытательной площадкой для исследователей. И это одновременно и хорошо, и плохо. Это хорошо, потому что даёт чувство, что передний край науки и технологии очень близок, и можно при желании пользоваться преимуществами новых подходов. При желании. И одновременно это плохо, потому что иногда, когда хочется использовать новую изящную библиотеку, оказывается, что она активно использует незнакомые и не совсем понятные понятия и идеи, и нужно быть готовым такие идеи осваивать.

Например, есть современная XML-библиотека HXT. Она очень много использует стрелки. Стрелки — более универсальные комбинаторы вычислений, чем монады, но мне потребовалось гораздо больше, чем один день, чтобы их более-менее понять. Строго говоря, стрелки не являются частью языка, но они — понятие, которое применяют пользователи этого языка. И таких примеров немало. Хотя тем, кому стрелки осваивать не хочется, можно пользоваться более традиционной и активно поддерживаемой XML-библиотекой HaXml.

Я думаю, важно не бояться «страшных слов». К счастью, основополагающие идеи хорошо описаны. Как правило, есть статьи их очень детально объясняющие. Я сам решил осваивать такие идеи по мере необходимости. Это обещает быть и увлекательным, и одновременно посильным.

Заключение



Я перечислил пять простых идей, осознав которые, мне стало легче привыкнуть к Хаскелю. Лямбды — это просто способ записи функций, и функции нескольких аргументов можно всегда записать как функцию одного, возвращающую другую функцию. Типы классов очень похожи на абстрактные полиморфные интерфейсы в объектно-ориентированном подходе. Монады — стандартизованный способ соединять вычисления вместе. А страшные слова — просто страшные слова. Без них можно жить, но скучно.

Надеюсь, мои заметки будут полезны и ещё кому-нибудь.

Эта статья есть также по-английски. This post is also available in English.

20090908

Ещё одна библиотека комбинаторного парсинга

Не так давно я писал о библиотеке pyparsing для комбинаторного парсинга в Python. В комментариях появилась ссылка на ещё одну библиотеку, о которой я вначале не знал, а именно на funcparserlib, написанную Андреем Власовских.

В общем-то, я посмотрел на новую библитечку, и она мне тоже понравилась. Подкупает сравнительная простота самой библиотеки, ясные исходники и подробно написанные руководства — понять как работает библиотека нетрудно. Правда, при чтении документации нужно быть знакомым с нотацией типов, принятой в Haskell (ArgType -> ResultType). Общее впечатление: имеющиеся в funcparserlib комбинаторы практически полностью ортогональны друг другу, записываются кратко, места на экране занимают мало, называются понятно.

Комбинаторов в библиотеке всего-то: some, a, many, finished, maybe, skip, oneplus, forward_decl, +, | и >>. Действие большинства комбинаторов угадывается из названия. Рабочие лошадки: some(функция-предикат) берёт токен, удовлетворяющий условию, a(токен) берёт токен, указанный в аргументе, many(парсер), maybe(парсер), oneplus(парсер) — множественное, возможное или хотя бы однократное срабатывание парсера, skip(парсер) отбрасывает всё найденное парсером. Плюс (+) последовательно применяет два парсера, «или» (|) пробует альтернативные варианты, >> подставляет результат парсера в функцию и создаёт новый парсер (очень полезный комбинатор!). Ещё есть tuple для группировки.

Для пробы я решил написать пример разбора того же файлового формата, что и в заметке о pyparsing. Мне кажется, что наиболее эффективно применять funcparserlib с предварительным лексическим анализом (разбиением текста на токены-лексемы). В принципе, готовый токенизатор — часть стандартной библиотеки Python, поэтому это не проблема. Удобная обёртка приведена в руководстве к funcparserlib. Однако мне хотелось написать пример в том же стиле, что и для pyparsing, поэтому в примере ниже я разбираю текст на уровне отдельных символов.

Полный текст примера с тестами кладу в pastebin. Далее пояснения.

Половина кода пришлась на разбор чисел. Вообще, по-моему, в такого рода библиотеках было бы хорошо класть готовые парсеры для чисел, дат, e-mail и тому подобных обыденных вещей где-нибудь в разделе contrib... Впрочем, написать разбор чисел было даже занятно.

Чтение знака числа. Если ни плюса, ни минуса нет, подразумеваю плюс. Волшебный комбинатор >> позволяет обработать результат и сразу подготовить ответ, или -1, или +1:
sign = maybe(a("-")|a("+")) >> (lambda c: c == "-" and -1 or +1)

Целые числа состоят из знака и последовательности цифр. Так и запишем:
digits = many(some(lambda c: c.isdigit()))
int_num = sign + (digits >> to_int) >> mk_int

Здесь я пользуюсь опять комбинатором >> и двумя вспомогательными функциями. Одна превращает последовательность символов-цифр в число (to_int), другая умножает результат на -1, если необходимо (mk_int). Эти функции вспомогательные, можно пропустить:
powers = lambda digs: zip(digs,xrange(len(digs)-1,-1,-1))
add_digit = lambda acc, dp: acc+int(dp[0])*10**dp[1]
to_int = lambda digs: reduce(add_digit, powers(digs), 0)
mk_int = lambda (s,i): s*i

Аналогично строю и парсер для рациональных чисел, только прибавляю ещё и дробь, и, соответственно, ещё две вспомогательных функции:
to_frac = lambda digs: to_int(digs)*1.0/10**len(digs)
mk_frac = lambda (s,i,f): s*(i+f)
frac_num = sign + (digits >> to_int) + (skip(maybe(a("."))) + (digits >> to_frac)) >> mk_frac

Вот и всё. Использовать примерно так:
>>> frac_num.parse("-1.25")
-1.25

Второй половиной кода оказалось написание парсеров для аналогов Literal и Word из pyparsing. Это необходимо, потому что без токенизации приходится распознавать цепочки токенов. Дополнительно создал парсер ws для пропуска пробелов:
pack = lambda cs: ''.join(cs)
literal = lambda s: reduce(lambda a,b: a+b, map(a,s)) >> pack
word = lambda p: oneplus(some(p)) >> pack
ws = skip(many(some(lambda c: c.isspace())))

После этих определений собственно парсер выбранного формата укладывается в три выражения:
varname = word(lambda c: c.isalpha())
var = (ws + varname + ws + frac_num + ws)
custom_format = skip(literal("Inspection")) + \
ws + skip(literal("#")) + \
ws + int_num + \
ws + skip(literal("SHOULD")) + \
ws + skip(literal("Ref. Sys")) + \
ws + int_num + \
many(var)

Смотрим на результат:
$ python test.py < input.txt 
(2, 1, [('X', 28.749300000000002), ('Y', 78.995999999999995), ('Z', -1.0014000000000001)])


В общем, впечатления хорошие. Документация у библиотеки приличная. Использовать приятно. Только одно досадное неудобство было связано с тем, что setup.py install --prefix=... из дистрибутивного тарбола не сработал как надо. Впрочем, библиотека такая маленькая, что можно положить все три её файла прямо в свой проект, без общесистемной установки.

Тонкости: нужно быть осторожным, не помещая универсально успешный парсер внутрь many, чтобы избежать вечного цикла. Короче, нельзя внутрь many помещать many, maybe и pure. Подробнее — см. FAQ.

См. также заметку про pyparsing.

20090904

Как сделать видеофайл из анимированного GIF-а

Для того, чтобы из анимированного GIFа сделать видеофайл, я недавно использовал gifsicle (чтобы разоптимизировать GIF и разбить на кадры) и ffmpeg (чтобы сделать из кадров видео):
gifsicle -U --explode "input.gif"
for f in *.gif ; do mv "$f" "$f.gif" ; done
ffmpeg -r 25 -i "input.gif.%03d.gif" -sameq -s 320x240 output.flv

Если нужно добавить чёрных полей (до нужного размера), действую примерно так (в данном случае, хочу получить 320×240):
ffmpeg -i input.file -s 320x180 -padtop 30 -padbottom 30 output.file

Я не использую для разделения на кадры ImageMagick (convert), потому что мне кажется, что gifsicle работает быстрее и требует меньше памяти.

(in English)