Lisp, the Universe and Everything

2009-11-07

О страшных монадах

Всегда полезно поспорить, чтобы еще раз озаботиться своим пониманием темы. Поэтому я решил снова взглянуть на монады и попытаться ответить на вопрос, в чем их проблема (а также попутно понять, в чем от них польза).

Как верно было подмечено, монады — это очень просто. А еще монады — везде. Почему же тогда у многих с ними проблемы?

1. Терминология
Где-то написано, что использование нетипичной для предметной области терминологии — это аттрибут троллинга (к сожалению, не удалось найти ссылку). Можно возразить: "да ведь монады — это же математический термин". Да, но математика и инженерия (в частности, программирование) — разные дисциплины. Математики не строят мосты, да. А термин монада не относится к предметной области программирования. В ней есть такое понятие как "вычисление" (computation). (Пусть меня осудят, но) фактически, монада в Haskell — это стратегия вычисления.

Почему же тогда говорят "поместить/извлечь значение в/из монаду/ы"? Говорят так для простоты. На самом деле, можно лишь запустить определенное вычисление с определенным входным значением (отсюда и идиома run...). Слишком похоже на "запустить процедуру", не так ли?

Так что с термином "монада" либо создатели Haskell были недостаточно дальновидны, либо, просто, хотели эпатировать программистский мир.

2. Подход к изучению
К сожалению, большинство объяснений монад так или иначе отталкиваются от того, что монада — это всего навсего тип и 2 простые функции. Но проблема в том, что от того, что вы поймете, как работают функции return и bind, вам не станет сразу ясно, зачем они нужны вместе. Отталкиваться в понимании того, что такое монады стратегии вычисления, нужно от того, зачем они нужны? Сейчас я сделал именно так, и все легко стало на свои места.

Какие бывают стратегии вычисления? Я сейчас не буду называть самую важную из них, а напишу о ней в следующей записи — пока предлагаю прийти к ответу самостоятельно. Могут быть: вычисление, которое завершается неудачно, если неудачно завершается хотя бы одно из внутренних подвычислений — это знаменитая монада Maybe, которую мы сейчас рассмотрим. Может быть вычисление, которое использует какое-то доступное для всех подвычислений "хранилище" данных — State. Есть вычисление, которое связанно с обменом данными с внешней средой — IO и т.д.

Бытует мнение, что Haskell — ленивый язык (более того, это официальная позиция). На самом деле, все немного сложнее или проще, зависит от того, как посмотреть. Во-первых, всем понятно, что абсолютно (или, как говорится, чисто) ленивого языка быть не может, поскольку в таком случае мы никогда не получим какого-то результата работы программы. Каким образом Haskell'исты выходят из этого затруднения? Принято говорить, что, в целом, язык ленивый, а вся неленивость/энергичность (eagerness) находится (так и хочется сказать, заточЕна) внутри монад. Но дело в том, что любая Haskell-программа находится внутри монады IO. В итоге оказывается, что ленивыми в Haskell являются только чистые функции, а все вычисления (в понимании программистском), которых, на самом деле, большая часть — все равно остаются энергичными. В то же время любая чистая Haskell-функция, учитывая pattern matching и немутируемые переменные, по большому счету представляют из себя одну case-конструкцию и вызов других функций. Красиво? Часто, да. Однобоко? Тоже. ОК, но самое интересное дальше.

И в других языках примерно то же самое! По сути, разница только в том, что:
(а) не проведена четкая граница между ленивыми вычислениями и энергичными. Впрочем, каждый может для себя ее проводить сам: например, можно думать о ленивых вычислениях как об аттрибутах "математических" функций (и таким образом их и программировать), а об энергичных — как о процедурах. В любом языке может быть реализована ленивая стратегия вычислений, однако не везде это сделать одинаково легко...
(б) программист самостоятельно может строить процесс вычисления адекватный конкретной задаче. По сути, построение вычислительных процессов и есть одно из главных составляющих собственно того, что мы понимаем под программированием
Можно выдвинуть предположение, что все разные виды вычислений можно свести к нескольким десяткам определенному количеству монад. И, изучив их, можно будет составлять вычисления, как кубики в конструкторе. (И это предположение действительно выдвигается). В таком случае монады и вправду становились бы решающим методом абстракции в программировании. Впрочем, оказывается, что даже сведя вычисления к монадам, составлять их также легко, как кубики, не удается, и приходится прибегнуть к помощи еще одних монад — монадных преобразователей (monad transformers). В итоге, монада на монаде сидит и монадой погоняет... :) (Можно было догадаться исходя из принципа вычислительной эквивалентности. Вообще говоря, приняв во внимание этот принцип, можно понять, что максима "there is no silver bullet" таки справедлива всегда).
И еще одно: вывод про сведение всех вычислений к определенному набору не нов. Он лежит в основе структурного программирования, где доказано, что все вычисления можно свести к комбинации последовательного выполнения, условного выражения и цикла.

Вот тут то разница между Haskell'ем и другими языками становится наиболее существенной: другие языки имеют синтаксическую поддержку определенного узкого набора монад, и дают возможность создавать другие монады ad hoc с использованием того или иного механизма. Haskell, по сути, делает то же самое, только создание новых монад регуляризирует и вгоняет в определенные рамки. Итак Haskell — это язык, в котором дисциплина на первом месте.

В общем, концепция монад очень полезна, чтобы понять, что могут быть совершенно любые стратегии вычислений и не стоит считать, что возможны только те, которые определенный язык считает родными (вот, в С не было стратегии вычисления Exception, а в С++ появилась :)

Возвращаясь к Maybe. Его все понимают, так ведь? Это нужно для того, чтобы если где-то в последовательности вычислений у нас получился элемент Null (Nothing), это значение в итоге вернулось в качестве результата, и при этом другие части вычисления не имели проблем с обработкой такого значения.

Этот шаблон хорошо знаком всем, кому доводилось встречаться с логическими операторами с коротким замыканием. Идеально он работает в Common Lisp, где значение nil также является логической ложью (это, кстати, считается многими одним из кардинальных преимуществ Common Lisp над Scheme, в котором эти 2 значения разделены). Ниже приводится пример реализации Maybe на CL, аналогичный примеру из пособия по монадам:

(defmacro maybecall (val &rest funs)
"Consequently apple a sequence of FUNctionS to the
return value of the previous computation, starting with VAL"
`(and-it ,val
,@(mapcar (lambda (fun)
`(funcall ,fun it))
funs)))

;; где:
(defmacro and-it (&rest args)
"Like AND, but IT is bound to the value of the previous computation"
(cond ((null args) t)
((null (tail args)) (first args))
(t `(let ((it (first ,args)))
(when it
(and-it ,@(tail args)))))))

;; работает?

(defun mother (x)
(gethash x *mothers*))

(defun father (x)
(gethash x *fathers*))

(setf (gethash :b *fathers*) :e
(gethash :a *fathers*) :d
(gethash :b *mothers*) :c
(gethash :a *mothers*) :b)

(maybecall :a #'mother #'father) => :e
(maybecall :a #'mother #'father #'father #'brother) => nil


Впрочем, я понял, что обычный AND (или AND-IT) в CL — это и есть полноценная реализация Maybe стратегии. Притом простая и понятная, нет?

3. Нужны ли монады за пределами Haskell'я?
То, что в других языках представляет из себя единую ткань вычислений, в рамках Haskell разделено на 2 класса: чистые (обычные функции) и "грязные" (монады). Это приводит к разделению (дублированию) синтаксиса и усложнению программного кода (полная противоположность дуалистического подхода). В то же время, это делает рассуждения о программах более структурированными и помогает (однако не гарантирует!) соблюдению принципа ссылочной целостности. В теории концепция монад полезна для понимания того, что такое вычисления. На практике же введение дополнительных правил приводит к тому, что появляется определенный предел выразительности языка и близости его к предметной области. Именно так, несмотря на заявления программистов на Haskell об обратном: концепция монады, монадного трансформера и т.д. неимоверно далеки от любой предметной области, которая встречается при программировании, а абстрагировать их средствами Haskell не удастся. Более того, само понятие вычисления, которое в других языках остается имплицитным, в Haskell'е достается на поверхность. Именно с этим, по-моему, связанны частые претензии к преувеличенной сложности Haskell программ.

---

В следующей записи я дам ответ на вопрос про самую главную монаду, а также попробую разобрать по-отдельности несколько других монад.

Еще по теме:
* All About Monads
* Why monads have not taken the Common Lisp world by storm

2009-11-02

О Haskell

Обновление: обсуждение на habrahabr.ru Изучай Haskell ради... Haskell'а

Я долго (несколько лет) не решался составить окончательное мнение о Haskell'e: слишком противоречивы были мысли. И вот, наконец, благодаря этой записи о разборе программки определения двудольности графа я могу это сделать :)

Я понял, что Haskell-программисты — в основном, нужно сказать, хобби-программисты — это те, кто программирует не решение задачи, алгоритм, систему, а Haskell! [1] Посмотрите, какой простой алгоритм описан в заметке, а сколько вокруг него нагромождено языковых конструкций, объяснений и дискуссий. Чтоб доказать, что он очень простой, привожу пример кода на Lisp'е, который решает ту же задачу без никаких монадных трансформеров и т.п. (не обращайте внимание на большой docstring):
(defun test-bipartite (graph)
"Test for bipartiteness a GRAPH, that is given as a list of pairs
vertix - list of immediately connected vertices, like:

'((0 . (1)) (1 . (0 2 8))
(2 . (1 3)) (3 . (2 6))
(4 . (5 7)) (5 . (4))
(6 . (3)) (7 . (4 8))
(8 . (1 7)))
-- bipartite one

'((0 . (1 2)) (1 . (0 2 8))
(2 . (1 3)) (3 . (2 6))
(4 . (5 7)) (5 . (4))
(6 . (3)) (7 . (4 8))
(8 . (1 7)))
-- not bipartite one

'((0 . (1 2)) (1 . (0 2 8))
(2 . (1 3)) (3 . (2 6))
(4 . (5 7)) (5 . (4))
(6 . (3)) (7 . (4 8))
(8 . (1 7)) (9 . ()))
-- not connected one"

(let ((map (make-hash-table))
(visited '()))

(labels ((con1 (v)
"vertices immediately connected to V"
(tail (assoc v graph)))

(paint (v level)
"paint the graph from vertix V, return all
visited so far (in subsequent calls of PAINT) vertices"
(pushnew v visited)
(setf (gethash v map) level)
(mapc (lambda (v) (paint v (1+ level)))
(remove-if (lambda (v) (member v visited))
(con1 v)))
visited))

;; first we paint and check, that all vertices are visited
(unless (set-exclusive-or (paint 1 0)
(mapcar #'first graph))
(every (lambda (entry)
(every (lambda (v)
(oddp (+ (gethash (first entry) map)
(gethash v map))))
(tail entry)))
graph)))))


Мне хорошо знакомо это умонастроение — когда в погоне за максимальным использованием мощи языка забываешь о самой задаче,— поскольку в Lisp-мире оно тоже часто встречается: есть языки, которые способны действительно увлечь. И выражение "это взорвало мне мозг" часто звучат и по поводу Lisp'а, и по поводу Haskell'а. Но это же — фигня! Конечно, не может не радовать узнать что-то новое, но не нужно же радоваться этому, как ребенок новой игрушке. Хороший язык программирования должен быть максимально понятен и прост, должен давать человеку свободу самовыражения. Честно говоря, именно этому я обрадовался, когда открыл для себя Lisp: что нашел то, что искал. А не тому, что увидел какую-то конструкцию или изворот, который не доводилось встречать раньше.

Так что же, вывод: всем программировать на Lisp? Я, конечно, за, но вывод тут другой: Haskell — очень интересный язык, у которого есть как плюсы, так и минусы. Плюсы: это интересная семантика и сильная теоретическая база, хорошая скорость выполнения современных компиляторов. Минусы: ужасно нерегулярный синтаксис [2], искусственная ограниченность, которая приводит к необходимости задействовать сложные подоходы там, где отлично справятся и простые. И им просто обязательно стоит заниматься, если вас интересует тема языков программирования как таковых, их развития и исследований. Из Haskell берут многое другие более практичные языки: яркий пример тому Clojure. Но он не для написания больших систем и даже не для исследования алгоритмов в общем случае. У языков программирования кроме синтаксиса и семантики есть еще третий аспект, пожалуй даже важнейший, о котором часто забывают — прагматика. То, как язык используется, для чего он предназначается, чем живет сообщество его разработчиков и пользователей. Прагматика Haskell'а заключается в том, что он существует прежде всго для исследования... Haskell'а.

[1] Есть, конечно, исключительные, прекраснейшие Haskell-программисты, написавшие на нем много полезного кода для реального мира, но это, как говорится в нелюбимом мной афоризме, только подтверждает правило.

[2] Для современного языка нерегулярный синтаксис — это неуважение к своим пользователям. Ведь никто в современном мультиязыковом мире не программирует на одном языке, поэтому нельзя требовать от человека держать в голове идиосинкразии каждого. И этих общеупотребимых языков будет все больше и больше, а количество legacy кода уменьшаться не будет. Я сейчас имею дело с Lisp, Python, Php, C, JavaScript, Shell, Java. И это ведь не самый яркий пример.

2009-08-03

Очередная встреча по Lisp

Первая встреча по Lisp'у, я считаю, удалась.
Попробуем еще раз: http://www.developers.org.ua/calendar/event/f2hv5cnvjg2uj3qqtk43r2fv7s/.
Тема: кодогенерация.

2009-06-12

О микроплатежах

Написано для: Лига.Блоги

В своем предыдущем комментарии я затронул тему того, что до сих пор ни в Украине, ни в мире не появился удобный и получивший широкое распространение сервис микроплатежей. Вообще, под микроплатежами можно понимать разные вещи. Я имею в виду платежи, которые незначительны настолько, что человеку проще перелатить даже до 10-20% от их суммы, зато произвести их быстро и без лишних забот. Понятно, что для каждого это разные суммы, но в среднем это платежи, комиссия по которым не превышает пары десятков гривен (для нашей страны в данное время). В общем, современная экономика говорит, что главная проблема — это транзакционные издержки и их нужно стараться минимизировать. Эти издержки складываются из финансовых, временных, а также усилий и рисков. Микроплатежи находятся в той части спектра экономической активности, где увеличение финансовых издержек наименее нежелательное из перечисленных пунктов.

Еще раз, зачем нужны микроплатежи?

* Для оплаты товара в Интернет-магазинах. Иногда да, хотя в данном случае, поскольку имеет место акт передачи товара из рук в руки, можно применять оплату наличными (как, собственно, у нас в основном и делаеют). К сожалению, как в анекдоте, что заказывать товары при коммунизме можно будет по телефону, а получать по телевизору — у нас пока не получается. Микроплатежи отлично могли бы применяться при доставке товаров по почте, но таких товаров у нас, традиционно, не много.
* Для переводов небольших сумм между людьми. Однозначно, это намного проще, чем банковские или мгновенные переводы. И они в этой сфере уже применяются, но пока по-серому.
* Для оплаты различных услуг: коммунальных, виртуальных и некоторых реальных. Это основное применение для микроплатежей. На самом деле, как раз отсутствие такой полноценной возможности тормозит развитие очень большого числа мелких услуг (в первую очередь виртуальных).

На мой взгляд, единственный путь появления полноценных микроплатежей — через операторов мобильной связи. По сути дела, сейчас они предоставляют такую возможность, но только в ограниченной форме: пополнение счета другого человка (*125*...) И этой возможностью некоторые уже сейчас пользуются даже для организации оплаты мелих услуг. Это типичные мироплатежи, в которых комиссия составляет до 25% (и не смотря на такой высокий процент, люди пользуеются этой услугой).
В чем ограниченность? Главное, нельзя использовать переведенные деньги для оплаты чего-то иного, кроме услуг самого оператора и его афиллированных компаний, нельзя, что называется, "вывести деньги из системы". Второй, менее значительный недостаток: сейчас суммы платежей должны быть кратны 10 (а ведь нет никаких проблем снизить кратность до 1).

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

Так почему же этого не делается? Загвоздки:

* Юридические. При этом оператор становится банком? Не совсем. Всего лишь расчетной системой. Скорее всего подобная деятельность требует лицензирования и согласования. Однако операторам и так приходится заниматься подобными вещами постоянно. Использование системы для ухода от налогов? Во-первых, не больше, чем при расчете наличными :) Во-вторых, есть пути мимимизации этого: например, обязательная регистрация для мерчантов (тех, кто хочет использовать систему для получения оплаты за товары или услуги). А так ведь, всё прозрачно — наоборот удобно для налоговой.
* Издержки при вводе денег в систему: условно говоря, человек покупает карточку предоплаты на 30 гривен, выпуск и распространение которой стоит 1 грн. Эта гривна — это 3%, которые не сможет компенсировать комиссия за вывод денег из системы (которая тут должна быть не больше тех же 2-3%). Но ее можно компенсировать введя комиссию на каждый платеж: ту же гривну или даже 50 копеек.
* Мошенничество. Для микроплатежей этот вопрос стоит на последнем месте. Ведь никого не смущают возможности мошенничества при пополнении чужого мобильного счета: такая функция намного полезнее, так ведь?
* Организационные. Безусловно, построение подобной системы — это серьезный проект. Однако, он менее серьезен, чем построение самой сети оператора, с чем они успешно справляется.

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

А в перспективе подобная система может стать вообще глобальной и составить конкуренцию таким фирмам как Visa и MasterCard.

2009-06-02

k-nucleotide benchmark

Read the discussion on k-nucleotide benchmark in debian shootout (http://groups.google.com/group/comp.lang.lisp/msg/d50c86053c9000b7) and decided to play with it a little: to use as hash-table keys numbers in base 4 instead of gene strings. That gave a speed gain of around 30%: http://shootout.alioth.debian.org/u32q/benchmark.php?test=knucleotide&lang=sbcl&id=3.
But still 12 times slower, than the C++ variant. Perhaps native strings (instead of full unicode) would have given 2-2.5 times additional speedup. But what else can be improved?

k-nucleotide бенчмарка

Почитал дискуссию о k-nucleotide бенчмарке в debian shootout'е (в c.l.l.) и решил немного с ней поиграться: вместо строк генов в качестве ключей хеш-таблицы использовать соответствующие числа в базе 4. В результате удалось улучшить время выполнения примерно на 30%: http://shootout.alioth.debian.org/u32q/benchmark.php?test=knucleotide&lang=sbcl&id=3.
Но все равно медленнее С++ аналога в 12 раз. Допустим, если отнять unicode-строки, то получится ускорение еще в 2-2,5 раза. А что еще можно улучшить?

2009-05-27

Идеи по поводу tedxkyiv

@tagline
У оригинального TED обычно есть какие-то большие темы (по 3-4 на конференцию), например: "зеленое будущее", "переосмысление бедности", "сознание" и т.д. Наверно, было бы неплохо, чтобы и у TEDx тоже была какая-то объединяющая тема. И посколько в последнее время Украина как-то не дает таланты, являющиеся глобальными лидерами мысли (у нас сейчас хорошо получаются только футболисты и боксеры), то и тему можно выбрать только локальую, украинскую. Мне сразу приходит на ум "Как нам модернизировать Украину?"

@speakers
Ну и, в соотвествии с темой должны подбираться докладчики. Я так понимаю, что формула TED: известная личность + видение. В этом плане у нас тоже не густо: во всяком случае трудно выделить среди известных политиков, бизнесменов, ученых или гиков тех, у кого действительно есть видение, которое бы было шире их личных интересов и касалось хотя бы всей Украины, не говоря уже о мире в целом.

Единственный хороший пример, который мне приходит на ум -- это Татьяна Монтян. Она известный человек с вполне конкретными достижениями (была адвокатом в нескольких резонансных делах, создала блог "Инфопорн", признанный лучшим коллективным блогом Украины на BlogCampCEE'08), и у нее есть видение: от коррупции нас спасет введение всеобщих открытых реестров недвижимого имущества и переход к титульной системе учета прав на него.

Другие известные люди, теоретически могущие иметь видение, которые приходят на ум:
* Вячеслав Брюховецкий, бывший ректор Киево-могилянской академии
* Михаил Згуровский, ректор КПИ
* Михайло Свистович (создатель сайта maidan.org.ua и один из инициаторов Гражданской кампании "Пора") и его жена Мирослава, бывшая мером Ирпеня
* из бизнесменов можно упомянуть разве что Романа Хмиля, директора компании "GlobalLogic"

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

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