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

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



Artem Chuprina <ran@lasgalen.net> writes:

> 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).

Ну и таки да, Хаскель не будет делать дубликат словаря, ибо эти "разницы
между исходной версией и данной" для него суть thunk-и, и это одно из
полезных следствий ленивости. Но тут мы вроде бы занимались выяснением
вопроса "почему иногда императив всё-таки нужен".

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

Ну, в принципе, создание EDSL -- это метод. Но это, как я понимаю, в
основном прерогатива Lisp-ов. Там они органичны более-менее. (Хотя это
смотря где ещё. Вот гигиенические макросы из Scheme на меня тоску
наводят). А как в Haskell с расширением синтаксиса?

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

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

Что ж, возможно, тут есть свои подходы. Вот об этом-то и надо бы писать
в книжках.

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

Поменять все? Вряд ли. Их может быть очень много. Тут ещё интересный
вопрос: куда добавлять новый аргумент? В конец, в середину, в начало?
Судя по всему в начало, иначе каррирование может Вас свести с ума (это
справедливо, например, если у вас где-то происходит выбор между
функциями с похожими, но слегка различными аргументами методом
каррирования до общей базы).

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

>  DK> Как Вы знаете, некоторое время тому назад я с Вашей подачи пытался
>  DK> работать на Haskell, и предпринял даже попытку написать дипломную работу
>  DK> с использованием этого языка.
>
>  DK> В то время язык оказался неподходящим. Я, конечно, разработал чисто
>  DK> функциональные алгоритмы, которые сильно упростили мне проектирование
>  DK> будущих версий программы, но я столкнулся вот с чем:
>
>  DK> Программа была научная, и её целью было исследование метода. Так вот:
>  DK> очень, очень сложно производить дебаг алгоритма, когда вычисления
>  DK> производятся ленивым образом. 
>
> А вот это место можно поподробнее?  Непонятно, откуда берется проблема.

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

Я решал задачу расчёта поля течения на некоторой сетке (короче,
эйлеровский подход к построению эволюции течения подразумевает, что мы
разбиваем профиль на ячейки, образуя "сетку". В каждой ячейке содержатся
значения параметров течения, и они по некоторым законам изменяются со
временем. Каждую итерацию по заданному полю вычисляется, какое поле
течения будет черед некоторое время tau. На tau, конечно, есть вполне
определённые ограничения сверху).

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

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

Я сумел разобраться, как поставить точки останова и выйти в repl ghci,
чтобы посмотреть состояние окружения в момент останова (посмотреть там
значения параметров ячейки, которую мы сейчас обрабатываем, и т.п.).

Но разумное желание после такого останова -- пройти выше по callstack и
посмотреть, в какую функцию что было передано. Я не сумел этого добиться
никак. Возможно, я не разобрался, конечно, но знаете, это было весьма
проблемно: сидишь на закрытом объекте, ни гугла, ни ирки, ни рассылки, в
которой Артём Чуприна может подсказать что-то дельное... :)

И тут ладно, что нету у меня callstack-а. Я мог бы и сам поставить
остановы в места, из которых данная функция вызывается,
но... Вай-вай-вай, а когда у меня эта функция вызывается? У меня же
добрых пять десятков законов, описывающих различные аспекты вычисления
поля, и я понятья не имею, какой в какой момент вызвается для получения
конечного результата. Победа, товарищи.

Концепцию монад я переварил уже сильно позже, после того, как решил
махнуть рукой на Haskell и переписал программу на SBCL. Кстати, вот тут
я бы отдельно подчеркнул, что лиспы для прототипирования --
великолепны. Именно что: в момент останова (намеренного ли, или при
ошибке), я вываливаюсь в отличный дебаггер, где могу проследовать вверх
по вызовам, посмотреть на все параметры, переданные в функцию. Если там
список, то пройтись по элементам, со всем разобраться.

Разумеется, отладка граничных случаев в этом случае весьма нетривиальна,
но до неё, в общем-то, в моей работе дела так и не дошло.

>  DK> Я уверен, что Haskell -- язык хороший, но в основном для алгоритмов, в
>  DK> которых Вы заранее можете быть уверены: либо достаточно простых (простых
>  DK> не значит коротких), либо строго доказанных математически.
>
>  DK> Промучившись с этим делом полгода, я отошёл от этой задачи в сторону
>  DK> Common Lisp'а, который хотя и динамически типизирован, сэкономил мне
>  DK> кучу времени макросами и приличным FFI-ем (как подружить с которым
>  DK> Haskell я понятья не имею, ибо опять же, ленивость).
>
> С FFI в хаскеле как раз довольно хорошо.  В норме оно в монаде IO,
> т.е. зависит от RealWorld, и будет выполняться последовательно и столько
> раз, сколько вызвали.  Но если есть уверенность, что вот этот вот вызов
> семантически чисто функциональный, есть unsafePerformIO, и можно
> сэкономить миллион-другой вызовов :)
>
> А так оно довольно простое и прямолинейное.

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

>  DK> Сейчас я работаю в основном на Racket Scheme, Emacs/Common Lisps и
>  DK> Ocaml. Последний выглядит для меня примерно как Haskell, только без
>  DK> ленивости. Синтаксис выразительный, но логика вычислений проще поддаётся
>  DK> осмыслению, и дебаг также не вызывает трудностей.
>
> Мозги разные?  Для меня логика вычислений на хаскеле на порядок (по
> основанию явно больше 2, но не 10) проще для осмысления.  Именно в силу
> функциональности.  Ведь императивный алгоритм - это инструкции для
> тупого, но очень неленивого исполнителя.

Эгегей, а кто говорит, что функциональные алгоритмы сложнее? )

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

Для меня лично основную проблему представляет именно ленивость. Я
понимаю, как писать программы с добавлением ленивости в определённых
местах, но у меня, хоть убейте, не получается делать наоборот.

> Тупого настолько, что связь "данный алгоритм решает данную задачу" ему
> недоступна.  Можно выполнить дословно, но что за задачу мы решили?  Да
> фиг ее знает...  Извлечь из императивного алгоритма задачу, которую он
> решает (или проверить, что он решает известную задачу, в императивном
> случае это тот же уровень сложности) требует от меня очень недетского
> напряжения.

Классический вариант "море императивщины без комментариев"?
Соболезную. :(

> Ленивость, правда, порой добавляет сложности в процесс понимания (если
> она в алгоритме задействована нетривиальным образом - если тривиальным,
> то разницы никакой).

Нетривиальным -- это каким?

> Но порой и убавляет.  Работа с рекурсивными конструкциями зачастую
> заметно упрощается...

Охотно верю!

********************

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

Самое важное в программировании -- это понятный процесс вычисления. При
использовании Haskell в силу ленивости он несколько тяжелее поддаётся
осмыслению.

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

Attachment: signature.asc
Description: PGP signature


Reply to: