tag:blogger.com,1999:blog-376289762024-03-20T16:39:46.226+03:00советыUnknownnoreply@blogger.comBlogger11125tag:blogger.com,1999:blog-37628976.post-66972965393639803092010-10-01T16:26:00.016+04:002010-10-10T20:41:58.282+04:00Как аппликативные функторы с if-ами боролись<div style="padding-left: 50%; font-style: italic;">Maybe А и Maybe Б сидели на трубе,<br />А упало, Б пропало, что осталось на трубе?</div><br /><br />На днях <a href="http://twitter.com/#!/jetxee/status/25870218553">заметил</a>, как полезны могут быть аппликативные функторы для избавления от леса вложенных условных конструкций (<code>if</code>-ов и <code>case</code>-ов). А именно, как удобно, что тип <code>Maybe</code> можно использовать как аппликативный функтор.<br /><br /><h3>О функторах, пирогах и яблоках</h3><br /><br />Речь пойдёт о языке Хаскель, но постараюсь рассказать так, чтобы пользователям других языков было тоже понятно. Кто Хаскель знает, этот раздел может пропустить и сразу перейти к следующему.<br /><br />Итак, для представления такого результата, которого может и не быть, в Хаскеле используется тип <code>Maybe a</code>, где <code>a</code> — любой тип. У <code>Maybe a</code> бывают значения двух видов: <code>Just a</code> («просто а», если значение есть) и <code>Nothing</code> («ничего», если значения нет). <code>Maybe</code> является функтором, то есть из любой функции из <code>a</code> в <code>b</code> (тип <code>:: a -> b</code>) можно сделать функцию из <code>Maybe a</code> в <code>Maybe b</code> (тип <code>:: Maybe a -> Maybe b</code>). В общем, функтором является любой тип <code>f</code>, для которого определена<sup>*</sup>:<br /><br /><pre class="sh_haskell">fmap :: (a -> b) -> (f a -> f b)</pre><br /><br /><blockquote style="font-size:85%;"><sup>*</sup> определение <code>fmap</code> должно удовлетворять при этом двум условиям: 1) <code>fmap id = id</code>, где <code>id</code> — функция, возвращаюая свой аргумент, 2) <code>fmap</code> должна быть дистрибутивна слева относительно композиции функций... <a href="http://www.haskell.org/haskellwiki/Functor">[ссылка]</a></blockquote><br /><br />Например, пусть у нас есть типы <code>Яблоко</code> и <code>Пирог</code> и функция <code>испечь :: Яблоко -> Пирог</code>, превращающая одно в другое. Тогда мы легко можем взять яблоко, <em>которого может и не быть</em>, и испечь из него пирог, , <em>которого может и не быть</em>. То есть если будет просто яблоко, то выйдет просто пирог, а если ничего не было, то ничего и не выйдет. Делается это проще простого: <code>fmap испечь</code>.<br /><br /><pre class="sh_haskell">data Яблоко = Яблоко deriving Show<br />data Пирог = Пирог deriving Show<br /><br />испечь :: Яблоко -> Пирог<br />испечь Яблоко = Пирог<br /><br />испечьЕслиЕсть :: (Maybe Яблоко) -> (Maybe Пирог)<br />испечьЕслиЕсть = fmap испечь</pre><br /><br />После этого мы можем печь пироги, которых может и не быть:<br /><br /><pre class="sh_haskell">ghci> испечьЕслиЕсть (Just Яблоко)<br />Just Пирог<br />ghci> испечьЕслиЕсть Nothing<br />Nothing</pre><br /><br />А что делать, если самой функции из <code>a</code> в <code>b</code> <em>может и не быть</em>? Как применить <code>Maybe (a -> b)</code> к <code>Maybe a</code>? Такая операция определена для аппликативных функторов, <code>Maybe</code> — один из них. В Хаскеле для этого подключают модуль <code>Control.Applicative</code>, саму же операцию последовательного применения называют звучным словом <code>(<*>)</code>.<br /><br />Поясняю на яблоках. Что делать случае на входе у нас <code>Maybe (Яблоко -> Пирог)</code> — рецепт, которого может и не оказаться? Естественно, что пирог у нас получится тогда и только тогда, когда есть и яблоко, и сам рецепт. Если хоть что-то отсутствует, то и пирога не будет, будет <code>Nothing</code>. Итак, дано:<br /><br /><pre class="sh_haskell">безРецепта = Nothing<br />сРецептом = Just испечь</pre><br /><br />Проверяем, как работает с такими «рецептами» аппликативный функтор <code>Maybe</code>:<br /><br /><pre class="sh_haskell">ghci> :m + Control.Applicative<br />ghci> сРецептом <*> Just Яблоко<br />Just Пирог<br />ghci> сРецептом <*> Nothing<br />Nothing<br />ghci> безРецепта <*> Just Яблоко<br />Nothing<br />ghci> безРецепта <*> Nothing<br />Nothing</pre><br /><br />Это всё, что нам сейчас потребуется из «теории». И ещё пара замечаний: 1) с аппликативными функторами часто применяется ещё операция <code>(<$>)</code>, которая является для них синонимом <code>fmap</code> и в инфиксной записи выглядит короче; 2) любая монада является также аппликативным функтором (обратное неверное), и для неё <code>ap</code> и <code>(<*>)</code> совпадают (об этом полезно знать, хотя нам сейчас и не потребуется).<br /><br /><h3>Избавление от условных конструкций</h3><br /><br />Перехожу к практическим вопросам. На днях писал небольшой парсер для файлов формата Matrix Market. Это несложный формат, состоящий из заголовка в котором указывается, как хранится структура матрицы (покоординатно или сплошным массивом) и тип значений её элементов (целые, действительные или комплексные). Возможны разные комбинации.<br /><br />Логику разбора такого формата на императивном псевдокоде можно описать так:<br /><br /><pre>if (структура == координатная) {<br /> if (тип == целые) {<br /> читатьЦелыеПоКоординатам;<br /> } else if (тип == действительные) {<br /> читатьДействительныеПоКоординатам;<br /> } else if (тип == комплексные) {<br /> читатьКомплексныеПоКоординатам;<br /> } else {<br /> ошибка;<br /> }<br />} else if (структура == массив) {<br /> if (тип == целые) {<br /> читатьМассивЦелых;<br /> } else if (тип == действительные) {<br /> читатьМассивДействительных;<br /> } else if (тип == комплексные) {<br /> читатьМассивКомплексных;<br /> } else {<br /> ошибка;<br /> }<br />} else {<br /> ошибка;<br />}</pre><br /><br />Естественно, что такого развесистого дерева выбора можно избежать, потому что выбор типа элементов и выбор структуры матрицы независимы друг от друга. В функциональном стиле можно просто составить «словарь» поддерживаемых функций для разбора структуры и отдельный «словарь» функций для чтения значений разных типов, выбирать подходящие, и потом передавать вторые в качестве параметра первым. <a href="http://bitbucket.org/jetxee/hs-matrix-market/src/tip/MatrixMarket.hs#cl-68">Так я и сделал</a>.<br /><br />Опять псевдокод для пояснения этой идеи:<br /><br /><pre class="sh_haskell">let читатьСтруктуру = lookup структура словарьФункцийРазбора<br /> читатьЧисло = lookup тип словарьФункцийРазбораЧисла<br />in читатьСтруктуру читатьЧисло исходныеДанные</pre><br /><br />На практике дело обстоит немного сложнее. Операция поиска в словаре может ничего не найти. Как легко догадаться, <code>lookup</code> в Хаскеле возвращает <code>Maybe a</code>. Итак, у нас есть <code>Maybe функция</code>, <code>Maybe аргумент</code>, а мы хотим получить <code>Maybe результат</code>. Ничего не напоминает? Да-да, тут самое время применить <code>(<*>)</code>.<br /><br />Однако представим на минуту, как эта задача решается без операции последовательного применения <code>Maybe a</code>. И для функции, и для её аргумента нужно проверять являются ли они чем-то (<code>Just a</code>) или ничем (<code>Nothing</code>), и в результате получается тот же лес проверок, что и в императивном псевдокоде, только в этот раз с проверкой на результат поиска. Можно, правда, все проверки объединить в одной единственной условной конструкции на каждое применение функции, но все проверки надо всё равно записывать явно. То есть без <code>(<*>)</code> получается что-то такое:<br /><br /><pre class="sh_haskell">let читатьСтруктуру = lookup структура словарьФункцийРазбора<br /> читатьЧисло = lookup тип словарьФункцийРазбораЧисла<br />in case (читатьСтруктуру, читатьЧисло) of<br /> (Just f, Just g) -> Just (f g исходныеДанные)<br /> _ -> Nothing</pre><br /><br />Код склонен паталогически ветвиться, если таких <code>Maybe</code>-аргументов у функции больше одного, или если полученный <code>Maybe</code>-результат нужно использовать в качестве аргумента ещё одной функции (тут не обойтись без вложенных проверок).<br /><br />К счастью, все эти проверки тривиальны, и код можно записать гораздо короче:<br /><br /><pre class="sh_haskell">let читатьСтруктуру = lookup структура словарьФункцийРазбора <br /> читатьЧисло = lookup тип словарьФункцийРазбораЧисла <br />in читатьСтруктуру <*> читатьЧисло <*> (Just исходныеДанные)</pre><br /><br />Здесь нет ни одной явной условной конструкции, но код, тем не менее, делает все необходимые проверки, и вернёт <code>Just результат</code> только если нашлись обе функции.<br /><br />У меня получилось <a href="http://bitbucket.org/jetxee/hs-matrix-market/src/tip/MatrixMarket.hs#cl-68">немного длиннее</a>, но суть та же:<br /><br /><pre class="sh_haskell">let p = lookup fmt parsers :: Maybe FormatReader<br /> nr = lookup fieldq readers :: Maybe (Int, ValueReader)<br /> sy = lookup symq symmetries :: Maybe Symmetry<br /> p' = uncurry <$> p <*> nr :: Maybe ([String] -> Maybe MatrixData)<br /> d = join $ p' <*> (Just toks) :: Maybe MatrixData<br /> m = MM <$> d <*> sy :: Maybe Matrix<br />in ...</pre><br /><br />Что тут можно заметить. Во-первых, <code>(<$>)</code> практически так же часто используется, как и <code>(<*>)</code>. Эта операция позволяет применять обычные функции к <code>Maybe</code>-значениям. Вместо <code>(<$>)</code> можно поднимать значения в функтор с помощью <code>pure</code> (для любых аппликативных функторов) или <code>Just</code> (конкретно для <code>Maybe</code>) и использовать только <code>(<*>)</code>.<br /><br />Во-вторых, если у нас кругом функции, возвращающие <code>Maybe</code>, и они сами оказываются внутри <code>Maybe</code> (см. <code>p'</code> в моём примере), то рано или поздно появляются «вложенные» значения типа <code>Maybe (Maybe a)</code>. Тут помогает монадный <code>join</code>, превращающий их в просто <code>Maybe a</code>. <code>join</code> определён в <code>Control.Monad</code>. И кстати, это тоже помогает избавиться от вложенных тривиальных проверок (<code>join (Just Nothing)</code> даёт <code>Nothing</code>).<br /><br />В-третьих, при таком подходе мы, естественно, теряем информацию о том, какое именно вычисление не дало результата (дало <code>Nothing</code>). Для передачи «исключения» по цепочке можно использовать другие типы, например <code>Either e</code>, определив для них <code>Applicative</code>.<br /><br /><h3>Заключение</h3><br /><br />Мне такой способ комбинировать вычисления очень понравился. По-моему, случай, когда все условные проверки сводятся к «если в порядке, то считать дальше, а если нет, то прервать» — довольно распространённый. Тип <code>Maybe</code> с операцией последовательного применения <code>(<*>)</code> позволяет такие проверки, любой степени вложенности, не писать вообще.<br /><br /><a href="http://flattr.com/thing/68786/if-" target="_blank"><br /><img src="http://api.flattr.com/button/button-compact-static-100x17.png" alt="Flattr this" title="Flattr this" border="0" /></a>Unknownnoreply@blogger.comtag:blogger.com,1999:blog-37628976.post-46229770443380310752010-03-22T18:20:00.009+03:002010-03-23T16:12:50.369+03:00Впечатления о ЦюрихакеВчера вернулся с <a href="http://www.haskell.org/haskellwiki/ZuriHac" title="ZuriHac">Цюрихака</a>. Интересная была поездка. Программировать в компании 80 других фанатов очень даже занятно. Здорово, что можно прямо тут же что-то обсудить, наметить цели, поделить работу и её сделать. Приятно браться за незнакомый код и в сжатый срок добавлять к нему что-то полезное (а с Хаскелем такое по силам даже новичкам; и новичкам с готовностью помогали). Большинство участников поселились все вместе в одном хостеле, так что вечерами собирались большими компаниями, чтоб выпить кружечку пива, познакомиться, послушать рассказы других людей, поделиться своим опытом и впечатлениями. Ну и просто увидеть людей, имена которых были уже знакомы, тоже интересно. Вот они, цюрихакеры (я тут тоже есть):<br /><br /><a href="http://img-fotki.yandex.ru/get/3814/s-astanin.c/0_2927e_89efeb27_orig"><img class="blackbg" src="http://img-fotki.yandex.ru/get/3814/s-astanin.c/0_2927e_89efeb27_L.jpg" title="Участники Zurihac-а" style="max-width: 99%; height: auto;"/></a><br /><br /><a name='more'></a><br /><br />Были, конечно, люди, для которых Хаскель — основная работа. Это и PhD-студенты, творящие что-то с Хаскелем, и сотрудники нескольких хаскельных контор, и небезызвестные исследователи, например <a href="http://www.cs.uu.nl/research/ozis/atze.html">Atze Dijkstra</a>. Но было много и таких, как я, для которых Хаскель — хобби. Кто-то работает программистом, кто в банке, кто ещё где, а в свободное время — ну понятно. Много хороших, увлечённых людей. Из наших там ещё был <a href="http://ro-che.info/">Роман Чепляка</a> из Киева. Очень приятными собеседниками оказался Бартек Войчик, работающий в Мюнхене, и <a href="http://www.dtek.chalmers.se/~kolmodin/">Леннарт Колмодин</a>, швед с итальянскими корнями (с ним мы жили в одной комнате). Много ещё кого, всех трудно перечислить. Контактов в твиттере прибавилось.<br /><br />Я, наконец-то, сподобился выложить свой <a href="http://bitbucket.org/jetxee/snusmumrik/wiki/Home">Snusmumrik</a> на Hackage. Так что он теперь должен устанавливаться по одному волшебному заклинанию <code>cabal install Snusmumrik</code> (но только со старым GHC 6.10). Среди выложивших пакеты на Hackage разыгрывалась футболка, но мне она всё равно не досталась. Зря выкладывал?<br /><br />В основном же работал с программой автоматического учёта времени, <code>arbtt</code>. Добавил в её классификатор календарные функции, которых мне прежде не хватало. Ребята сделали ещё кое-что полезное, так что набралось на <a href="http://darcs.nomeata.de/arbtt/doc/users_guide/release-notes.html#id3524273">новый релиз</a>. Эту работу, как видно из лога, мы сделали с <a href="http://www.joachim-breitner.de/">Йоахимом Брайтнером</a>, немецким боснийцем <a href="https://launchpad.net/~al-maisan">Мухаремом Хрнъядовичем</a> (сомневаюсь в правильности произношения фамилии) и Мартином Кифелем.<br /><br />К сожалению, разные доклады, назначенные на вечер воскресенья, мне пришлось пропустить. Поезд. Очень жаль, что их не поставили пораньше.<br /><br />Весь <a href="http://picasaweb.google.com/CamenzindEvolution/NewGoogleOfficeZurich#">расчудесный офис Гугла в Цюрихе</a> нам не показали. Мы проходили только через фойе, кухню-столовую (это где спиральная горка со второго этажа) и потом сразу в какой-то большой зал. То ли спортивный, то ли для встреч. В пятницу вечером Гугл угостил всех закуской, предложив, в том числе, сэндвичи метр на метр и ледяного пива с избытком. Воду и напитки можно было брать из холодильника без ограничений, чай тоже был. В гугловом туалете над писсуаром и в кабинке висят памятки «Git on the Go», как пользоваться Git-ом. Прочитал :-) А вот фотографировать в офисе запретили (но есть <a href="http://picasaweb.google.com/david.jc.anderson/Zurihac#">фотки — много фоток —</a>, сделанные гуглерами-организаторами; всё так и было).<br /><br />Сам Цюрих я не особо разглядел. Ну так, аккуратный городок, ок. Есть красивые места, но уж слишком много магазинов со сверкающими витринами. Убедительной обшарпанности ему не хватает. Ну и новая архитектура — как и офис гугла: кубы домов с панелями приглушённых серо-коричневых оттенков. Тоска. А вот дорога на поезде из Милана в Цюрих очень красива. Узкие, глубокие долины. Местами дорога поднимается достаточно высоко, по моим оценкам не ниже 1000 метров, там ещё лежит снег. А ниже — уже зеленеют луга. Кстати, в швейцарских электричках велосипедные места есть во всех вагонах. В самом Цюрихе велосипедистов тоже много. Видимо, Швейцария вообще чрезвычайно дружественна к велосипедистам (вспоминаю Лозанну, там с этим тоже хорошо). Вот только швейцарские банкиры хуже таксистов: разница обменных курсов 6% + 4 франка (почти 3 €) комиссии за каждый обмен. И всё дорого.<br /><br />Общее же впечатление, что 2½ дней очень мало. Мне не хватило.<br /><br /><b>Дополнение:</b> <a href="http://ro-che.blogspot.com/2010/03/zurihac-some-critics.html">Критика хакатона</a> Романом Чеплякой. По-английски.Unknownnoreply@blogger.comtag:blogger.com,1999:blog-37628976.post-49388950185236767882010-01-29T18:10:00.019+03:002010-03-22T19:09:09.094+03:00Автоматический учёт времени: Arbtt macht frei!В линуксе есть несколько разных программок для учёта времени, самая простая и незамысловатая, и при этом вполне функциональная — это, пожалуй, <a href="http://projecthamster.wordpress.com/about/">Hamster</a>. С ней всё понятно: добавляем на панель, вбиваем новое дело всякий раз, когда за него берёмся. Главное, не забывать.<br /><br />А вот есть программка похитрее: <a href="http://darcs.nomeata.de/arbtt/doc/users_guide/">arbtt</a>. Пользоваться ей, правда, легче. Она полностью автоматическая. Достаточно запустить <code>arbtt-capture</code> и заниматься своими делами<sup>*</sup>. <code>arbtt-capture</code> будет записывать когда и какие программы были запущены и какие у окон были заголовки.<br /><br /><blockquote><sup>*</sup> Автор <code>arbtt</code> рекомендует сразу добавить <code>arbtt-capture</code> в автоматически запускаемые приложения.</blockquote><br /><br />Чтобы увидеть необработанные сырые данные, можно выполнить <code>arbtt-dump</code>, но это не очень полезно. Для просмотра статистики удобнее использовать использовать утилитку <code>arbtt-stats</code>.<br /><br />Чтобы <code>arbtt-stats</code> могла выдавать осмысленные результаты, нужно вначале задать свою классификацию запущенных программ. Эти правила вписываются в файл <code>~/.arbtt/categorize.cfg</code>. Пример и описание формата правил <a href="http://darcs.nomeata.de/arbtt/doc/users_guide/configuration.html">есть в документации</a>. Приведу свой (сокращённый) пример с комментариями по-русски:<pre class="sh_haskell">-- правила имеют вид:<br />-- [условие ==>] tag [категория_тега:]тег,<br />-- в условиях и тегах можно использовать несколько специальных переменных,<br />-- почти все они встречаются в примерах ниже<br /><br />-- Не учитывать время простоя<br />$idle > 60 ==> tag inactive,<br /><br />-- Все записи за последние 24 часа пометить тегом last-day<br />$sampleage <= 24:00 ==> tag last-day,<br />-- Пометить тегом last-hour все записи за последний час<br />$sampleage <= 1:00 ==> tag last-hour,<br /><br />-- Все типы окон Firefox учитывать в одном теге program:web (program — это категория тега)<br />current window $program == "Navigator" ==> tag program:web,<br />current window $program == "firefox-bin" ==> tag program:web,<br />current window $program == "gecko" ==> tag program:web,<br />-- Общий тег для всех видов терминалов (на будущее)<br />current window $program == "gnome-terminal" ==> tag program:terminal,<br />-- Пометить все остальные программы пометить тегами вида program:имя_программы<br />tag program:$current.program,<br /><br />-- Классифицировать заголовки Firefox с помощью регулярных выражений. Тут у каждого будут свои шаблоны.<br />-- Присваивать теги категории web.<br />current window ($program == "Navigator" && $title =~ /^Gmail.*/) ==> tag web:Gmail,<br />current window ($program == "Navigator" && $title =~ /.*Google Search.*/) ==> tag web:Google,<br />current window ($program == "Navigator" && $title =~ /^Twitter.*/) ==> tag web:Twitter,<br />current window ($program == "Navigator" && $title =~ /.* on Twitter - Iceweasel$/) ==> tag web:Twitter,<br />current window ($program == "Navigator" && $title =~ /^Springer.*/) ==> tag web:Papers,<br />current window ($program == "Navigator" && $title =~ /^Wiki - Editing.*/) ==> tag web:Papers,<br />-- ...<br />--<br />current window $program == "Navigator" ==> tag web:$current.title,<br /><br />-- Теги категории time-of-day для классификации по времени суток<br />$time >= 2:00 && $time < 8:00 ==> tag time-of-day:night,<br />$time >= 8:00 && $time < 12:00 ==> tag time-of-day:morning,<br />$time >= 12:00 && $time < 14:00 ==> tag time-of-day:lunchtime,<br />$time >= 14:00 && $time < 18:00 ==> tag time-of-day:afternoon,<br />$time >= 18:00 && $time < 22:00 ==> tag time-of-day:evening,<br />$time >= 22:00 || $time < 2:00 ==> tag time-of-day:late-evening,<br /><br />-- Помечать над каким проектом работаю судя по заголовку окна.<br />-- Присваивать теги категории project.<br />current window $title =~ m!~/work/projectA! ==> tag project:projectA,<br />current window $title =~ m!~/work/projectB! ==> tag project:projectB,<br />-- ...<br />--<br /><br />-- Помечать, какой тип текста я редактирую судя по заголовку окна.<br />-- Присваивать теги категории edit.<br />current window ($title =~ /^[^ ]+\.c .* - G?VIM.*$/) ==> tag edit:c,<br />current window ($title =~ /^[^ ]+\.py .* - G?VIM.*$/) ==> tag edit:python,<br />current window ($title =~ /^[^ ]+\.hs .* - G?VIM.*$/) ==> tag edit:haskell,<br />-- Когда использую suduedit<br />current window ($title =~ m!.*\(/var/tmp\) - G?VIM.*$!) ==> tag edit:config,<br />-- Когда редактирую что-то онлайн в Its All Text<br />current window ($title =~ m!.*/itsalltext\) - G?VIM.*!) ==> tag edit:itsalltext,</pre><br />Для отчёта по определённой категории: <pre class="sh_sh">$ arbtt-stats -c имя_категории</pre>Для просмотра отчётов по всем категориям:<pre class="sh_sourceCode">$ arbtt-stats --each-category</pre>Для ограничения выборки только записями с определённым тегом, например, <code>last-hour</code>, есть опция <code>-o</code>. Всё вместе: <pre class="sh_sourceCode">$ arbtt-stats -o last-hour -c program -c edit<br />Statistics for category program<br />===============================<br />_____________Tag_|___Time_|_Percentage_<br />program:terminal | 29m00s | 48.33<br /> program:gvim | 17m00s | 28.33<br /> program:web | 13m00s | 21.67<br /> program:Pidgin | 1m00s | 1.67<br /><br />Statistics for category edit<br />============================<br />_____________Tag_|___Time_|_Percentage_<br /> edit:itsalltext | 17m00s | 28.33<br /> edit:haskell | 4m00s | 6.67<br />(unmatched time) | 39m00s | 65.00</pre>В последнем примере я показал примерный вывод программы. Сразу видно, сколько времени за последний час я что-то редактировал и что именно и какие программы использовал. Писал эту заметку, в общем.<br /><br />Кстати, <code>arbtt</code> есть не только в линуксовых репозиториях, но в скором времени (а может и уже) будет доступна и пользователям Windows.<br /><br />Некоторые замеченные изъяны: <strike><code>arbtt-stats</code> при печати портит заголовки окон с уникодом (патчем на 20 строк исправляется, должно быть ОК при сборке новым GHC), пока нельзя классифицировать по дням недели или по месяцам, сообщения о синтаксических ошибках в правилах очень невнятны</strike>.<br /><br /><b>Дополнение:</b> замеченные недостатки, не без моего скромного <a href="http://sovety.blogspot.com/2010/03/back-from-zurihac-impressions.html">участия</a>, исправлены во время Хакатона в Цюрихе; используйте GHC 6.12 и устанавливайте <a href="http://darcs.nomeata.de/arbtt/doc/users_guide/release-notes.html#id3524273">новую версию 0.5</a>; там всё ОК.<br /><br />Приятных всем выходных!Unknownnoreply@blogger.comtag:blogger.com,1999:blog-37628976.post-18815016673813638072009-09-18T01:04:00.021+04:002010-03-27T03:00:56.969+03:00(Новичковые) ужасы ХаскеляЯ — начинающий программист на Хаскеле, и пока я ещё помню всё, чем он страшен. И это хочу записать. Сразу скажу, когда я приступал к Хаскелю, я ещё не знал практически ничего о функциональном программировании, поэтому одновременно с языком, нужно было осваивать новые идеи и образ мысли. И вообще-то это было здорово. А у страха, как известно, глаза велики. В общем, я думаю, эта заметка может быть полезна и другим начинающим. В ней я укажу на пять непонятных мне вначале, но относительно несложных вещей, поняв которые хотя бы приблизительно, освоить язык мне было гораздо легче.<br /><br /><h3>1. Ламбда-функции</h3><br /><div style="padding-left: 50%; font-style: italic;">— Но мы называем его лембас или путевой хлеб, он подкрепляет лучше, чем любая пища людей, и он гораздо вкуснее.</div><br /><br />Вообще-то, именно поэтому я и выучил Хаскель. Мне было просто любопытно, что означают все эти лямбды. Очень помогла в самом начале статья Пола Худака <a href="http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.83.6505">Conception, evolution, and application of functional programming languages</a> (<a href="freenet:CHK@7V55cv5604SmgsFEfqm8~iJmMHmzohlLf7J6H9-E8hA,4hsE5sD-guPDm3aHMOd2xtmOY7SKKJWSr3rayM8Sbmk,AAIC--8/hudak89functional.pdf">PDF также здесь</a>). Возможно, есть введения и получше, но я начинал с него.<br /><br />Ламбда-выражение — самая суть «функций» — это выражение вида<br /><blockquote><img style="border: 0pt none ; padding: 1ex 2ex;" alt="\lambda x \;.\; \text{expression with $x$}" src="http://mathurl.com/krqmc7.png"/></blockquote>Значением этого выражения является пока ещё безымянная <em>функция</em> одного аргумента (<em>x</em>), что-то с ним вычисляющая (а именно, выражение справа от точки). С лямбда-функциями связана серьёзная математическая теория, но с точки зрения программирования можно считать <img alt="\lambda" src="http://mathurl.com/c8c2x3.png"/> <em>ключевым словом</em> для определения функций. Действительно, когда я и до этого уже пользовался лямбдами в Питоне (и почти во всех других современных языках они тоже есть). В Питоне они выглядят вот так:<br /><pre class="sh_python">lambda x: expression with x</pre>Ими было очень удобно пользоваться в <code>filter()</code> и <code>reduce()</code>. И вообще, почти везде, где в качестве аргумента требуется имя функции. Однако у лямбда-функций нет имён, и именно поэтому их ещё называют <a href="http://ru.wikipedia.org/wiki/%D0%90%D0%BD%D0%BE%D0%BD%D0%B8%D0%BC%D0%BD%D0%B0%D1%8F_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F">анонимными (безымянными) функциями</a>. В пайтоне иногда я давал им имена прямо на лету:<br /><pre class="sh_python">add_42 = lambda x: x + 42</pre>Теперь имя <code>add_42</code> указывает на функцию. Точно такой же результат можно было получить, записав определение функции как обычно:<br /><pre class="sh_python">def add_42(x):<br /> return x+42</pre>А что же насчёт Хаскеля? Да почти то же самое. Символ <code>\</code> заменяет <img alt="\lambda" src="http://mathurl.com/c8c2x3.png"/>, <code>-></code> служит вместо точки. Всё вместе записывается так:<br /><pre class="sh_haskell">\x -> выражение с x</pre>И мы даже можем давать имена таким безымянным функциям, так же как и в Питоне:<br /><pre class="sh_haskell">add_42 = \x -> x + 42</pre>Согласитесь, очень похоже.<br /><br />Однако тут есть одна тонкость. Как только я начал читать о Хаскеле, я увидел лямбда-выражения, которые поначалу казались немного странными:<br /><pre class="sh_haskell">\x -> \y -> выражение с x и y</pre>Что означают все эти «стрелочки»? Ответ оказался очень прост и очень полезен в дальнейшем освоении языка.<br /><br />В Хаскеле все функции являются функциями одного аргумента. Поначалу это может показаться ограничением, но на деле это очень удобная и практичная идея. Любую функцию <em>n</em> аргументов можно представить как функцию одного аргумента, возвращающую другую функцию <em>n–1</em> аргементов. И по науке это азывается <a href="http://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D1%80%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5">каррированием</a>. Эта идея, в частности, позволяет передавать функции только часть аргументов.<br /><br />Узнав об этом, мы теперь можем читать любые выражение с множеством «стрелок»:<br /><pre class="sh_haskell">\x -> (\y -> выражение с x и y)</pre>Значением такого выражения будет фукнция, берущая один аргумент и производящая другую функцию, которая берёт ещё один аргумент. Такое выражение в целом ведёт себя как функция двух аргументов. Например, мы можем вычислить такую функцию двух аргументов в интерпретаторе Хаскеля <code>ghci</code>:<br /><pre>ghci> (\x -> \y -> x + y ) 3 4<br />7</pre>Конечно, есть более краткий способ записи функций двух аргументов (обратите внимание, что список аргументов брать в скобки совсем не нужно):<br /><pre>ghci> (\x y -> x + y) 3 4<br />7</pre>Однако знать, что на самом деле все функции нескольких аргументов являются функциями одного аргумента очень полезно. Например, это помогает читать описания типов функций. Например, тип функции <code>map</code> выглядит так:<br /><pre class="sh_haskell">map :: (a -> b) -> [a] -> [b]</pre>Я обычно читаю это следующим образом: «функция <code>map</code> принимает два аргумента, первый — функцию преобразующую <code>a</code> в <code>b</code>, второй — список элементов типа <code>a</code>, а возвращает список элементов типа <code>b</code>». Но иногда гораздо естественнее записать тот же самый тип так:<br /><pre class="sh_haskell">map :: (a -> b) -> ([a] -> [b])</pre>«Функция, которая берёт функцию, преобразующую <code>a</code> в <code>b</code>, и возвращает функцию, преобразующую список <code>a</code> в список <code>b</code>».<br /><br />Даже эти простые понятия о лямбда-функиях были уже достаточны, чтобы начать пользоваться Хаскелем и понять большинство примеров и объяснений.<br /><br /><h3>2. Знак равенства</h3><br /><div style="padding-left: 50%; font-style: italic;">— Ну что, если тут нет смысла, — сказал Король, — тогда у нас гора с плеч: нам незачем пытаться его найти! Сэкономим кучу работы! И все же...</div><br /><br />По-моему, знак равенства (<code>=</code>) — самый важный символ в Хаскеле. Понять его важно для понимания языка. И мне кажется, смысл равенства недостаточно подчёркивается во всевозможных учебниках. Например, это единственное ключевое слово, отсутствующее в <a href="http://www.haskell.org/haskellwiki/Keywords">списке ключевых слов Хаскеля</a> в его вики.<br /><br />В отличии от большинства императивных языков, где <code>=</code> означает <a href="http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B8%D1%81%D0%B2%D0%B0%D0%B8%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5">присваивание</a> (то есть <em>действие</em>), в Хаскеле он означает, что левая часть <em>равна</em> правой (то есть описывает <em>свойство</em>).<br /><br /><blockquote><b>Дополнение</b>: в комментариях уточняют, «равно» в Хаскеле — связывание имени слева с определением справа.</blockquote><br /><br />«Равна» — не значит «становится». Это означает, что нечто равно чему-то ещё. Всегда. Как в математике. <code>a = b</code> в Хаскеле означает, что <code>a</code> равно <code>b</code> <a href="http://mathworld.wolfram.com/Defined.html">по определению</a>, <code>a</code> эквивалентно <code>b</code>.<br /><br />Таким образом, <code>=</code> в Хаскеле служит для записи определений. «Равно» может определять самые разные вещи, но определяет их статично. Оно не зависит от порядка выполнения операций. На него можно положиться.<br /><br />Пользователям функциональных языком это покажется слишком уж очевидным, но именно в смысле знака равенства самое важное изменение для тех, кто раньше пользовался императивными языками. Теперь, кстати, мы можем давать имена нашим безымянным функциям:<br /><pre class="sh_haskell">add = \x -> \y -> x + y</pre>Признаю, что читается это плохо, поэтому в большинстве случаев функции в Хаскеле определяются так:<br /><pre class="sh_haskell">add x y = x + y</pre>Но и это по-прежнему <em>определение</em> функции <code>add</code>.<br /><br /><h3>3. Классы типов</h3><br /><div style="padding-left: 50%; font-style: italic;">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. — приписывается Б. Страуструпу</div><br /><br />Система типов в Хаскеле просто прекрасна. В ней очень легко и естественно выражаются многие идеи. И возможно, именно классы типов — это наименее чуждая концепция для тех, кто приходит в Хаскель из процедурного и объектно-ориентированного мира. Во всяком случае, мне так показалось. Однако классы типов — это совсем не то же самое, что классы в Си++ или в Джаве. Гораздо больше они похожи на абстрактные шаблоны классов в Си++, потому что классы типов<br /><ul><li>определяют только абстрактный интерфейс</li><li>позволяют создавать несколько независимых реализаций интерфейса (таким образом, для любого типа можно определить экземпляр класса, если предоставить реализацию его методов)</li><li>полиморфны по своей природе и поддерживают наследование</li><li>не могут иметь переменных состояния</li></ul><br />Как только мы привыкнем, что типы классов — это не классы Си++, а абстрактные интерфейсы, и экземпляры классов это не «объекты», а конкретные реализации абстрактных интерфейсов, Хаскель сразу станет привычным и уютным.<br /><br />Я очень советую почитать вики-статью <a href="http://www.haskell.org/haskellwiki/OOP_vs_type_classes">OOP vs type classes</a>, которая гораздо более детально сравнивает объектно-ориентированный подход и классово-типовой.<br /><br /><br /><h3>4. Монады</h3><br /><div style="padding-left: 50%; font-style: italic;">И так как всякое настоящее состояние простой субстанции, естественно, есть следствие ее предыдущего состояния, то настоящее ее чревато будущим, — Лейбниц, «Монадология»</div><br /><br />Не важно, насколько <a href="http://rsdn.ru/article/haskell/haskell_part1.xml">мягкое введение в Хаскель</a>, рано или поздно его читатель упрётся лбом в крепкую стеную из <a href="http://ru.wikipedia.org/wiki/%D0%9C%D0%BE%D0%BD%D0%B0%D0%B4%D0%B0_%28%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%29">монад</a>. Да, это вам не плюшки тырить, это вам серьёзная математика. Где-то за этой стеной.<br /><br />Но вот что я понял: изучать абстрактную математику совсем не обязательно, чтобы монады использовать, а они и правда очень изящная программистская техника. Вначале они казались мне немного странными, но понять раз и навсегда монады гораздо легче, чем запоминать (и правильно применять!) бесчисленные <a href="http://ru.wikipedia.org/wiki/%D0%A8%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD%D1%8B_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F">шаблоны ОО-проектирования</a>. Монады логичней.<br /><br />Поскольку тьюториалов по монадом огромное множество, я не буду их здесь повторять и ожидаю, что вы их уже прочитал <a href="http://www.haskell.org/haskellwiki/Tutorials#Using_monads">парочку</a>. Что же не так с монадами? Для человека, привыкшему к императивным языка, испорченному годами объектно-ориентированного мышления монады кажутся странными. Они выглядят как абстрактный класс-контейнер с загадочным методом <code>>>=</code>:<br /><pre class="sh_haskell">class Monad m where<br /> return :: a -> m a<br /> (>>=) :: m a -> (a -> m b) -> m b<br /> fail :: String -> m a</pre>Хорошо, если <code>return</code> — конструктор, то почему такое чудное имя? Если это класс-контейнер, то как из него что-либо извлечь? И какой смысл применять функцию внутри контейнера (а именно это делает метод <code>>>=</code>, называемый также операцией <em>связывания</em>), если мы не можем вытащить результат из этого контейнера?<br /><br />Отвечу вначале на последний вопрос. <b>Зачем нужно связывание (<code>>>=</code>)?</b> Монады являются и одновременно не являются контейнерами. Они — обёртки, упаковки для <em>вычислений</em>, а не для значений (<code>return</code>). Однако они обёртывают вычисления не для того, чтобы их было удобнее хранить в монадных коробочках, а чтобы их можно было удобнее соединять друг с другом. Вместо «коробочек» представьте обыкновенные кирпичи, которые ровно кладутся друг к другу. Это, кстати, похоже на шаблон Adapter в ОО-проектировании. Каждая монада определяет какой-то способ передавать результат от одного вычисления к другому и реализует стандартный интерфейс, чтобы этот способ использовать (<code>>>=</code>). И что бы ни случилось, результат всегда останется в той же монаде (даже, если произойдёт сбой, <code>fail</code>).<br /><br />Самая простая программистская аналогия монадам, которую я придумал, это конвееры (pipes) в командной оболочке Unix. Монады обеспечивают однонаправленный конвеер для вычислений. То же самое делают и конвееры в Unix. Например:<br /><pre>$ seq 1 5 | awk '{print $1 " " $1*$1*$1 ;}'<br />1 12 83 274 645 125</pre><code>seq</code> создаёт список целых чисел. <code>awk</code> вычислят куб каждого из них. Что здесь замечательного? У нас есть две слабо связанные друг с другом программы, которым мы можем легко указать работать вместе. Поток текста создаваемый программой слева попадает по конвееру в программу справа, которая может читать этот поток, что-то с ним делать и создавать уже новый текстовый поток. Текстовый поток — общий формат для результата вычислений, <code>|</code> — операция, связывающая их воедино.<br /><br />Монады очень похожи. <code>>>=</code> берёт внутреннее вычисление из монады слева и подставляет его в вычисление справа, которое всегда должно создавать ещё одну монаду того же типа.<br /><br />Как вы уже, наверное, знаете, списки и тип <code>Maybe</code> в Хаскеле — монады. Например, пусть у нас есть простое вычисление, которое возвращает пару из числа и его куба обратно в монаду (<code>return</code>):<br /><pre class="sh_haskell">\x -> return (x, x^3)</pre>тогда мы можем взять список и направить его «по конвееру» в это вычисление:<br /><pre class="sh_haskell">ghci> [1,2,3,4,5] >>= \x -> return (x,x^3)<br />[(1,1),(2,8),(3,27),(4,64),(5,125)]</pre>Заметьте, что мы получили список пар. Это та же самая исходная монада (то есть список). Однако если мы возмьём значение <code>Maybe</code> и направим его в то же вычисление, на выходе у нас будет та же сама монада <code>Maybe</code>:<br /><pre class="sh_haskell">ghci> Just 5 >>= \x -> return (x,x^3)<br />Just (5,125)</pre>Таким образом, мы можем создать конвеер из двух вычислений, и поведение этого конвеера зависит от контекста (т.е. от того, какая монада используется), а не от самих вычислений. В отличии от юниксовых конвееров, монады строго типизованы, и сама система типов заботится о том, чтобы выход одной монады был совместим с входом другой. И в отличии от юниксовых конвееров, мы можем задавать наши собственные правила связывания (<code>>>=</code>). Например, такие: «не делать более 42 вычислений подряд» или «посмотреть на входное значение, сделать то или это». Классы монад содержат в себе подобные правила, как соединять вычисления.<blockquote><b>Дополнение:</b> в комментариях подсказывают, что гораздо более подробно и более строго аналогия между юниксовым конвеером и монадами разобрана в статье <a href="http://okmij.org/ftp/Computation/monadic-shell.html">Monadic i/o and UNIX shell programming</a>.</blockquote>Теперь, я надеюсь, вы понимаете монады не хуже меня (не обязательно полностью). Хочу обсудить несколько мнемонических правил. <b>Почему <code>return</code> называется <code>return</code>?</b><br /><br />В большинстве языков <code>return</code> возвращает результат вычисления из функции. В Хаскеле же он конструктор для монад. Это очень странно. Однако посмотрим как работает <code>>>=</code>: эта операция <em>извлекает</em> значение из монады слева, а затем связывает его с аргументом функции справа (отсюда, кстати, и другое название метода — bind). А функция справа должна <em>вернуть</em> значение обратно в монаду, чтобы можно было передать эстафетную палочку дальше следующей операции <code>>>=</code>. Это первое мнемоническое правило: <code>return</code> — возвращает вычисленное значения обратно в монаду.<br /><br />Вторая мнемоника. Функция верхнего уровня любой программы на Хаскеле выполняется в монаде <code>IO</code> (тип функции <code>main</code> — <code>IO ()</code>). Эта монада позволяет выполнять ввод-вывод и вообще любые последовательные действия. Таким образом, монадный код выполняется на самом верхнем уровне программы, и именно он вызывает любой «чистый» код по мере необходимости, а не наоборот. Таким образом, любое «чистое» значение, если не отбрасывается, то рано или поздно <em>возвращается</em> в монаду её вызвавшую.<br /><br />Надеюсь, что после этих объяснений имя <code>return</code> для монадного конструктора больше не кажется таким уж странным. Я, однако, не утверждаю, что мои объяснения 100% технически верны.<br /><br />Следующий вопрос бывшего ОО-программиста, <b>как вытащить значение вычисления из монады?</b>. Начнём с того, что монады преднамеренно спроектированы именно так, что это не всегда возможно. Например, нельзя извлечь чистое значение из монады <code>IO</code>. Если дана такая «односторонняя» монада, то всё, что можно с ней делать — передавать внутреннее значение по монадному конвееру дальше. В Хаскеле разработан специальный синтаксис с ключевым словом <code>do</code>, которые делает такую многократную передачу монады по конвееру очень похожей на последовательную императивную программу. Следующие две программы делают одно и то же. Первая записана с <code>do</code>-нотацией:<br /><pre class="sh_haskell">main :: IO ()<br />main = do<br /> name <- getLine<br /> putStrLn ("Hi, " ++ name)</pre>а вторая явно использует <code>>>=</code>:<br /><pre class="sh_haskell">main :: IO ()<br />main = getLine >>= \name -> putStrLn ("Hi, " ++ name)</pre>Эквивалентная программа на Питоне:<br /><pre class="sh_python">from sys import stdin, stdout<br /><br />if __name__ == "__main__":<br /> name = stdin.readline()<br /> stdout.write("Hi, " + name)</pre>Однако иногда вытащить чистое значение из монадного вычисления можно. Это не предусмотрено общим монадным интерфейсом, поэтому разработчик монады должен специально предусмотреть возможность извлекать значения наружу. Например, можно извлекать значения из монады <code>Maybe</code>, используя функцию <code>fromMaybe</code>:<br /><pre class="sh_haskell">ghci> fromMaybe 0 $ Just 3<br />3<br />ghci> fromMaybe 0 $ Nothing<br />0</pre><br /><br /><b>Заключение по монадам</b><br /><br />Итак, связывание (<code>>>=</code>) позволяет объединять различные монадные вычисления вместе. Почти везде, где есть цепь вычислений, монады очень подходят. Конкретные реализации монад могут содержать разные правила комбинирования вычислений. Имя метода <code>return</code> сбивает с толку начинающих, это метод возвращает результа вычисления обратно в монаду, а не из монады. В общем, когда я понял эти простые идеи, это сильно помогло.<br /><br /><h3>5. Страшные слова</h3><br /><div style="padding-left: 50%; font-style: italic;">Я знаю только то, что ничего не знаю.</div><br /><br />Даже спустя месяцы после того, как я начал учить Хаскель, умея написать какие-то полезные программы на нём, я вижу вокруг, в мире Хаскеля, ещё много понятий, о которых не знаю ничего или имею только очень смутное представление. Я называю такие понятия «страшными словами». И я вижу, что есть люди, которые создают и используют библиотеки, воплощающие эти понятия в жизнь.<br /><br />Надо признать, Хаскель остаётся испытательной площадкой для исследователей. И это одновременно и хорошо, и плохо. Это хорошо, потому что даёт чувство, что передний край науки и технологии очень близок, и можно при желании пользоваться преимуществами новых подходов. При желании. И одновременно это плохо, потому что иногда, когда хочется использовать новую изящную библиотеку, оказывается, что она активно использует незнакомые и не совсем понятные идеи, и нужно быть готовым такие идеи осваивать.<br /><br />Например, есть современная XML-библиотека HXT. Она очень много использует <em>стрелки</em>. Стрелки — более универсальные комбинаторы вычислений, чем монады, но мне потребовалось гораздо больше, чем один день, чтобы их более-менее понять. Строго говоря, стрелки не являются частью языка, но они — понятие, которое применяют пользователи этого языка. И таких примеров немало. Хотя тем, кому стрелки осваивать не хочется, можно пользоваться более традиционной и активно поддерживаемой XML-библиотекой HaXml.<br /><br />Я думаю, важно не бояться «страшных слов». К счастью, основополагающие идеи хорошо описаны. Как правило, есть статьи их очень детально объясняющие. Я сам решил осваивать такие идеи по мере необходимости. Это обещает быть и увлекательным, и одновременно посильным.<br /><br /><h3>Заключение</h3><br /><br />Я перечислил пять простых идей, освоив которые, мне стало легче привыкнуть к Хаскелю. Лямбды — это просто способ записи функций, и функции нескольких аргументов можно всегда записать как функцию одного, возвращающую другую функцию. Типы классов очень похожи на абстрактные полиморфные интерфейсы в объектно-ориентированном подходе. Монады — стандартизованный способ соединять вычисления вместе. А страшные слова — просто страшные слова. Без них можно жить, но скучно.<br /><br />Надеюсь, мои заметки будут полезны и ещё кому-нибудь.<br /><br /><em>Эта статья есть также <a href="http://nix-tips.blogspot.com/2009/06/haskell-horrors.html" title="Haskell horrors">по-английски</a>. <a href="http://nix-tips.blogspot.com/2009/06/haskell-horrors.html" title="Haskell horrors">This post is also available in English.</a></em>Unknownnoreply@blogger.comtag:blogger.com,1999:blog-37628976.post-28490035771917287362009-05-26T17:57:00.008+04:002009-08-20T15:36:46.086+04:00Twtrize — сократитель речиКак известно, письменность избыточна: мы можем угадывать написанные слова, даже если некоторые буквы неразборчивы, перепутаны местами или вообще отсутствуют. К счастью, в компьютерной письменности все буквы разборчивы, почерк у всех одинаково хорош. Именно поэтому появилась возможность очень сильно сокращать слова, убирая из них «лишние» буквы.<br /><br />Люди иногда сознательно сокращают слова, набирая SMS или твиты — чтобы потратить меньше денег или укоротить сообщение.<br /><br />Идея возникла, когда на одном из многочисленных «сократителей URL» я увидел надпись «Shrink text». И мне пришло в голову, что вот он возьмёт, и сократит сам текст: выдаст что-нибудь вроде «shrnk txt». Конечно, сервис всего лишь заменял в тексте URL, но я подумал, что можно было бы сокращать и сам текст.<br /><br />Не знаю, как в английском, а в русском, по-моему, можно убрать довольно много гласных букв, а текст будет по-прежнему читаться. Я решил испытать идею, и написал этот сократитель.<br /><br />Программа преобразует текст на русском языке, выкидывая из него некоторые буквы и символы. Прошу рассматривать это как забавную игрушку и программой не злоупотреблять.<br /><br /><h3>Зависимости</h3><br />Программа написана на Literate Haskell (это значит, что то, что, вы сейчас читаете, и есть программа!). Используются следующие модули:<br /><pre>> import System.IO.UTF8 as U<br />> import Data.Char (toLower)<br />> import Text.Regex.Posix ((=~))<br />> import Data.Char (isPunctuation)</pre><br />TODO: Я использую старый способ работать с UTF-8 (utf8-string), надо переделать под новую библиотеку text.<br /><br /><h3>Алгоритм</h3><br />Данная программа «сжимает» русский текст так:<br />I. Из слов убираются (почти) все гласные и мягкие знаки,<br /><pre>> filterVowels = filter (`notElem` (aVowels ++ jVowels))</pre><br />Неприкосновенны гласные, которые:<br />I.a. являютя частью приставки «не-»<br /><pre>> rmVowels = map wordFilter<br />> where<br />> wordFilter ('н':'е':cs) = "не" ++ wordFilter cs</pre><br />I.b. стоят в трёх- и менее -буквенных словах<br /><pre>> wordFilter w = if length w <= 3<br />> then w</pre><br />I.c. стоят в начале или конце слова<br /><pre>> else<br />> let (prefix,inner,ending) = splitWord w<br />> in prefix ++ (ajaFilter inner) ++ ending</pre><br /><pre>> splitWord s = let p = takeWhile dontRemove s<br />> r = drop (length p) s<br />> e = reverse $ takeWhile dontRemove $ reverse r<br />> m = take ((length r) - (length e)) r<br />> dontRemove c = c `elem` vowels || isPunctuation c<br />> in (p,m,e)</pre><br />I.d. являются комбинациями со звуком «й»: «-ою-», «-ая—» и проч.<br /><pre>> ajaFilter [] = []<br />> ajaFilter s = let (b,m,a) = s =~ diftPat :: (String,String,String)<br />> diftPat = "[" ++ vowels ++ "][" ++ jVowels ++ "]"<br />> in (sameConsFilter b) ++ m ++ (ajaFilter a)</pre><br />I.e. стоят меж двух одинаковых согласных<br /><pre>> sameConsFilter [] = []<br />> sameConsFilter s =<br />> let (b,m,a) = s =~ sameConsPat :: (String,String,String)<br />> sameConsPat = "(["++consonants++"])[" ++ vowels ++ "]\\1"<br />> in (filterVowels b) ++ m ++ (sameConsFilter a)</pre><br />Программа использует такой список гласных:<br /><pre>> vowels = aVowels ++ jVowels</pre><br />где есть и простые гласные (к ним же причислен и мягкий знак)<br /><pre>> aVowels = "аиоуыэь"</pre><br />и дифтонгообразующие (не знаю правильного термина — в общем, дающие звук «й»),<br />к ним же причислена и буква «й»:<br /><pre>> jVowels = "яйёюе"</pre><br />Для некоторых правил требуется также список русских согласных:<br /><pre>> consonants = "бвгджзклмнпрстфхцчшщ"</pre><br />II. из предложений убираются знаки препинания, кроме точек, вопросительных и восклицательных знаков<br /><pre>> rmSomePunctuation = filter (not . null) . map rmTrailing<br />> where rmTrailing = reverse . rmHead . reverse<br />> rmHead [] = []<br />> rmHead s@(c:cs) = case c `elem` rmlist of<br />> True -> rmHead cs<br />> False -> s</pre><br /> Список подлежащих удалению знаков препинания:<br /><pre>> rmlist = ",;-—:–"</pre><br />III. из текста удаляются некоторые предлоги (в телеграфном стиле)<br /><pre>> rmPrepositions = filter (`notElem` preps) . words<br />> where preps = [ "в", "во", "на", "над", "к", "от", "из"<br />> , "по", "под", "через" ]</pre><br />IV. для пущей стилизации текст пишется в нижнем регистре<br /><pre>> tolower = map toLower</pre><br /><br /><h3>Использование программы</h3><br />Программу можно использовать как простой unix-фильтр: он читает текст из потока stdin и печает «сжатый» текст в стандартный вывод (stdout).<br /><pre>> main = U.interact $ (++ "\n") . twtrize<br /><br />> twtrize = unwords . filter ( not . null ) .<br />> rmVowels . rmSomePunctuation . rmPrepositions . tolower</pre><br />Пример:<br /><blockquote><pre> $ <strong>printf "Гласные, а также некоторые предлоги — как, например, «на», — из \<br /> текста удаляются, но какие-то остаются.\n" | runhaskell twtrize.lhs</strong><br /> глсные а ткже нектрые прдлги как нпрмр «на» ткста удляются но какие-то<br /> остаются.</pre></blockquote><br /><br />Последняя версия: <a href="http://bitbucket.org/jetxee/twtrize/src/tip/twtrize.lhs">исходник здесь</a>. Лицензия: BSD-3.Unknownnoreply@blogger.comtag:blogger.com,1999:blog-37628976.post-77135910258484347162009-05-15T16:31:00.005+04:002009-06-12T21:12:57.979+04:00Ledger — бухучёт в командной строкеРешил, что надо наводить порядок в своей жизни и деньги считать. В общем, вести домашнюю бухгалтерию. Познания в бухучёте у меня очень скромные (хотя когда-то и прослушал вводный курс), помню только, что такое <a href="http://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B9%D0%BD%D0%B0%D1%8F_%D0%B7%D0%B0%D0%BF%D0%B8%D1%81%D1%8C">двойная запись</a>, и <a href="http://ru.wikipedia.org/wiki/%D0%94%D0%B5%D0%B1%D0%B5%D1%82">дебет/кредит</a>. Когда-то, правда, пользовался <a href="http://eugeniavlasova.com/archives/zhizn_v_linukse/gnucash_programma_ucheta_finansov_.html" title="Введение в GNUcash">GNUcash</a>.<br /><br />Кроме GNUcash в линуксе есть ещё несколько программ для учёта личных финансов: KMyMoney и Grisbi. Программы красивые, удобные, наглядные. Однако меня впечалил и заинтересовал <code>ledger</code>. Это бухучёт в стиле unix. В общем, для фанатов.<br /><br />Идея проста: записываем все расходы/доходы в текстовый файл (файл редактируем сами, программа его не трогает), а программа всегда поможет проверить баланс и составить отчёт о текущем состоянии или по периоду. Что может быть естественней, проще, надёжней? Полный простор в организации учёта.<br /><br />Отмечу, что вначале я нашёл не сам <a href="http://github.com/jwiegley/ledger/tree" title="ledger: A double-entry accounting system with a command-line reporting interface">ledger</a>, а его клон <a href="http://hledger.org/Hledger" title="hledger is a text-mode double-entry accounting tool">hledger</a>, написанный на Haskell. Есть ещё и вариант написанный на Python — <a href="http://furius.ca/beancount/" title="BeanCount: Command-line Double-Entry Accounting">beancount</a>. Какую программу выбрать — дело вкуса. Формат файла, к счастью, у них (почти) одинаковый. «Старший» <code>ledger</code> уже есть в репозиториях Ubuntu и Debian, но мне пока больше понравился вариант на Haskell (исходники короче и понятнее), про него и буду рассказывать.<br /><br />Хотя сложного в использовании <code>ledger</code> вроде ничего нет, трудно начать, потому что все примеры в сети на английском и используют английскую бухгалтерскую терминологию. Для меня, чтобы разобраться, было важно понять <a href="http://joyful.com/repos/ledger/doc/ledger/File-format.html#File-format">формат</a> в котором вести записи. Итак, файл состоит из записей, формат каждой записи<pre>ДАТА[=ФАКТИЧЕСКАЯ ДАТА] [*|!] [(КОД)] [ОПИСАНИЕ] [ ; КОММЕНТАРИЙ ]<br />отступ НАЗВАНИЕ СЧЕТА хотя бы два пробела СУММА [ ; КОММЕНТАРИЙ ]<br />отступ НАЗВАНИЕ ДРУГОГО СЧЕТА [ хотя бы два пробела СУММА ] [ ; КОММЕНТАРИЙ ]<br />[другие счета, если необходимо]</pre>Каждая запись начинается с цифры, то есть даты. Даты пишем в формате ISO, ГГГГ-ММ-ДД. Для краткости можем указать в файле<pre>Y2009</pre>и все последующие записи без указания года будут относится к 2009-му году.<br /><br />Описание проводки может быть любым. Номера квитанций, счетов и тому подобные коды можно вписывать в поле (КОД) (указывать перед описанием в скобках).<br /><br />СУММЫ можно писать так, как удобно, «13», «$42», «17 EUR», «121 Kb», «9 L». <code>Ledger</code> понимает, что разные единицы измерения нужно считать отдельно. Единицы можно указывать любые, те, которые нужно учитывать. В том числе и неденежные (и программа умеет их правильно пересчитывать, если ей дать файл с историей цен).<br /><br />В каждой записи обычно два или три счета, но указывать сумму для одного из них необязательно, понятно, что изменение будет равно сумме всех других, но с противоположенным знаком:<pre>2009-05-14<br /> расходы:обед 123 RUB<br /> актив:наличные ; понятно, что здесь должно быть -123 RUB</pre>Счета можно называть как угодно. В том числе и по-русски. Можно делать подсчета, используя двоеточие в названии счёта, например, «расходы:услуги:интернет». Видимо, необходимо иметь счета как минимум пяти категорий: актив, долги, доходы, расходы и какой-то счёт для уравнивания балансов. Я назвал его «собственные», в англоязычных примерах его обычно называют «equity».<br /><br />Дело в том, что двойная запись подразумевает, что всегда выполняется закон сохранения денег. Всегда, когда к какому-то счёту мы их приписываем, точно такую же сумму мы должны с какого-то счёта списать. Поэтому уже в начале, чтобы указать сумму денег в кошельке и на счету в банке, нужно ввести некий счёт, с которого они «пришли».<br /><br />Составим такой файл со счетами, для начала. Пусть у нас есть текущий счёт в банке, на котором лежит 1001 рубль (единицы не будем указывать для краткости), есть что-то в кошельке, скажем 150 рублей. Пишем файл:<br /><pre>Y2009<br /><br />05-14 начальное состояние счета в банке<br /> актив:банк 1001<br /> собственные:начало<br /><br />05-14 начальное содержимое кошелька<br /> актив:наличные 150<br /> собственные:начало</pre>Сохраняем в файл, например <code>~/.ledger</code> (там его по-умолчанию ищет <code>hledger</code>). Смотрим, что получилось:<pre>~$ <strong>hledger</strong><br /> 1151 актив<br /> 1001 банк<br /> 150 наличные<br /> -1151 собственные:начало</pre>Программа сама догадалась подсчитать баланс виртуального счёта «актив». Всё сходится: всё, что было списано со счёта «собственные:начало» оказалось в «активе». Если что-то не сходится, программа будет страшно ругаться.<br /><br />Как записывать расход-приход — см пример записи выше. Допустим, мы что-то купили и что-то получили в подарок: <pre>05-15 * подарок<br /> актив:наличные 100<br /> доходы:подарки<br />05-15 * яблоко<br /> расходы:покупки 9<br /> актив:наличные</pre>Я добавил звёздочку перед описнием проводок. Как её интерпретировать — дело хозяйское. Я помечаю ей те операции, которые уже завершены (а восклицательным знаком — те, которые запланированы). Программа потом позволяет легко отбирать проводки со звёздочкой и без.<br /><br />Предположим, например, что на завтра я запланировал ответный подарок, но ещё его не сделал: <pre>05-16 ! ответный подарок<br /> расходы:подарки 90<br /> актив:наличные</pre>Теперь, если посмотреть баланс с ключиком <code>-C</code>, увидим только то, что завершено: <pre>$ <strong>hledger -C bal</strong><br /> 91 актив:наличные<br /> -100 доходы:подарки<br /> 9 расходы:покупки</pre>Чтобы посмотреть не баланс, а список проводок, есть команда <code>register</code>: <pre>~$ <strong>hledger reg</strong><br />2009/05/14 начальное состояни.. актив:банк 1001 1001<br /> собственные:начало -1001 0<br />2009/05/14 начальное содержим.. актив:наличные 150 150<br /> собственные:начало -150 0<br />2009/05/15 подарок актив:наличные 100 100<br /> доходы:подарки -100 0<br />2009/05/15 яблоко расходы:покупки 9 9<br /> актив:наличные -9 0<br />2009/05/16 ! ответный подарок расходы:подарки 90 90<br /> актив:наличные -90 0</pre>Отмечу, что в «сишной» версии <code>ledger</code> колонки здесь <a href="http://hpaste.org/fastcgi/hpaste.fcgi/view?id=4900#a4900" rel="nofollow">разъедутся</a>. А вот в <code>hledger-0.5</code> уже всё в порядке (и для 0.4 я тоже сделал <a href="http://sites.google.com/site/sovetyplus/Home/hledger-0.4-utf8.diff?attredirects=0">патчик</a>).<br /><br />В общем, баланс (<code>balance</code>) и проводки (<code>register</code>) — два главных отчёта. Можно ограничивать отчёты по диапазону дат или периодам. Или по названиям счетов. Так, чтобы выбрать все счета со словом «подарки» в названии, используем регулярное выражение: <pre>$ <strong>hledger reg .*подарки.*</strong><br />2009/05/15 подарок доходы:подарки -100 -100<br />2009/05/16 ! ответный подарок расходы:подарки 90 -10</pre>Чтобы выбрать записи по описанию, поступаем так: <pre>$ <strong>hledger reg desc:яблоко</strong><br />2009/05/15 яблоко расходы:покупки 9 9<br /> актив:наличные -9 0</pre>В общем, идея понятна. Для отчётов по неделям и по месяцам есть ключики <code>-W</code> и <code>-M</code>.<br /><br />Бывают ещё периодические записи, но только в <code>ledger</code>, в <code>hledger</code> их пока нет. Они не означают, что со счёта что-то автоматически списывается. Проводку нужно всё равно вносить вручную, но периодические записи позволяют рассчитывать прогноз бюджета. В <code>ledger</code> также можно делать виртуальные записи, ими можно автоматически учитывать разные комиссии или проценты.<br /><br />Вот такая программка. Думаю, не одному мне понравится. Вижу большое достоинство программы в том, что формат данных очень простой и естественный. Посмотрю теперь, будет ли её хватать для моих нужд.<br /><br />Вот пример <a href="http://sites.google.com/site/sovetyplus/Home/ledger.%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80.txt?attredirects=0">ledger-файла</a>.Unknownnoreply@blogger.comtag:blogger.com,1999:blog-37628976.post-60928466876306422182009-04-13T02:46:00.008+04:002009-05-07T01:31:25.909+04:00Функциональное веб-программированиеМногие читали <a href="http://www.paulgraham.com/avg.html">Beating the Averages</a> Пола Грэхэма (есть <a href="http://www.nestor.minsk.by/sr/2003/07/30710.html" title="Побеждая посредственность / Beating the Averages">перевод</a>). Он использовал Lisp, и считает, что именно поэтому создал самое удачное веб-приложение (которое потом превратилось в Yahoo! Store). Прекрасно!<br /><br />А вот что реально уже готово и можно использовать <em>прямо сейчас</em>? Я решил не зацикливаться на Хаскеле, и вспомнил, что есть ещё Erlang, Caml, Scala, Scheme, Lisp. Стоило только начать поиски, и я нашёл много интересного. Делюсь находками.<br /><br /><span style="font-size:200%;">1.</span> Язык программирования <a href="http://www.erlang.org/">Erlang</a> и каркас для разработки веб-приложений <a href="http://nitrogenproject.com/" title="Nitrogen. Web Framework for Erlang">Nitrogen</a>. Кстати, сайт Nitrogen на нём же, видимо, и работает. Рекомендую посмотреть видео:<br /><br /><a target="_self" href="http://nitrogenproject.com/NitrogenDec2008.html">Видео: возможности Nitrogen (дек. 2008) (.mov)<br /><br /><img alt="скринкаст: Новые возможности Nitrogen (дек. 2008)" src="http://nitrogenproject.com/images/NitrogenDec2008.png" align="middle" width="320" height="215"></a><br /><br />Обратите внимание на счётчик числа строк, нужных, чтобы запрограммировать страницу (такой счётчик есть и на всех страницах сайта Nitrogen). Число, как правило, двузначное. Меня лично поразило <a href="http://nitrogenproject.com/web/samples/continuations">вот это — запуск долгоиграющих процессов на сервере</a>. 31 строчка! Чтобы сделать подобное на Python и Django, мне пришлось попыхтеть.<br /><br />Кроме Nitrogen, для Erlang есть ещё <a href="http://erlyweb.org/">Erlyweb</a> и <a href="http://www.erlang-web.org/">Erlang Web</a>.<br /><br /><span style="font-size:200%;">2.</span> Язык программирования <a href="http://www.haskell.org/">Haskell</a> и сервер приложений <a href="http://happstack.com/">Happstack</a>. Самое приятное, что проект хоть ещё и молодой, но уже <em>работающий</em>. На нём сделан <a href="http://tutorial.happstack.com/">Happstack-tutorial</a>, на нём работает <a href="http://patch-tag.com/">Patch-tag.com</a>. Первые энтузиасты даже делают на нём <a href="http://gregorycollins.net/posts/2009/03/28/building-a-website-part-1">свои блоги</a>.<br /><br />Запустить его на своей машине оказалось не очень трудно. И он <em>работает</em>. Но всё же, я чувствую, мне потребуется время, чтобы с ним более-менее разобраться. Документация, с точки зрения новичка, конечно, уступает документации Django.<br /><br />Основная отличительная черта Happstack: реляционная база данных ему не нужна. Можно пользоваться теми структурами данных, которые наиболее удобны. Возможность с одной стороны спорная, полагаться на неё страшно, с другой — весьма интересная.<br /><br />Вот маленькая презентация Happstack: <div style="width:425px;text-align:left" id="__ss_1278287"><a style="font:14px Helvetica,Arial,Sans-serif;display:block;margin:12px 0 3px 0;text-decoration:underline;" href="http://www.slideshare.net/guestc9e6277/better-than-x?type=powerpoint" title="Better Than X">Better Than X</a><object style="margin:0px" width="425" height="355"><param name="movie" value="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=better-than-x-090412145107-phpapp01&rel=0&stripped_title=better-than-x" /><param name="allowFullScreen" value="true"/><param name="allowScriptAccess" value="always"/><embed src="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=better-than-x-090412145107-phpapp01&rel=0&stripped_title=better-than-x" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="425" height="355"></embed></object><div style="font-size:11px;font-family:tahoma,arial;height:26px;padding-top:2px;"></div></div><br /><br />Но и не Happstack единым. Читаем <a href="http://jekor.com/article/is-haskell-a-good-choice-for-web-applications">Is Haskell a Good Choice for Web Applications?</a>. Весьма обнадёживает. Исходник работающего сайта — в подарок.<br /><br /><span style="font-size:200%;">3.</span> Язык программирования <a href="http://scala-lang.org/">Scala</a> (гибридный функциональный язык программирований для виртуальной машины Java) и каркас для веб-приложений <a href="http://liftweb.net/">Lift</a>. Можно посмотреть на его <a href="http://demo.liftweb.net/">демо</a>.<br /><br />Интересно, что буквально на днях Гугл объявил поддержку Java на AppEgnine, и народные умельцы уже <a href="http://mawson.wordpress.com/2009/04/10/first-steps-with-scala-on-google-app-engine/" title="Scala на Google AppEngine">используют там Scala</a>. Более того, точно так же <a href="http://lift-example.appspot.com/index">на AppEngine запустили и Lift</a>.<blockquote><small>Scala — по-итальянски лестница. Думаю, игра слов scala — lift теперь всем понятна.</small></blockquote><br /><br /><span style="font-size:200%;">4.</span> Язык программирования <a href="http://caml.inria.fr/">Caml</a> и каркас для веб-приложений <a href="http://ocsigen.org/" title="Ocsigen: OCaml web framework">Ocsigen</a>. Точнее, как я понял, Ocsigen — это веб-сервер, а каркас для веб-приложений называется Eliom. И есть ещё набор библиотек Ocsimore. Однако это детали.<br /><br />Интересные особенности Ocsigen: валидность XHTML документа гарантируется на уровне типов языка, активно используется стиль <a href="http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D0%B4%D0%BE%D0%BB%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5">отложенных вычислений</a>, управление сессиями, URL-схемами и параметрами страниц автоматическое.<br /><br />В репозитариях основных дистрибутивов Ocsigen уже есть, так что приступить к использованию будет легко. Вот, например, <a href="http://code.google.com/p/nurpawiki/">вики-органайзер</a> написанный с использованием Ocsigen. Разработчики тоже не стесняются использовать детище <a href="http://www.pps.jussieu.fr/">для своих сайтов</a>. Добрый знак. Работает, говорят, <a href="http://eigenclass.org/R2/writings/standalone-ocaml-webapps">очень быстро</a>. Однако будем помнить, что это прежде всего исследовательский проект.<br /><br /><blockquote>Читая сайт Ocsigen, нашёл и совершенно удивительный проект. Немного не в тему, но очень здорово: <a href="http://www.pps.jussieu.fr/~canou/obrowser/tutorial/">O'Browser</a>, написанная на Javascript виртуальная машина для байт-кода OCaml. Что это значит? Значит, что код на OCaml можно встраивать в веб-страницы! Вот, например, <a href="http://www.pps.jussieu.fr/~canou/obrowser/examples/boulderdash/">клон Boulderdash</a>. Что-то подобное <a href="http://www.haskell.org/haskellwiki/Haskell_in_web_browser">есть и для Haskell</a>.</blockquote><br /><br /><span style="font-size:200%;">5.</span> Язык программирования <a href="http://www.plt-scheme.org/">Scheme</a> и каркас для веб-приложений <a href="http://blog.leftparen.com/">LeftParen</a>. Документация выглядит толково. К сожалению, не нашёл сайтов, которые его используют. Или какого-нибудь демонстрационного сайта.<br /><br />Вообще-то LeftParen построен вокруг уже довольно развитой инфраструктуры для веб-программирования в PLT Scheme. И её вполне можно использовать непосредственно, см. <a href="http://docs.plt-scheme.org/web-server/index.html">документацию</a> и <a href="http://docs.plt-scheme.org/continue/index.html">учебник</a>.<br /><br />Для Scheme есть ещё <a href="http://www.lshift.net/blog/2006/05/22/icing-lightweight-web-development-in-scheme">Icing</a>.<br /><br /><span style="font-size:200%;">6.</span> Язык программирования <a href="http://ru.wikipedia.org/wiki/Common_Lisp">Lisp</a> и веб-каркас <a href="http://weblocks.viridian-project.de/">Weblocks</a>. Сайт и документация очень мне понравились. Если бы я писал на Лиспе, начал бы, наверное, с этого каркаса. Вот <a href="http://weblocks.viridian-project.de/weblocks-demo">демонстрация</a>.<br /><br />Есть ещё <a href="http://homepage.mac.com/svc/kpax/">KPAX</a> (это не русское слово, просто такое смешное сокращение!). Пишут, что уже давно и серьёзно используется, но документация страдает. Для Лиспа есть ещё <a href="http://common-lisp.net/project/ucw/">UnCommon Web</a>, <a href="http://www.bknr.net/html/home.html">BKNR</a>.<br /><br /><span style="font-size:200%;">7.</span> Ещё один диалект Лиспа — язык программирования <a href="http://clojure.org/">Clojure</a>, как и Scala рассчитан на использование на платформе JVM. Надо ли говорить, что и для него есть веб-каркас: <a href="http://github.com/weavejester/compojure/tree/master">Compojure</a>. И на AppEngine его тоже оперативно <a href="http://elhumidor.blogspot.com/2009/04/clojure-on-google-appengine.html">запустили</a>.<br /><br /><br /><br />Вот так-то. Сколько всего, оказывается. Глаза разбегаются.<br /><br />Созвучен этой заметке будет вот <a href="http://groups.google.com/group/ltu-kiev/browse_thread/thread/5b335f371c26c1e0?fwc=1">этот пост</a>.Unknownnoreply@blogger.comtag:blogger.com,1999:blog-37628976.post-55461993198900604612009-04-10T11:38:00.008+04:002010-07-07T19:09:59.424+04:00Иерархия числовых типов в HaskellВ Хаскеле очень затейливая иерархия числовых типов. Всё дело в том, что кроме обычных машинных целых и чисел с плавующей точкой, в ней нашлось место и числам произвольной точности (настоящим целым и рациональным), и комплексным, и числам с фиксированной точностью.<br /><br />Цитирую по официальному <a href="http://www.haskell.ru/basic.html#sect6.4" title="числовые типы в Haskell">описанию языка</a>: <blockquote>Класс <code>Num</code> числовых типов является подклассом класса <code>Eq</code>, так как все числа можно сравнить на равенство; его подкласс <code>Real</code> также является подклассом класса <code>Ord</code>, так как остальные операции сравнения применимы ко всем числам, за исключением комплексных (определенных в библиотеке <code>Complex</code>). Класс <code>Integral</code> содержит целые числа ограниченного и неограниченного диапазона; класс <code>Fractional</code> содержит все нецелые типы; а класс <code>Floating</code> содержит все числа с плавающей точкой, действительные и комплексные.</blockquote>С одной стороны, система устроена очень логично: введение дополнительных операций расширяет множество чисел. Так, деление целых дополняет целые — рациональными. Логарифмы и экспоненты требуют действительных. Однако сразу представить и запомнить всю иерархию классов непросто. <em>Лучше один раз увидеть!</em> Поэтому <s>хозяйке</s> себе на заметку и другим на потеху я составил вот такую памятку:<br /><br /><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcC3ZI3jF2ClyueNnztGseVVZqVESmnT-WlzAo3wbLlRSOKM_g1OzgS0eG9hG_HkiaU9_Dr5YCMzswZggZjkz_2C_EB4YxNdmObsjzqJuVtaWNc6mVpSY7URtW698Q80mg0ocQ6g/"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgIe71lgrCvaWC2vbIE3_eycUoCeaqSHzi1hpVNK-7N-RlXG2l17cBfIxXO0gK68OqXKmVSyAS3RGssIIrhD7uOJcxyX02FqOeOZYcmaDEJhxbhyFdtYoY8sDvklEDRdljH7Sm8LA/s576/hs-nums.png" /></a><br /><small><a href="http://sites.google.com/site/sovetyplus/Home/hs-nums.txt">Исходник диаграммы</a></small><br /><br />Оформил как «диаграмму классов», чтобы всем привыкшим к ООП было понятно, что от чего «наследуется» (линия со стрелкой к родителю). Почти все эти типы являются абстрактными («интерфейсами» в ОО-терминологии). Конкретные же числовые типы я обвёл серыми рамочками (можно было обвести и <code>Ratio</code>). Полиморфные классы обозначил прямоугольниками со скруглёнными углами. Типы параметров таких полиморфных классов указал «аггрегацией» (линия с ромбиком на стороне полиморфного класса). При составлении ориентировался на <a href="http://www.haskell.org/ghc/docs/latest/html/libraries/base/Prelude.html">первоисточник</a>.<br /><br /><a href="http://nix-tips.blogspot.com/2010/07/hierarchy-of-numeric-typeclasses-in.html" title="The hierarchy of numeric typeclasses in Haskell">Also in English</a>.Unknownnoreply@blogger.comtag:blogger.com,1999:blog-37628976.post-79804962751889531342009-04-04T00:35:00.003+04:002009-04-04T00:55:44.322+04:00gettext для ХаскеляВасиль Пастернак написал Haskell-интерфейс для gettext (библиотека для перевода программ на разные языки — интернационализации, как теперь <s>говорят</s> пишут).<br /><br />Я хочу дать ссылки на пару его заметок:<br /><ul><li><a href="http://progandprog.blogspot.com/2009/03/i18n-and-haskell.html">пример простой программы, использующей hgettext</a><br /><li><a href="http://progandprog.blogspot.com/2009/04/configure-and-install-internationalized.html">пример создания cabal-пакета при использовании hgettext</a></ul>Заметки написаны по-английски, но там, в основном, примеры кода, так что разобраться будет нетрудно.<br /><br />Также: <a href="http://hackage.haskell.org/cgi-bin/hackage-scripts/package/hgettext">hgettext в HackageDB</a>.Unknownnoreply@blogger.comtag:blogger.com,1999:blog-37628976.post-12838199321470872952009-03-30T12:34:00.006+04:002009-08-20T15:28:50.094+04:00Скрипты на Хаскеле (пробую писать)Я, кажется, созрел, чтобы переходить от чтения книжек и статей про Хаскель к попыткам что-то на нём писать самому. Вначале какую-нибудь мелочь. Скрипты, в общем. Поскольку я уже как-то публиковал здесь bash-скрипт <a href="http://sovety.blogspot.com/2008/02/rss-bash.html">rss2lj</a> (кросспост RSS в ЖЖ), то решил в качестве упражнения его переписать и улучшить. Думаю, получилось. В этой заметке расскажу о том, как писал. Ну и о впечатлениях. Скрипт выложен <a href="http://bitbucket.org/jetxee/feed2ljhs/src/">на BitBucket</a> и <a href="http://hackage.haskell.org/package/feed2lj">на Hackage</a>.<br /><br />Содержание: <ul><li><a href="#intro">Вступление</a><br /><li><a href="#overview">Предварительное описание задачи и подхода</a><br /><li><a href="#ljpost">Модуль отправки сообщений в ЖЖ (<code>LjPost</code>)</a><br /><li><a href="#feed2lj">Обработка RSS/Atom фида (<code>Feed2Lj</code>)</a><br /><li><a href="#happyend">Заключение</a></ul><br /><a id="intro"> </a>Задача состоит из кучи рутинных операций. Я думаю, именно поэтому, будет полезно и мне на будущее, и другим начинающим и пробующим, увидеть, как они выполняются на Хаскеле. В частности, по ходу дела я разобрался как <ul><li>обрабатывать аргументы коммандной строки, <li>читать и писать файлы, <li> использовать регулярные выражения, <li> отсылать HTTP-запросы, <li> выполнять ввод-вывод в уникоде (UTF-8), <li> получать системное время.</ul>Писать буду <em>как начинающий — начинающим</em>. На словах получается довольно долго, но <a href="http://bitbucket.org/jetxee/feed2ljhs/src/" title="A Haskell script to crosspost any feed (RSS, Atom) to LiveJournal. Custom templates.">сам код</a> получился гораздо короче, чем эта статья (около 200 строк, считая комментарии, необязательные декларации типов, пустые строки и декларации импорта внешних модулей).<br /><br />Хотя Хаскель язык компилируемый и строго типизированный, использовать его для таких дел вполне можно. Код получается примерно такой же, если не более, краткий, как на Python, а компилируется даже на лету достаточно быстро. Есть и особенности. Во-первых, вместо беззаботного <a href="http://ru.wikipedia.org/wiki/%D0%A3%D1%82%D0%B8%D0%BD%D0%B0%D1%8F_%D1%82%D0%B8%D0%BF%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F">duck-typing</a> здесь — строгая типизация. Поэтому писать надо аккуратнее (но и ошибок при исполнении меньше). Однако в Хаскеле эта строгая типизация сделана на основе <a href="http://ru.wikipedia.org/wiki/%D0%92%D1%8B%D0%B2%D0%BE%D0%B4_%D1%82%D0%B8%D0%BF%D0%BE%D0%B2">системы типов Хиндли–Миллнера</a> и, в отличие от C++, под ногами не путается. Во-вторых, чтобы использовать преимущества функционального подхода (например, <a href="http://antilamer.livejournal.com/262237.html">отложенные вычисления</a>, частичное применение функций) нужно отделять чисто функциональную часть программы от императивных фрагментов. В простейшем случае, это означает необходимость отделить операции ввода-вывода от вычислений (преобразования информации). Переводя на Хаскель: функции ввода-вывода будут иметь монадный тип <code>IO a</code>, остальные же будут чистыми (без <code>IO</code> в типе).<br /><br /><h3 id="overview">Предварительное описание задачи и подхода</h3>В моём примере можно выделить следущие операции ввода-вывода: <ul><li>получение URL из аргументов командной строки,<li>чтение содержимого RSS или Atom фида по заданному URL,<li>чтение (и потом запись) файла со списком уже обработанных записей,<li>чтение файла с настройками доступа к учётной записи ЖЖ,<li>получение системного времени,<li>коммуникация с ЖЖ по установленному <a href="http://www.livejournal.com/doc/server/ljp.csp.flat.protocol.html">протоколу</a>.</ul>И соответственно следующие преобразования данных:<ul><li>извлечение идентификаторов всех записей в фиде,<li>отсев уже обработанных записей,<li>извлечение заголовков, ссылок и текста оставшихся записей,<li>форматирование записей по заданному шаблону,<li>разбор файла с настройками.</ul>Для разбора произвольных фидов я велосипед изобретать не стал, а воспользовался библиотекой <a href="http://hackage.haskell.org/cgi-bin/hackage-scripts/package/feed" title="feed: Interfacing with RSS (v 0.9x, 2.x, 1.0) + Atom feeds.">feed</a>. А для всех коммуникаций по HTTP протоколу использовал библиотеку <a href="http://hackage.haskell.org/cgi-bin/hackage-scripts/package/curl" title="curl: Haskell binding to libcurl">curl</a> (мне понравился её интерфейс). Обе библиотечки нашёл на <a href="http://haskell.org/hoogle/">Hoogle</a>, а установил с помощью <code>cabal</code>. Из остальных зависимостей: нужен модуль <code>Codec.Binary.UTF8.String</code> (в убунту и дебиан он помещён в пакет <code>libghc6-utf8-string-dev</code>), модуль <code>Text.Regex.Posix</code> (соответственно, пакет <code>libghc6-regex-posix-dev</code>). Потом я сейчас заметил, что использовал <code>urlEncode</code> из <code>Network.HTTP</code> (у меня в <code>~/.cabal</code>), хотя можно было обойтись пакетным <code>escapeURIString</code> (из <code>Network.URI</code>). То есть одна зависимость могла бы быть попроще.<br /><br />В отдельный модуль я выделил всё, что касается связи связи с ЖЖ и его протокола (файл <a href="http://bitbucket.org/jetxee/feed2ljhs/src/tip/LjPost.hs">LjPost.hs</a>). Собственно всю логику скрипта я поместил в другом файле (<a href="http://bitbucket.org/jetxee/feed2ljhs/src/tip/Feed2Lj.hs">Feed2Lj.hs</a>). Вспомогательную утилитку для тестирования модуля <code>LjPost</code> я поместил в <a href="http://bitbucket.org/jetxee/feed2ljhs/src/tip/RunLjPost.hs">RunLjPost.hs</a>. Для использования скрипта она не нужна, я её использовал при его написании.<br /><br /><h3 id="ljpost">Модуль отправки сообщений в ЖЖ (<code>LjPost</code>)</h3><h4>Использование библиотеки Curl</h4>Как я уже сказал, для работы по HTTP протоколу я использовал библиотечку <code>curl</code>. Соответственно, помещаю в списке импортов <pre>import Network.Curl</pre>а основную функцию оформляю так, всё это достаточно «императивно»:<pre>postToLj ljuser ljpass subj msg = withCurlDo $ do<br /> curl <- initialize<br /> ...</pre>Функция <code>withCurlDo</code> должна охватывать все вызовы к <code>curl</code> и отвечает за инициализацию и деинициализацию библиотеки; <code>initialize</code> собственно и позволяет к библиотеке потом обращаться. Собственно HTTP запрос делается так (запрашиваю аутентификационный токен ЖЖ):<pre> r <- do_curl_ curl ljFlatUrl getChallengeOpts :: IO CurlResponse</pre>Т.е. используем <code>do_curl_</code>, чтобы получить данные HTTP-ответа; результат (HTTP-ответ) связываю (<code><-</code>) с переменной <code>r</code>; аргументы <code>do_curl_</code> были определены мной ранее, URL <a href="http://www.livejournal.com/doc/server/ljp.csp.flat.protocol.html">ЖЖ-API</a> <pre>ljFlatUrl = "www.livejournal.com/interface/flat"</pre>и собственно параметры запроса: <pre>getChallengeOpts = CurlPostFields ["mode=getchallenge"] : postFlags<br />postFlags = [CurlPost True]</pre>Дальнейшие действия определяются логикой протокола ЖЖ.<br /><h4>Разбор ответа ЖЖ</h4>Во flat-протоколе, ответ сервера выглядит так: <pre>ключ_1<br />значение_1<br />ключ_2<br />значение_2<br />...</pre>Нужно, во-первых, проверять значение ключа <code>success</code>, во-вторых извлекать значения других ключей, для начала ключа <code>challenge</code>.<br /><br />Поскольку здесь никакого ввода-вывода уже нет, эту часть кода вполне можно написать «функционально». Самый простой и универсальный сделать это, мне кажется, разбить тело ответа (<code>respBody</code>) на строчки (<code>lines</code>), преобразовать их в ассоциативный список (<code>list2alist</code>) и поискать в нём нужный ключ (<code>lookup</code>), получив, может быть (монада <code>Maybe</code>), значение: <pre>lookupLjKey :: String -> CurlResponse -> Maybe String<br />lookupLjKey k = ( lookup k . list2alist . lines . respBody )</pre>При этом функция преобразования списка в ассоциативный список простая двухстрочная рекурсия: <pre>list2alist :: [a] -> [(a,a)]<br />list2alist (k:v:rest) = (k,v) : list2alist rest<br />list2alist _ = []</pre>Всё, мы написали всё необходимое, чтобы разбирать ответы сервера.<br /><br />Вспомогательная функция, проверяем, успешен ли был запрос (тогда и только тогда, когда в ответе есть ключ <code>success</code> со значением <code>OK</code>): <pre>isSuccess :: CurlResponse -> Bool<br />isSuccess = (=="OK") . fromMaybe "" . lookupLjKey "success"</pre>Мы определили <code>isSuccess</code> композицией трёх функций. <code>lookupLjKey</code> возвращает монаду <code>Maybe String</code>. Функция <code>fromMaybe</code> достаёт из неё строковое значение. Функция сравнения <code>(==)</code> записана в префиксной форме и сравнивает значение со строкой «<code>OK</code>».<br /><br />Прошу заметить, что вытащить из монады <code>Maybe</code> собственно значение всегда можно с помощью <code>fromJust</code>, но если там ничего нет (<code>Nothing</code>), то будет возбуждена ошибка. Здесь функция <code>fromMaybe</code> возвращает в такой ситуации значение по умолчанию (пустую строку), но в других местах скрипта я часто использую <code>fromJust</code> без проверок (т.е. при отсутствии значения скрипт будет прерываться). В программах посерьёзнее, я думаю, лучше всегда использовать функции <code>maybe</code> или <code>fromMaybe</code>, позволяющие использовать <code>Maybe</code>-значения, указав для них значения по-умолчанию.<br /><h4>Отправка сообщения в ЖЖ</h4>Возвращаемся к функции <code>postToLj</code> и пишем, что если аутентификационный токе был успешно получен (<code>isSuccess r</code>), взять текущее время (<code>timeopts <- currentTimeOpts</code>, об этом ниже), подготовить запрос для публикациии сообщения (<code>let opts = postOpts ...</code>) и отправить. Результатом функции будет ответ на последний выполненный запрос: <pre> if (isSuccess r) <br /> then do<br /> let challenge = fromJust $ lookupLjKey "challenge" r<br /> timeopts <- currentTimeOpts<br /> let opts = postOpts ljuser ljpass challenge subj msg timeopts<br /> r <- do_curl_ curl ljFlatUrl opts :: IO CurlResponse<br /> return r<br /> else return r</pre>Как всегда в Хаскеле, если сказал <code>if — then</code>, говори и <code>else</code> (с тем же типом).<br /><br />Ещё одно «новичковое» замечание: в блоке <code>do</code> мы <em>связываем</em> переменные с монадным значением с помощью <code>(<-)</code> (это соответствует присваиванию в императивных языках), но <em>определяем</em> переменные чистыми выражениями с помощью <code>(=)</code>. Вообще, <code>(=)</code> в Хаскеле почти всегда можно читать как «равно по определению». Как только я это понял — жить стало проще ;-) <br /><br />Теперь подробности. Чтобы отправить сообщение, нужно сформировать POST-запрос согласно протоколу. В моём примере этим занимается функция <pre>postOpts u p c subj msg topts =<br /> CurlPostFields ("mode=postevent" : (authOpts u p c)<br /> ++ ["event=" ++ quoteOpt msg, "subject=" ++ quoteOpt subj,<br /> "lineendings=unix", "ver=1"]<br /> ++ topts ) : postFlags</pre>которая аналогичная <code>getChallengeOpts</code>, только список полей, которые нужно отослать, гораздо больше. И есть некоторые тонкости.<br /><br />Во-первых, нужно защищать («квотировать») некоторые символы в отсылаемых значениях. Их немного, на помощь приходит определение функции с помощью шаблонов аргумента: <pre>quoteOpt ('=':xs) = "%3d" ++ quoteOpt xs<br />quoteOpt ('&':xs) = "%26" ++ quoteOpt xs<br />quoteOpt (x:xs) = x : quoteOpt xs<br />quoteOpt [] = []</pre>Одно дело сделано. Во-вторых, нужно по имени пользователя, паролю и аутентификационному токену подготовить все поля запроса, касающиеся аутентификации: <pre>authOpts u p c = [ "user=" ++ quoteOpt u, "auth_method=challenge",<br /> "auth_challenge=" ++ quoteOpt c,<br /> "auth_response=" ++ quoteOpt (evalResponse c p) ]</pre>Собственно ответ на токен рассчитывается в одну строчку: <pre>evalResponse c p = smd5 ( c ++ (smd5 p) ) where smd5 = md5sum . fromString</pre>Кроме этого нужно импортировать соответствующие функции преобразования уникодной строки в байт-строку UTF-8 и функцию вычисления MD5-суммы: <pre>import Data.ByteString.UTF8 (fromString)<br />import Data.Digest.OpenSSL.MD5 (md5sum)</pre>И в-третьих, нужно заполнить в запросе поля, касающиеся времени публикации (текущего времени). Импортируем: <pre>import Data.Time<br />import System.Locale (defaultTimeLocale)</pre>Берём текущее время: <pre>currentTime = do<br /> t <- getCurrentTime<br /> tz <- getCurrentTimeZone<br /> return $ utcToLocalTime tz t</pre>Заметим, что функция эта связана с вводом-выводом и не является «чистой» (не возвращает одно и то же значение всякий раз). По этой причине я предпочёл не вызывать её из «чистой» <code>postOpts</code>, а передать уже готовый список опций, касающихся времени в <code>postOpts</code> из <code>postToLj</code>. Там, напомню, я писал: <pre>timeopts <- currentTimeOpts</pre>а <code>currentTimeOpts</code> определил так: <pre>currentTimeOpts :: IO [String]<br />currentTimeOpts = do<br /> t <- currentTime<br /> let opts = [ "year=%Y", "mon=%m", "day=%d", "hour=%H", "min=%M" ]<br /> return $ map (flip showTime t) opts</pre>Т.е. взял текущее время и подставил его в каждый из списка форматов (ЖЖ хочет в таков виде). Вспомогательная функция преобразования времени в строку по формату выглядит так: <pre>showTime = formatTime defaultTimeLocale</pre>Эта функция двух (неуказанных) аргументов получена <a href="http://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D1%80%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5">каррированием</a> функции <code><a href="http://hackage.haskell.org/packages/archive/time/1.1.2.2/doc/html/Data-Time-Format.html#v:formatTime">formatTime</a></code>. В <code>map</code> я меняю местами её аргументы (<code>flip</code>), чтобы формат передавался последним, и «перчу» ещё раз текущим временем.<br /><br />Всё, у нас уже есть всё необходимое для отправки любых сообщений в любой ЖЖ. Нужно только знать логин и пароль.<br /><h4>Чтение файла конфигурации</h4>Где-то логин и пароль хранить надо, и самое простое, что приходит в голову, поместить его в файле настроек, написанном в виде <pre>username=мойлогин<br />password=мойпароль</pre>В коде скрипта указываю путь по-умолчанию к этому файлу: <pre>ljPassFile = "~/.ljpass"</pre>Читаем этот файл и делаем из него знакомый и удобный ассоциативный список:<pre>readPassFile f = do<br /> ljpass <- readFile f<br /> return $ map (\(f,s) -> (f,tail s)) $ map (break (== '=')) $ lines ljpass</pre>Поскольку файл заведомо небольшой, можно использовать простую в обращении <code>readFile</code>. Далее как обычно: режем на строки (<code>lines</code>), каждую строку разбиваем по первому знаку «равно» (<code>map (break (== '='))</code>), правим получившийся ассоциативный список список, откидывая знаки «равно» (λ-функция во втором <code>map</code>). Результат заворачиваем в <code>IO</code>-монаду (<code>return</code>) как того требует тип функции.<br /><br />Почти готово. Для пущего удобства сделаем себе раскрытие тильды в пути к файлу: <pre>expandhome ('~':'/':p) = do h <- getHomeDirectory ; return (h ++ "/" ++ p)<br />expandhome p = return p</pre>и собственно функцию, которая, будет нам давать значение любого ключа из файла конфигурации: <pre>readLjSetting key = do<br /> passfile <- expandhome ljPassFile<br /> s <- readPassFile passfile<br /> return (lookup key s)</pre>В этот раз нам надо добавить ещё две декларации импорта: <pre>import IO<br />import System.Directory (getHomeDirectory)</pre>Последний штрих: в объявлении модуля перечисляем экспортируемые вовне функции, а вспомогательные замалчиваем: <pre>module LjPost (readLjSetting, postToLj, isSuccess, lookupLjKey, putLjKey) where</pre>Наш модуль готов к использованию. Он позволяет нам задавать настройки доступа в файле конфигурации, понимает ЖЖ-протокол, поддерживает challenge-response аутентификацию и позволяет публиковать в ЖЖ сообщения. Меньше 100 строк кода, если не считать комментарии.<br /><br /><h3 id="feed2lj">Обработка RSS/Atom фида (<code>Feed2Lj</code>)</h3>Переходим к заключительной части рассказа. Скрипт <a href="http://bitbucket.org/jetxee/feed2ljhs/src/tip/Feed2Lj.hs">Feed2Lj.hs</a> берёт URL фида из командной строки, настройки ЖЖ из файла с настройками (для него там добавляем третью настройку, имя файла со списком уже обработанных записей), скачивает фид и отсеивает уже обработанные, необработанные преобразует в plain-text, форматирует по образцу и отсылает в ЖЖ, обновляя список обработанных записей. Теперь подробно.<h4>Получение аргументов командной строки</h4>Получить список аргументов просто, его даёт функция <code>getArgs</code> из <code>System.Environment</code>. У нас аргумент один, адрес фида, поэтому может сразу связать нужную переменную (<code>url</code>) с первым элементом списка, проигнорировав остальные: <pre> url:_ <- getArgs</pre>Такое связывание по шаблону мне кажется очень элегантным приёмом.<br /><h4>Скачивание фида</h4>На помощь опять приходит библиотечка <code>curl</code>. И опять связывание по шаблону, чтобы взять только интересующую нас часть результата: <pre> (_,rawfeed) <- curlGetString url []</pre><h4>Используем модуль <code>LjPost</code> для чтения настроек</h4>В общем-то вся работа уже сделана, осталось только использовать функцию <code>readLjSetting</code>. У неё тип <code>[Char] -> IO (Maybe [Char])</code>, т.е. по строке она возвращает IO-монаду, внутри которой, может быть строка (значения настройки найдено и считано), а может и не быть (настройка не найдена). Поскольку у нас тут сразу две монады (<code>IO</code> и <code>Maybe</code>), одна в другой, то, чтобы вытащить просто (<code>Just</code>) значение, я поступаю так: <pre>ljuser <- return fromJust `ap` readLjSetting "username"</pre>т.е. функцию <code>fromJust</code> применяю внутри монады <code>IO</code> (<code>ap</code> из <code>Control.Monad</code>). Аналогично с остальными значениями из файла настроек. Кажется немного громоздно с непривычки, но не так уж сложно потом. Уверен, можно написать короче.<br /><h4>Чтение списка обработанных записей</h4>Мой старый bash-скрипт писал ID записей в файл, одно на строчку, поэтому новый скрипт использует тот же формат (и тот же файл). Читаем файл и преобразуем в список строк: <pre>sent_ids <- (return . lines) =<< readFile sentfile</pre>Здесь, чтобы не вводить временную переменную, я явно указал функцию связывания вычислений (<code>=<<</code>). <code>return</code> требуется типом <code>(=<<)</code>. Результат эквивалентен записи <pre>tmp <- readFile sentFile<br />let sent_ids = lines tmp</pre><h4>Отсеиваем обработанные записи</h4>Для начала разберём содержимое фида и подготовим список всех записей. Благодаря библиотечке <code>feed</code> это легко: <pre> let feed = fromJust $ parseFeedString rawfeed<br /> let items = feedItems feed </pre>Ну а отсеять уже обработанные можно с помощью <code>filter</code>: <pre> let newitems = reverse $ filter (isNotSent sent_ids) items</pre>Функция-предикат получилась за счёт каррирования <code>isNotSent</code>: <pre>isNotSent sent i = ((snd . fromJust . getItemId) i) `notElem` sent</pre>Буквально: взять просто ID элемента (возможна ошибка), проверить, что не входит в список <code>sent</code>. Сразу подготовим список ID подлежащих обработке записей: <pre>let new_ids = map ( snd . fromJust . getItemId) newitems</pre> <h4>Отправляем запись в ЖЖ</h4> Тупо используем уже написанный модуль <code>LjPost</code>. Если даны имя пользователя, пароль, шаблон записи для отправки и собственно запись: <pre>postItem u p t i = do<br /> let message = renderItem t i<br /> let subj = fromJust $ getItemTitle i<br /> r <- postToLj u p subj message<br /> if isSuccess r<br /> then putLjKey "url" r<br /> else putLjKey "errmsg" r</pre>Стоп-стоп-стоп! Какой ещё такой шаблон записи (<code>t</code>) и что делает <code>renderItem</code>? Объясняю: отослать запись нам надо в HTML-е, и хорошо бы можно было менять формат записи, не переделывая весь код. В общем, <code>renderItem</code> — это маленькая template engine, <code>t</code> — её шаблон. Я её опишу в следующих разделах статьи.<br /><br />Вызываем из <code>main</code> для каждой записи из списка необработанных: <pre> let t = encodeString "<p>%text%</p><p>( <a href=\"%link%\" title=\"%title%\">дальше</a> )</p>"<br /> mapM_ (postItem ljuser ljpass t) newitems</pre>Здесь мы формируем список IO-действий и их последовательно исполняем (<code>mapM_</code>). То есть последовательно отсылаем все записи из нашего списка. Обратим ещё внимание на <code>encodeString</code> из <code>Codec.Binary.UTF8.String</code>, которая кодирует строку в UTF-8.<br /><h4>Форматирование по шаблону (маленькая template engine)</h4>Напишем нашу маленькую функцию форматирования по шаблону. Пусть, допустим, все параметры шаблона будут представлены как «%параметр%», а спецсимвол «%» будет представлен в шаблоне как «%%». Параметры будет передавать ассоциативным списком, а шаблон — строчкой. На выходе — строчка с подставленными в шаблон параметрами: <pre>renderTemplate _ [] = []<br />renderTemplate alist s =<br /> let (b,t,a) = s =~ "%[a-z0-9]*%" :: (String,String,String)<br /> tagval t<br /> | t == "%%" = Just "%"<br /> | otherwise = let inner = take (length t - 2) $ drop 1 t<br /> in lookup inner alist<br /> val = tagval t<br /> in if isJust val<br /> then b ++ (fromJust val) ++ renderTemplate alist a<br /> else b ++ t ++ renderTemplate alist a</pre>Функция форматирования сообщения по шаблону готова. В ней мы последовательно «раскусываем» шаблон с помощью регулярных выражений на «текст-до», «тег» и «текст-после». Подставляем на место «тега» (<code>t</code>) значение соответствующего параметра, если есть, или буквальный «%», если тэг пустой. Продолжаем, пока не кончится шаблон.<br /><br />О регулярных выражениях. Включаем импортом <pre>import Text.Regex.Posix ((=~))</pre>После этого можем в любой строчке искать регулярное выражение: <pre>строка =~ выражение :: возвращаемый тип</pre>Регулярные выражения ведут себя по-разному в зависимости от возвращаемого типа. Мне пока что пригождаются больше всего два из них: <code>Bool</code> для проверки соотвествия строки выражению и тройной кортеж <code>(String,String,String)</code>, разрезающий строчку на три части.<br /><br />Функция форматирования по шаблону готова. Она просто работает со строками (шаблонами) и ассоциативными списками (словарями). А где же обещанная <code>renderItem</code>?<br /><h4>Форматируем запись по шаблону</h4>Итак, <code>renderItem</code> должна получать шаблон и запись из фида, а возвращать строчку. Всё, что делает эта функция — просто достаёт нужные параметры записи, помещает их в ассоциативный список и вызывает функцию форматирования по шаблону <code>renderTemplate</code>. В виде кода это выглядит гораздо понятнее: <pre>renderItem :: String -> Item -> String<br />renderItem t i =<br /> let title = ( fromJust . getItemTitle ) i<br /> link = ( fromJust . getItemLink ) i<br /> summary = ( takeSentences 5 . eatTags . fromJust . getItemSummary) i<br /> tags = zip [ "title","link","text" ]<br /> [ title, urlEncode link,summary ]<br /> in renderTemplate tags t</pre>Нетривиальна здесь только функция подготовки текста сообщения (<code>summary</code>).<br /><br />Поскольку я весь текст пересылать не хочу, а хочу только первые несколько предложений, то я вначале преобразую HTML в простой текст (в котором уже нет HTML-тэгов), а затем просто берую первые пять предложений. Таким образом, мне не нужно заботиться о предолжения будут гарантировано законченными.<br /><br />Функция <code>eatTags</code> использует тот же приём рекурсивного раскусывания строки с помощью регулярных выражений, что и <code>renderTemplate</code>: <pre>eatTags [] = []<br />eatTags s =<br /> let (b,t,a) = s =~ "</?[^>]*/?>" :: (String,String,String)<br /> in b ++ eatTags a</pre>Все HTML и XHTML теги должны быть этой функцией вырезаны.<br /><br />Упражнение: изменить функцию так, чтобы тег <code><img/></code> выразался не бесследно, а заменялся содержимым его аттрибута <code>alt</code>.<br /><br />Теперь осталось лишь взять первые <em>n</em> предложений. Возьмём вначале одно: <pre>takeSentence s = <br /> let ends = ".?!;"<br /> (first,rest) = break (`elem` ends) s<br /> in if not (null rest)<br /> then (first ++ [head rest],tail rest)<br /> else (first,[])</pre>Тут я обошёлся без регулярных выражений, просто задав список разделителей (<code>ends</code>) и раскусывая строку по символу из их числа (<code>break (`elem` ends)</code>). Напоследок присоединяю разделитель, если он есть, к «откушенному» предложению (<code>break</code> прикрепляет его к «остатку»).<br /><br />Осталось лишь взять первые <em>n</em> штук: <pre>takeSentences n s<br /> | n > 0 = let (s',r) = takeSentence s<br /> in s' ++ takeSentences (n-1) r<br /> | otherwise = ""</pre>Теперь любая запись может быть представлена так, как мы захотим.<br /><h4>Обновляем список обработанных записей</h4>Записи получены, отобраны, отформатированы, отправлены. Осталось только обновить список обработанных. Вначале сохраним предыдущую версию файла (переименованием), а потом запишем на его место новый список: <pre> renameFile sentfile (sentfile ++ "~")<br /> writeFile sentfile $ unlines (sent_ids ++ new_ids) </pre>Здесь использована функция <code>renameFile</code> из <code>System.Directory</code>.<br /><br /><h3 id="happyend">Заключение</h3>Вот вроде и всё. Можно вызывать получившийся скрипт: <pre class="sh_sh">$ runhaskell Feed2Lj.hs URL-вашего-фида</pre>Пробовал пока только с GHC, но, думаю, и с Hugs должно работать. Я, кстати, осознал, что у интерпретатора Hugs есть важное преимущество перед GHC: установка GHC тянет около 100 МБ, а Hugs — всего порядка 10 МБ. Так что как разберусь с Hugs, буду стараться проверять свои скрипты и на нём.<br /><br />В целом впечатления от опыта «написать на Хаскеле» очень положительные. Во-первых, очень приятно, когда удаётся написать полезную функцию в одну-две строчки. Во-вторых, интересно думать о программе <em>иначе</em>, писать более декларативно. В третьих, очень приятно, когда раз — и работает! (Ну это с любым языком). В четвёртых, мне нравится «математичный» синтаксис Хаскеля, он, по-моему, очень выразителен. Поначалу, пока не знакомо, конечно долго и непривычно, но когда входишь во вкус, получается быстрее и легче.<br /><br />Кроме, понятно, гугла, большой подмогой является Hoogle. Сообщения GHC довольно подробные и понятные (разбирать ошибки C++-компиляторов про шаблоны гораздо труднее). Радует, что уже сейчас коллекция библиотек весьма богата (кажется, сопоставима с набором библиотек Python в то время, когда я с ним впервые познакомился). С уникодом, опять же, никаких проблем.<br /><br />Есть и всякие «но»: но в коде других людей мне ещё далеко не всё понятно, но пихать ввод-вывод в любую точку кода в Хаскеле неудобно и не нужно (сделано намеренно, для отладки служит <code>trace</code> из <code>Debug.Trace</code>), но представить порядок ленивых вычислений не всегда легко, но документированы библиотеки в Hackage весьма лаконично (строго, по делу, но не так доходчиво и очевидно для новичков, как, например в Python), но <code>cabal</code> до сих пор нет ни в Debian, ни в Ubuntu.<br /><br />Но всё равно, мне понравилось. Буду рад замечаниям и вопросам. Уверен, что-то можно было написать лучше (короче, понятнее и выразительнее). Что-то, наверное, забыл объяснить.Unknownnoreply@blogger.comtag:blogger.com,1999:blog-37628976.post-73237308557785793402009-03-24T16:26:00.006+03:002009-03-24T17:20:06.088+03:00Статистика скачивания библиотек Haskell из HackageВчера, видимо, была впервые опубликована статистика скачивания библиотек Haskell с сайта <a href="http://hackage.haskell.org/packages/archive/pkg-list.html">Hackage</a>. Тем, кого интересуют детали, предлагаю ознакомиться с <a href="http://www.galois.com/blog/2009/03/23/one-million-haskell-downloads/" title="One Million Haskell Downloads…">оригиналом статьи</a>. Я лишь помещу одну картинку:<br /><br /><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZ0q9FQezs_f9xWTyFFnnP93LBFsGf2P1YRCnfmkGlbf2ka54hXnKqdlGYNzHu9tBJME9djWJF4FZVLSBc_dtY9O3q6fuCa9xqhyg35eJiYoJDLw-6fCBde4rS4Fzrh2PLIPAoRA/s400/hackage.png" /><br /><br />Учтём, что пользуются Hackage в основном разработчики (конечные пользователи устанавливают дистрибутивные пакеты библиотек). И получается, что количество программистов на Haskell в данный момент очень быстро растёт. Если взглянуть на график в логарифмической шкале (см. источник выше) — вполне себе экспоненциальный рост. Судя по цифрам на графике (порядка 10⁵ скачиваний исходников в месяц), оценить количество активных разработчиков и тех, кто ими скоро станет, можно, по-моему, как порядка 10³ или даже 10⁴.<br /><br /><blockquote><small>Для тех, кто ещё не начал разбираться с Haskell. Hackage — это такой централизованный репозиторий разных библиотек для Haskell. Репозиторий предназначен прежде всего для разработчиков и содержит исходники последних версий. Установка нужной библиотеки из Hackage обычно выглядит так: <pre>$ cabal update<br />$ cabal install название-библиотеки</pre>Дальше скачаются и скомпилируются все зависимости библиотеки, а библиотека будет установлена в <code>~/.cabal/</code>. Правда, похоже на <code>sudo aptitude update && sudo aptitude install пакет</code>?</small></blockquote><br /><br />В качестве побочного результата, в опубликованных данных есть рейтинг популярности некоторых библиотек и проектов. Тоже любопытно.<br /><br />Первоисточник: <a href="http://www.galois.com/blog/2009/03/23/one-million-haskell-downloads/" title="One Million Haskell Downloads…">One Million Haskell Downloads…</a><br /><br />PS. Как <a href="http://donsbot.wordpress.com/2009/03/16/visualising-the-haskell-universe/">известно</a>, для чего только Haskell уже не используется:<br /><br /><a href="http://donsbot.wordpress.com/2009/03/16/visualising-the-haskell-universe/" title="Haskell usage cloud"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtsYQSP7yj0V_rnCPNtCp2_4ah1gWA8cXlRxEIX0O0-OIaSBjLIJUBxuVBPBkm8fPG2nK_fNzUwvAwSpoM952UJ8xL2C3Dd4yILO_LSbVeWvjG3Kp2TOmcM6BcPjZ4I6jCElohBw/s400/haskellcloud.png" /></a>Unknownnoreply@blogger.com