четверг, 1 октября 2009 г.

Что я думаю о ФП

Наблюдая очередной срач на LOR-е, посвящённый выходу второго номера журнала "Практика функционального программирования" не перестаю удивляться. Дело дошло до обсуждения идиотского примера приготовления яичницы: как лучше описать решение? В императивном или функциональном стиле? Или может ещё надо добавить немного ООП?

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

В начале SICP приводится мнение, что
Программы пишутся, чтобы люди их читали, и лишь иногда - чтобы машины их выполняли
- я думаю, что оно немного устарело. Программы пишутся для того, чтобы люди их редактировали. Если бы программы писались лишь один раз, то, вероятно, стоило бы рассуждать о преимуществах функциональной или императивной парадигмы. Но если вы считаете, что разработка это, прежде всего, процесс экспериментирования и переписывания (рекомендую яркое эссе Пола Грэма на эту тему: "Хакеры и художники"), то проблематика выбора парадигмы отходит на второй план, гораздо важнее другие вещи: насколько легко вы можете внести в программу изменения и увидеть их результат? или насколько легко вы можете диагностировать ошибку?

Common Lisp удивительно хорош не только тем, что сочетает в себе различные парадигмы, что даёт возможность осуществить смену парадигмы для конкретного куска кода без смены языка, но и тем, что значительная часть языка посвящена облегчению процесса программирования (хакерства - в терминологии Пола Грэма), не облегчения кодирования известного алгоритма (хотя и в этом с ним мало кто может соперничать), но прежде всего поддержке процесса поиска решения. И благодаря именно этим возможностям возможен такой мощный инструмент, как SLIME. Пара свежих примеров из собственной практики:
  • Порой хочется пропатчить какую-нибудь библиотеку, что бы изменить поведение какой-либо функции. Просто для проверки, как изменится поведение моего кода. Либо порой может оказаться полезным добавить в библиотечную функцию диагностическое сообщение. При использование многих других языков это довольно проблематично, надо сделать очень много действий и обычно, без крайней необходимости, лучше такого не делать. В Common Lisp при использование SLIME это настолько тривиально, что я пользуюсь этим приёмом постоянно, надо то: установить курсор в месте вызова функции, нажать C-M-. - в результате откроется код нужной функции, с помощью C-x C-q переключить флаг read-only (это даст возможность редактировать буфер, но не сохранить на диск), исправить код и теперь С-M-x перекомпилирует изменённую функцию, всё, можно смотреть как изменилось поведение программы

  • Сайт lisper.ru иногда зависает. Это случается достаточно редко и у меня до сих не было возможности диагностировать ошибку. Но вот вчера сайт снова завис, а я имел немного свободного времени, что бы разобраться. Выяснение точного места в коде, приводящего к этому заняло менее 5 минут: с помощью SLIME подключился к серверу (который находится в другой стране), получил список потоков, выбрал поток "Hunchentoot listener" и выполнил команду slime-thread-debug, что привело к выводу стека вызовов зависшего потока, я увидел необработанное исключение и в течении минуты установил точное место в коде hunchentoot, где оно возникает и не получает должной обработки (надо заметить, это довольно редкая проблема, по крайней мере, я сталкиваюсь с ней только на одном сервере и то, иногда).
Может быть программы на ФП-языках действительно занимают меньшее количество строк кода и их проще понимать (при должной сноровке), чем программы на иных языках, хотя мне совершенно не понятно на чём основано подобное мнение (если проводить сравнение ФП-языков с таким языками как Common Lisp или Python; понятно, что трудно выглядеть проигрышно по сравнение с С). Но какое это имеет значения? Если важна на самом деле не красота/лаконичность законченного решения, а сложности пути, которым оно было достигнуто... ИМХО, сторонники ФП слишком большой упор делают на фукнциональную парадигму, на рассказы о том, насколько хорош язык для кодирования заранее известных алгоритмов (может быть это и так, но мне не очень это интересно), но вопрос применимости языков для реальной разработки остаётся соверешенно открытым. Можно ли пользоваться ФП без боли?

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

21 комментарий:

  1. Согласен на все сто.

    Есть же куча алгоритмов/задач где ФП трудно приложить, например сортировка "на месте".

    Мне Haskell тоже кажется каким-то насилием над мозгом и я предпочту ему OCaml.

    ОтветитьУдалить
  2. Хаскель заставляет думать над задачей, потому что в нём для впихивания состояния и ввода-вывода (т.е. усложнения кода) надо сделать вполне определённые усилия, что мешает делать это просто так, только когда есть основания. В итоге это сводится к тому, что код не даёт себя усложнять излишне, а думание над ним нередко приводит к упрощению решения.
    Я сомневаюсь, что можно избавиться от процесса размышления над задачей, думать надо, но возможность неосознанно усложнять код состояниями и нелокальными переходами лишь откладывает проблемы недодуманного решения на потом.

    ОтветитьУдалить
  3. Этот комментарий был удален автором.

    ОтветитьУдалить
  4. to VoidEx

    Вот, допустим, нужно мне сделать в чистой функции отладочную печать. Haskell мне этого сделать не даст и я буду вынужден добавить IO. А затем и во все чистые функции которые эту вызывают. Отладка программы на Haskell превращается в ад.

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

    По статье с автором совершенно согласен. Для себя я уже определил, что главное в лиспе - это свобода.

    ОтветитьУдалить
  6. > В итоге это сводится к тому,
    > что код не даёт себя усложнять излишне

    Угу, тут читаю книжку по Rails (ради образования, что бы знать с чем это едят), и там встретил термин "синтаксический уксус" - насколько я понял, достаточно известный термин в среде RoR. Идея всё та же: помешать писать плохо. Я сразу вспомнил С++, все эти public, protected, private, которые как бы тоже должны мешать писать плохо, а в итоге, рождается куча проблем, чрезмерная сложность, а полезной отдачи от этих свойств я в своих проектах не припомню (а писал я на С++ немало).

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

    ОтветитьУдалить
  7. to andy128k
    для отладки есть Debug.Trace

    to Valeriy
    К сожалению, не могу сравнить, по ощущениям, переколбашивать мне приходилось не чаще, проблем с этим _пока_ не встречал, а там посмотрим.

    Что действительно ощущается, так это то, что иногда над буквально 10-ю строками думать надо, не получается сходу наваять. Иногда, когда решение получается жутким, я размышляю, как бы я решил эту же задачу на C++/C#/etc., и прихожу к выводу, что не лучше (только длиннее раза в 3-4), разве что написать было бы чуть легче (из-за возможности впихивать состояния без отражения этого в типизации), но проблемы от этого не исчезают, т.е. само решение плохое, а не язык. Беда в том, что на C++/C# такое кривое решение даже не выглядит кривым, оно нормально и типично, в Хаскеле же это противоестесственно. Лично мне это наоборот нравится. Bondage&Discipline, так сказать.
    Это, разумеется, субъективные ощущения.

    ОтветитьУдалить
  8. to voidex:
    Сравнения с C++/C#/etc (инетерсно, что имеется в виду под etc) не интересно, это очень удобные мишени ;) лучше сравнивать с Perl/Python/Common Lisp/etc,

    ОтветитьУдалить
  9. to archimag
    > Я сразу вспомнил С++, все эти public, protected, private, которые как бы тоже должны мешать писать плохо, а в итоге, рождается куча проблем, чрезмерная сложность, а полезной отдачи от этих свойств я в своих проектах не припомню

    Ну так не надо по одной неудачной попытке обобщать. В Си++ я тоже не припомню полезной отдачи.

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

    Подозреваю, что вам нравится динамическая типизация?

    ОтветитьУдалить
  10. > Подозреваю, что вам нравится динамическая типизация?

    Угу. Я начинал изучать Common Lisp и Haskell в одно и то же время, когда работал над одной очень сложной проблемой, и даже склонялся в пользу Haskell именно из-за его более мощных (в функциональном плане) свойств. Но, это была действительно сложная проблема, первая версия алгоритма (в коем было в итоге менее 1000 строк кода) родилась только через два месяца и Haskell до этого срока не дожил. Я не знал как решить задачу, и пробовал разные варианты - Haskell для такой работы просто не подходит.

    ОтветитьУдалить
  11. to archimag
    > лучше сравнивать с Perl/Python/Common Lisp/etc
    В данном контексте нет разницы, я сравниваю с языками без функциональной чистоты.

    Неявное же введение переменных и динамическая типизация в Python хорошо сказывается при попытке изменять существующий код. В Хаскеле мне как раз жутко нравится невозможность где-то что-то исправить так, чтобы программа стала неверной, но скомпилировалась.
    Можно сказать, что настоящие мачо не ошибаются, но я в это смутно верю и больше доверяю type checker'у, тем более что это даёт мне возможность не растрачивать внимание на то, что может быть автоматизировано, а думать над задачей.

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

    Не понял, на чём была первая версия и каким образом не дожил Хаскель?

    > Я не знал как решить задачу, и пробовал разные варианты - Haskell для такой работы просто не подходит.

    Хотелось бы узнать, какие проблемы были с Хаскелем. Чисто практический интерес.

    ОтветитьУдалить
  12. > В данном контексте нет разницы,
    > я сравниваю с языками без функциональной чистоты.

    Есть огромная разница. Ибо утвержедние, что решение получается в 3-4 раза короче не может быть применимо ко всем языкам "без функциональной чистоты".

    > Не понял, на чём была первая версия и каким образом
    > не дожил Хаскель?

    Common Lisp, хаскель просто оказался неудобен в использовании и был благополучно забыт.

    > нравится невозможность где-то что-то исправить так,
    > чтобы программа стала неверной
    > Хотелось бы узнать, какие проблемы были с Хаскелем.
    > Чисто практический интерес.

    В этом корень проблем. В Common Lisp я не обязан выполнять всю программу целиком. Я спокойно тестирую в REPL отдельные её кусочки, изменяю их так, что программа нарушается, но я могу сразу понять как изменяется поведение конкретной функции и если это то, что мне надо, то тогда я уже переделываю программу. Мне не надо переделывать всю программу только для того, что бы убедиться в том, что моя идея была неверной, я могу увидеть это сразу.

    ОтветитьУдалить
  13. to archimag
    > Есть огромная разница. Ибо утвержедние, что решение получается в 3-4 раза короче не может быть применимо ко всем языкам "без функциональной чистоты".

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

    > В Common Lisp я не обязан выполнять всю программу целиком. Я спокойно тестирую в REPL отдельные её кусочки, изменяю их так, что программа нарушается, но я могу сразу понять как изменяется поведение конкретной функции и если это то, что мне надо, то тогда я уже переделываю программу

    А в чём проблема с REPL у Haskell'я? Я, собственно, всегда сижу с запущенным GHCi.

    ОтветитьУдалить
  14. > А в чём проблема с REPL у Haskell'я

    В том, что программа должна оставаться корректной.

    ОтветитьУдалить
  15. > В том, что программа должна оставаться корректной.

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

    ОтветитьУдалить
  16. to viodex:

    Разговор малость отошёл от темы статьи :)

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

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

    ОтветитьУдалить
  17. to archimag:

    Ну от темы-то отошли, потому что спорить не с чем, я с мыслью согласен. Кол-во строк важно, но совсем не потому, что их надо писать (я и тысячами на Си++ в своё время мог строчить), их надо читать, понимать, и модифицировать. Тут становятся важным лёгкость понимания (иногда я наоборот пишу вариант подлиннее, лишь бы он выглядет понятнее, как выражение мысли), и изменения.

    Как я неоднократно упоминал, Хаскель провоцирует писать более декларативно, это ему в плюс. В плане модифицирования тоже плюс, тут играет роль и типизация, и такие, казалось бы, мелочи, как то, что конструкторы надо писать с заглавной (тогда foo Bar = 34 никогда не воспримется так же, как foo bar = 34, т.е. стерев конструктор Bar мы не рискуем превратить паттерн в наиболее общий).
    Однако есть и минус. Любители писать монады забывают про монадические трансформеры, а язык никак не заставляет их писать. В итоге при желании впихнуть логирование в функцию бинарной сериализации я наткнулся на необходимость написать этот трансформер самому, хотя при наличии оного проблем бы не возникло, изменения небольшие и делаются просто.
    По поводу REPL я, вроде, написал. Проблем с изменением готового кода и использованием его в полусобранном состоянии нет, мощная типизация исключает множество ошибок.

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

    ОтветитьУдалить
  18. to voidex:

    Посмотрел я на Debug.Trace. Этож признание в собственной беспомощности. Этот unsafePerformIO -- не что иное как хак.

    ОтветитьУдалить
  19. to voidex:

    Ну я таки так и не понял, как мне понять причину, если мой удалённый сервер, написанный на Haskell, вдруг зависнет (во избежание флэйма на тему надёжности функциональных программ, сбой происходит на уровне системного вызова, очень редко)?

    Или я вот пишу для веб на Common Lisp, на одном мониторе у меня код, а на другом веб-страница. Я слегка изменяю код, нажимаю C-M-x для перекомпиляции изменённой функции и, без перезагрузки веб-сервера, без каких-либо дополнительных действий, на другом мониторе тут же смотрю изменения. (почти как в php :)) могу я так писать на haskell?

    ОтветитьУдалить
  20. to andy128k:
    > Этож признание в собственной беспомощности. Этот unsafePerformIO -- не что иное как хак.

    unsafePerformIO (как и unsafeInterleaveIO) не хак, unsafe он потому, что снимаются гарантии (чистота и порядок выполнения соответственно), за которые теперь ответственен программист. Он там как раз затем, чтобы протрейсить, как работает _чистый_ код (т.е. без повторных вычислений, без определённого порядка), т.е. как раз в отладочных целях. Там он вполне уместен.

    to archimag:

    А какое отношение поставленные вопросы имеют к языку? Удаленная отладка, вон, в C++ есть, уж не заслуга ли это языка и императивного подхода? Вас интересует теоретическая возможность горячей замены кода? Она есть. Ссылок не откопаю, но вроде кто-то писал proof-of-concept. Непонятно, почему бы ей не быть (возможности).
    Т.е. ответ: да, можете. Разумеется, при наличии выбора следует обращать внимание не только на сам язык, но и на библиотеки/инструментарий. В общем-то, оба ваших вопроса как раз не о языке как таковом. Для Хаскеля готовой, реализующей данный функционал, нету; но это не недостаток самого языка и уж тем более ФП подхода. Erlang с этим всем справляется, там готовенькое есть.

    ОтветитьУдалить
  21. to viodex:
    > А какое отношение поставленные вопросы имеют к языку?

    Язык может иметь блестящую спецификацию и безукоризненное математическое обоснование (как, например, Haskell), но на нём может оказать очень неудобно программировать реальные задачи. Я же не случайно упомянул в основном тексте о том, что заметная часть стандарта Common Lisp направлена на поддержку процесса разработки. Т.е. разработчики языка думали не только о том, насколько эффективно можно реализовать тот или иной алгоритм на CL, но и о том, как максимально упростить процесс разработки, какие инструментальные средства можно построить вокруг языка. В итоге, я очень сильно сомневаюсь, что для Haskell вообще возможно создание инструмента, хоть в какой-то мере сопоставимого со SLIME. В общем, это и отличает язык, созданный специально для решения сложных практических задач (Common Lisp), от языка, стремящего предоставить максимально чистую реализацию математической концепции (Haskell).

    ОтветитьУдалить