[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Re: Стабильная система?



Dmitrii Kashin -> debian-russian@lists.debian.org  @ Thu, 15 Oct 2015 21:49:30 +0300:

 >>  DK> За время работы с ним меня приятно удивило после Haskell:
 >>
 >>  DK> 1) Позволяет более просто комбинировать функциональное и императивное
 >>  DK> программирование: не надо изворачиваться монадами, чтобы добиться
 >>  DK> последовательного выполнения команд.
 >>
 >> ...
 >>
 >> а зачем, собственно, добиваться последовательного выполнения никак не
 >> связанных между собой команд?

 DK> Я думаю, тут есть недоразумение. Требование последовательного выполнения
 DK> определённых команд как раз и определяет связь между ними. Если конечно
 DK> не считать, что связь -- это "результат А нужен для вычисления B". Тут
 DK> важно понять, что императивное программирование -- это именно
 DK> программирование с изменением состояния системы.

Тогда это, натурально, связанные между собой команды.  Для вычисления B
требуется состояние системы, полученное после вычисления A.

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

 DK> Зачем это нужно? Ну, бывают разные случаи.

 DK> Бывает, что это повышает производительность. В случае большого словаря
 DK> создание его дубликата при изменении значения одного ключа создаёт очень
 DK> большие накладные расходы.

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

Хаскель, кстати, будет делать дубликат не всего словаря, а только той
ветки, в которой поменяли значение.  Для, допустим, Map это O(log n).

 DK> Бывает, что это упрощает описание алгоритма. Например, если Вы
 DK> реализуете программу, алгоритм которой описан в императивном стиле в
 DK> некоторой статье, то логично пользоваться тем же представлением, что и
 DK> автор.

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

 DK> Бывает, что без этого обойтись невозможно. Такое случается, когда Ваша
 DK> программа должна некоторым образом взаимодействовать со внешней
 DK> средой. Вот пишете Вы, допустим, сборочную систему, и Вам принципиально
 DK> важно, чтобы были выполнены последовательно сначала git clone, а потом
 DK> git checkout.

Это система с изменением состояния - "не склонировано", "склонировано",
"извлечено".  git checkout можно делать из состояния "склонировано", а
откуда и когда оно взялось, в этом месте неважно.  А берется оно из
завершения git clone (это состояние RealWorld)...

Система зависимостей тут как раз может гарантировать консистентность
вытащенного, потому что, если копать дальше, для выполнения git clone
надо, чтобы место было чистое - и вызывается команда приведения места в
чистое состояние...

 >>  DK> 2) Позволяет таки определять полиморфные функции. Да, это нарушение
 >>  DK> системы типов, но если этим не злоупотреблять, то это даже удобно.
 >>
 >> Какого именно вида полиморфных функций?  В хаскеле их минимум два (а с
 >> generics, пожалуй, и все три).

 DK> Прошу прощения, ошибся. Я уже начал подзабывать Haskell. Да, в Haskell
 DK> есть полиморфные функции.

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

 DK> Мне вот достался в наследство проект, который в виду крайне сумбурного
 DK> развития имеет ужасную архитектуру, и работает с горем
 DK> пополам. Благодаря возможности описания опциональных аргументов у
 DK> функций, я могу дополнять их функционалом, не меняя при этом их
 DK> поведения в старом коде.

 DK> По поводу каррирования, то вот к примеру можно определить функцию со
 DK> двумя опциональными аргументами:

 DK> let funA ?a ?(b=1) c = match a with
 DK>   | None -> b*c;
 DK>   | Some a -> a+b*c

 DK> И после этого каррировать её вот так:\

 DK> let funB = funA ~a:None ~b:2

 DK> При этом "funB 1" будет эквивалентна "funA ~b:2 1"

Я иногда тоже хочу параметров по умолчанию, но сильно подозреваю, что
это пережиток языков с duck typing.  На практике, учитывая, что язык
компилируемый, всегда можно либо добавить пару параметров и поменять все
(найденные компилятором) вхождения вызовов, добавив туда дефолтные
значения, либо, что чаще, одновременно со вводом дополнительных
параметров переименовать функцию, а старое имя определить как
каррирование нового.

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

 DK> Вот тут ещё можно посмотреть очень интересное мнение:
 DK> http://blog.ezyang.com/2010/10/ocaml-for-haskellers/

 >>  DK> Я в своё время с Вашей подачи пытался взять Haskell наскоком. Увы, он
 >>  DK> меня расстроил по целому ряду причин. Если хотите, могу рассказать.
 >>
 >> Было бы интересно.  Я его и сам осваивал небыстро, и кое-что открываю в
 >> нем до сих пор, пятый год уже.  (Кое-что из этого, впрочем, и
 >> появилось-то в этом году.)

 DK> Если Вы не возражаете, я сегодня на работе сделал некоторую заготовку
 DK> для ответа Вам, но адаптировать её и выверять у меня уже нет сил. Потому
 DK> помещаю как есть.

 DK> ********

 DK> Как Вы знаете, некоторое время тому назад я с Вашей подачи пытался
 DK> работать на Haskell, и предпринял даже попытку написать дипломную работу
 DK> с использованием этого языка.

 DK> В то время язык оказался неподходящим. Я, конечно, разработал чисто
 DK> функциональные алгоритмы, которые сильно упростили мне проектирование
 DK> будущих версий программы, но я столкнулся вот с чем:

 DK> Программа была научная, и её целью было исследование метода. Так вот:
 DK> очень, очень сложно производить дебаг алгоритма, когда вычисления
 DK> производятся ленивым образом. 

А вот это место можно поподробнее?  Непонятно, откуда берется проблема.

 DK> Я уверен, что Haskell -- язык хороший, но в основном для алгоритмов, в
 DK> которых Вы заранее можете быть уверены: либо достаточно простых (простых
 DK> не значит коротких), либо строго доказанных математически.

 DK> Промучившись с этим делом полгода, я отошёл от этой задачи в сторону
 DK> Common Lisp'а, который хотя и динамически типизирован, сэкономил мне
 DK> кучу времени макросами и приличным FFI-ем (как подружить с которым
 DK> Haskell я понятья не имею, ибо опять же, ленивость).

С FFI в хаскеле как раз довольно хорошо.  В норме оно в монаде IO,
т.е. зависит от RealWorld, и будет выполняться последовательно и столько
раз, сколько вызвали.  Но если есть уверенность, что вот этот вот вызов
семантически чисто функциональный, есть unsafePerformIO, и можно
сэкономить миллион-другой вызовов :)

А так оно довольно простое и прямолинейное.

 DK> Сейчас я работаю в основном на Racket Scheme, Emacs/Common Lisps и
 DK> Ocaml. Последний выглядит для меня примерно как Haskell, только без
 DK> ленивости. Синтаксис выразительный, но логика вычислений проще поддаётся
 DK> осмыслению, и дебаг также не вызывает трудностей.

Мозги разные?  Для меня логика вычислений на хаскеле на порядок (по
основанию явно больше 2, но не 10) проще для осмысления.  Именно в силу
функциональности.  Ведь императивный алгоритм - это инструкции для
тупого, но очень неленивого исполнителя.  Тупого настолько, что связь
"данный алгоритм решает данную задачу" ему недоступна.  Можно выполнить
дословно, но что за задачу мы решили?  Да фиг ее знает...  Извлечь из
императивного алгоритма задачу, которую он решает (или проверить, что он
решает известную задачу, в императивном случае это тот же уровень
сложности) требует от меня очень недетского напряжения.  Нет, я умею это
довольно хорошо, и очень часто нахожу ошибки в чужом коде методом его
чтения.  Но очень напряжно.  При функциональном выражении гораздо проще.

Ленивость, правда, порой добавляет сложности в процесс понимания (если
она в алгоритме задействована нетривиальным образом - если тривиальным,
то разницы никакой).  Но порой и убавляет.  Работа с рекурсивными
конструкциями зачастую заметно упрощается, если задействуется принцип
"дальше неинтересно, можно не вычислять".  Соответственно, меньше кода,
легче понять.

 >> Я Вам открою, наверное, страшную тайну: в хаскеле вообще невозможно
 >> добиться последовательного выполнения команд, кроме как через
 >> зависимость "результат A нужен для вычисления B".  В монадах на сей
 >> предмет нет ничего волшебного.  Даже в монаде IO.  Я когда-то, посмотрев
 >> на ее реализацию и осознав, что она такое, ухитрился даже нарисовать
 >> тест, который это демонстрировал.

 DK> Ну, вот видите. Принудительная ленивость по умолчанию порождает ряд
 DK> очень не очевидных проблем, а плюсов её навязывания я не ощущаю.

Ну, для желающих есть force.  Ленивость в хаскеле не столько
принудительная, сколько умолчательная.  Собственно, на проблему с
ленивостью я натыкался лишь однажды: когда в передаваемом состоянии
(время жизни состояния - время жизни всей программы) накапливались
невычисленные за невостребованностью thunk'и (обещания посчитать эту
штуку, когда и если она понадобится).  В результате текла память.  Ну,
попросил вычислять его до конца раз в итерацию.

 DK> Я не утверждаю, что они не решаемы. Возможно, я просто не смог до конца
 DK> разобраться. Но рекомендованного Вами "Learn Haskell for Great Good" мне
 DK> лично не хватило, чтобы уверенно работать.


Reply to: