Pull to refresh

Чем изучение Haskell/Python вредит программисту

Reading time 5 min
Views 27K
Original author: Luke Plant
От переводчика:

На этот перевод меня толкнула очередная ситуация «мозгового ступора» при написании банального кода на C++/C#: я часами смотрел на уродливый код, но не мог сделать его по-настоящему лучше; мне пришлось просто принять то, что уродливым его видел только я один, и это была моя проблема, а не проблема в коде или языке. Я вспомнил те времена, когда такого со мной не было — а заодно вспомнил и эту старую статью, автор которой пострадал так же, как и я, при этом хорошо написав об этом.


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

  1. Демотивация.

    Я заметил, что мыслю на Python и иногда даже на Haskell в какой-то мере — несмотря на то, что я использовал Haskell совсем немного. Я постоянно обнаруживаю в себе желание использовать идиомы из этих языков и подсчитываю, насколько сильно я мог бы сократить код, если бы использовал эти языки (которые, хоть и сильно отличаются друг от друга, оба намного мощнее того языка, который я использую на работе — C#). В большинстве случаев этот подсчет показывает, что используя любой из этих языков я мог бы уменьшить размер своего кода в 2-5 раз, и нередко в 10-20 раз для некоторых частей code base.

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

    Я так же нахожу C#-код весьма уродливым в сравнении с Python и Haskell. На визуальном уровне обязательное использование скобок (ну ладно, не всегда обязательное, но обычно стандарты кодирования это справедливо не поощряют) делает код очень шумным и разреженным, и в сочетании со строгостью и многословием библиотек — вы обнаруживаете, что целая страница C#'a, общем-то, ничего не делает. А если уж говорить про красоту на математическом уровне, то код на C# это просто корявая глиняная хибара в сравнении с элегантным шпилем кода на Haskell.

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

    В C# начали появляться фичи, которые располагают к функциональному стилю программирования. Итак, в один прекрасный день, столкнувшись со вполне шаблонной ситуацией — я попробовал функциональное решение. У меня был список объектов Foo, у каждого был метод Description(), возвращающий строку. Мне нужно было склеить все непустые description'ы, разделив их переносами строки.

    Код, который я бы написал на Python, выглядит так:
    "\n".join(foo.description() for foo in mylist if foo.description() != "")

    Или вот так, на Haskell:
    concat $ List.intersperse "\n" $ filter (/= "") $ map description mylist

    От переводчика, на Haskell:
    map description >>> delete "" >>> List.intersperse "\n" >>> concat

    Используя generics-методы из C# 2.0, лучшим, что я получил, было:
    string.Join("\n", mylist.ConvertAll<string>(
        delegate(Foo foo)
        {
            return foo.Description();
        }).FindAll(
        delegate(string x)
        {
            return x != "";
        }).ToArray());


    Если бы я работал с другой структурой данных, C#-версия вышла бы еще более ужасной — а ведь для C# существуют сотни разных типов коллекций, семантически несовместимых. А еще надо заметить, что для написания какого-либо метода, принимающего «делегаты» (ближайщий родственник функциям-объектам первого класса), вы должны объявить сигнатуру принимаемой функции отдельно от метода, что еще больше усугубляет ситуацию с читаемостью функционального кода.

    Вот несколько проблем с C#-решением, приведенным выше. Первая заключается в том, что размер кода не сильно уменьшился (если вообще уменьшился) по сравнению с императивным кодом. Сравните это с нудным циклом, который я бы написал вместо этого:
    string retval = "";
    foreach (Foo foo in mylist)
    {
        string desc = foo.description();
        if (desc != "")
        {
            if (retval != "")
                retval += "\n";
            retval += desc;
        }
    }


    Не очень-то и много кода.

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

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

    Факт — функциональные идиомы плохо работают в языках, не имеющих синтаксической поддержки для них. С Java, насколько я могу судить, всё было бы еще хуже. C# страдает от того, что несмотря на появление в C# 2.0 фич, помогающих программировать функционально, подавляющая часть библиотек для .NET осталась неизменной, не используя эти фичи — равно как и наш собственный код.

    Здесь можно возразить, что вы все еще можете использовать сами принципы функционального программирования (нет побочных эффектов, результат функции зависит только от её параметров и т.п.) и получать выгоду от этого, даже если нет языковой поддержки этих идиом. В реальности же, библиотеки и фреймворки разработаны для императивных языков и по-другому просто не работают. ASP.NET — особенно хороший пример этого. Вы разрабатываете контролы, наследуя их от Control и перекрывая некоторые методы. Большая часть этих методов не имеют возвращаемого значения и параметров, совершая лишь изменение состояния объекта или других объектов. Потом эти методы вызываются фреймворком в неясном и сложном порядке (да, это кошмар в отладке).

    Фактически, применение принципов функционального программирования привело бы меня к использованию лишь статических методов, не используя instance-методы, везде где это только возможно, избегая всего, что изменяет состояние или даже потенциально способного его изменить. Я бы использовал лишь простые 'тупые' типы данных, отделяя алгоритмы от них. Это противоречит идеологии (или, по крайней мере, практике) основной парадигмы программирования, популярной сегодня — OOP. Я не могу применять то, что я считаю хорошими принципами написания кода, без игнорирования самой парадигмы языка и библиотек, которыми я окружен. Это довольно безнадежно.
  3. — моя продуктивность, фактически, упала. Когда я впадаю в ступор при написании кода на C#, я иду и переписываю этот код заново на Haskell и Python, лишь для того, чтобы продемонстрировать себе, насколько эти языки лучше — это бессмысленное упражнение лишь демотивирует меня еще сильнее.

    Мораль истории: не пытайтесь улучшить себя до тех пор, пока у вас нет возможности улучшить и своё окружение соответственно. Это весьма печальный вывод.
Tags:
Hubs:
+76
Comments 222
Comments Comments 222

Articles