- PVSM.RU - https://www.pvsm.ru -

Функциональное программирование непопулярно, потому что оно странное

Я знаю людей, которые искренне недоумевают по поводу того, что функциональное программирование не очень популярно. К примеру, сейчас я читаю книжку «Из смоляной ямы» (Out of the Tar Pit), в которой авторы после аргументов в пользу функционального программирования говорят:

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

А я думаю, что причина недостаточной популярности намного проще: программирование в функциональном стиле часто происходит «задом наперед» и выглядит больше как решение головоломок, чем объяснение задачи компьютеру. Часто, когда я пишу на функциональном языке, я знаю, что хочу сказать, но в итоге я решаю головоломки, чтоб выразить это средствами языка. Короче, функциональное программирование просто слишком неестественное.

Чтоб дальше обсуждать функциональное программирование, давайте попробуем испечь пирог. Возьмем рецепт отсюда [1]. Примерно так мы будем печь императивный пирог:

  1. Разогрейте духовку до 175°C. Смажьте маслом и посыпьте мукой противень. В маленькой миске смешайте муку, пищевую соду и соль.
  2. В большой миске взбивайте масло, сахар-песок и коричневый сахар до тех пор, пока масса не станет легкой и воздушной. Вбейте яйца, одно за раз. Добавьте бананы и разотрите до однородной консистенции. Поочередно добавляйте в получившуюся кремовую массу основу для теста из п. 1 и кефир. Добавьте измельченные грецкие орехи. Выложите тесто в подготовленный противень.
  3. Запекайте в разогретой духовке 30 минут. Выньте противень из духовки, поставьте на полотенце, чтоб пирог остыл.

Я позволил себе несколько вольностей с нумерацией (очевидно, каждый шаг—это на самом деле несколько шагов), но давайте лучше посмотрим, как мы будем печь функциональный пирог:

  1. Пирог—это горячий пирог, остывший на полотенце, где горячий пирог—это подготовленный пирог, выпекавшийся в разогретой духовке 30 минут.
  2. Разогретая духовка—это духовка, разогретая до 175°C.
  3. Подготовленный пирог—это тесто, выложенное в подготовленный противень, где тесто—это кремовая масса, в которую добавили измельченные грецкие орехи. Где кремовая масса—это масло, сахар-песок и коричневый сахар, взбитые в большой миске до тех пор, пока они не стали легкими и воздушными, где…

А, ну его к черту—я не могу это закончить! (прим. перев. на самом деле, если следовать логике, даже приведенные пункты должен быть еще сложнее). Я не знаю, как перенести эти шаги в функциональный стиль без использования изменяемого состояния. Либо теряется последовательность шагов, либо надо писать «добавьте бананы», но тогда изменяется текущее состояние. Может, кто-нибудь в комментариях закончит? Хотелось бы посмотреть на версии с использованием монад и без использования монад.

В комментариях к оригинальноей статье предложили несколько вариантов

С монадами не было, но было с и без pipe forward operator.

Без использования pipe forward operator:

cake = cooled(removed_from_oven(added_to(30min, poured(greased(floured(pan)), stirred(chopped(walnuts),
alternating_mixed(buttermilk, whisked(flour, baking soda, salt), 
mixed(bananas, beat_mixed(eggs, creamed_until(fluffy, butter, white sugar, brown sugar)))), 
preheated(175C, oven))))))

C использованием pipe forward operator:

cake = bake(cake_mixture, 30min, prepare(pan, (grease, flour)), preheated(175C, oven))
where cake_mixture =
creamed :until_fluffy ‘butter’ ‘white’ ‘sugar’ ‘brown sugar’
|> beat_mixed_with ‘eggs’
|> mixed_with ‘bananas’
|> mixed_with :alternating ‘buttermilk’ ‘dry_goods’
|> mixed_with chopped ‘walnuts’
where dry_goods = whisked ‘flour’ ‘baking soda’ ‘salt’

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

Я пишу этот пост, потому что столкнулся недавно с похожей проблемой. Так уж оказалось, что шаблоны С++ — это функциональный язык. И когда разработчики С++ это поняли, то вместо того, чтоб решить проблему, стали всячески лелеять шаблоны в функциональном стиле, что иногда делает переписывание обычного кода через шаблоны очень утомительным. Например, вот что я недавно написал для парсера. (Я знаю, глупо писать свой собственный парсер, но старые тулзы типа yacc и bison плохие, а когда я попробовал пользоваться boost spirit, то столкнулся с проблемами, решение которых занимало слишком много времени, и в конце концов я просто решил написать свой парсер.)

ParseResult<V> VParser::parse_impl(ParseState state)
{
    ParseResult<A> a = a_parser.parse(state);
    if (ParseSuccess<A> * success = a.get_success())
        return ParseSuccess<V>{{std::move(success->value)}, success->new_state};
    ParseResult<B> b = b_parser.parse(state);
    if (ParseSuccess<B> * success = b.get_success())
        return ParseSuccess<V>{{std::move(success->value)}, success->new_state};
    ParseResult<C> c = c_parser.parse(state);
    if (ParseSuccess<C> * success = c.get_success())
        return ParseSuccess<V>{{std::move(success->value)}, success->new_state};
    ParseResult<D> d = d_parser.parse(state);
    if (ParseSuccess<D> * success = d.get_success())
        return ParseSuccess<V>{{std::move(success->value)}, success->new_state};
    return select_parse_error(*a.get_error(), *b.get_error(), *c.get_error(), *d.get_error());
}

Эта функция парсит входной параметр в variant type типа V, пытаясь запарсить входной параметр как тип A, B, C или D.

прим. перев.

Если я правильно понимаю, в этом контексте термин variant type значит примерно «размеченное объединение/алгебраический тип данных», и действительно: V может быть либо A, либо B, либо C, либо D. Такое значение есть и в википедии [2] (последний параграф перед оглавлением).

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

template<typename Variant, typename... Types>
ParseResult<Variant> parse_variant(ParseState state, Parser<Types> &... parsers)
{
    boost::optional<ParseError> error;
    template<typename T>
    for (Parser<T> & parser : parsers)
    {
        ParseResult<T> result = parser.parse(state);
        if (ParseSuccess<T> * success = result.get_success())
            return ParseSuccess<Variant>{{std::move(success->value)}, success->new_state};
        else
            error = select_parse_error(error, *result.get_error());
    }
    return *error;
}
ParseResult<V> VParser::parse_impl(ParseState state)
{
    return parse_variant<V>(state, a_parser, b_parser, c_parser, d_parser);
}

Этот код чуть-чуть неоптимален, потому что надо выбирать нужное сообщение об ошибке, но в целом это довольно тривиальная трансформация исходного примера. За исключением того, что вы не можете написать так на С++. Как только в игру вступают шаблоны, вам нужно думать более функционально. Вот мой вариант:

template<typename Variant, typename First>
ParseResult<Variant> parse_variant(ParseState state, Parser<First> & first_parser)
{
    ParseResult<First> result = first_parser.parse(state);
    if (ParseSuccess<First> * success = result.get_success())
        return ParseSuccess<Variant>{{std::move(success->value)}, success->new_state};
    else
        return *result.get_error();
}
template<typename Variant, typename First, typename... More>
ParseResult<Variant> parse_variant(ParseState state, Parser<First> & first_parser, 
    Parser<More> &... more_parsers)
{
    ParseResult<First> result = first_parser.parse(state);
    if (ParseSuccess<First> * success = result.get_success())
        return ParseSuccess<Variant>{{std::move(success->value)}, success->new_state};
    else
    {
        ParseResult<Variant> more_result = parse_variant<Variant>(state, more_parsers...);
        if (ParseSuccess<Variant> * more_success = more_result.get_success())
            return std::move(*more_success);
        else
            return select_parse_error(*result.get_error(), *more_result.get_error());
    }
}
ParseResult<V> VParser::parse_impl(ParseState state)
{
    return parse_variant<V>(state, a_parser, b_parser, c_parser, d_parser);
}

И я, честно говоря, очень доволен этим вариантом. Конечно, это тяжелее читать, потому что итерирование скрыто теперь в рекурсии, но если б вы только видели мой код до того, как я придумал это решение… У меня была структура с полем std::tuple<std::reference_wrapper<Parser>…>. Если вы когда-нибудь работали с кортежем переменной длины из стандартной библиотеки (то бишь variadic sized std::tuple), вы должны знать, что одно это превращает любой код в ребус.

Так или иначе, мой посыл таков: у меня был простой императивный код, который делал одно и то же несколько раз. Чтоб переписать его через шаблоны, нельзя просто так взять и обернуть повторяющийся фрагмент в цикл. Вместо этого надо полностью поменять логику программы. И для этого нужно решить слишком много головоломок. Более того, я даже и не решил их с первого раза. В первой попытке я остановился на чем-то слишком сложном и так и оставил императивный вариант. И только после возвращения к проблеме через несколько дней я придумал более простое решение выше. Переписывание кода через шаблоны не должно быть таким сложным. Когда основная проблема не в том, чтоб понять, что должна делать программа, а в том, чтоб понять, как заставить ее это делать.

И у меня такое ощущение в функциональных языках слишком часто. Я знаю, шаблоны С++ — плохой функциональный язык, но даже в хороших функциональных языках я трачу слишком много времени на попытки понять, как именно мне выразить вещи в языке, вместо размышлений о том, что же я хочу выразить.

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

Автор: Bas1l

Источник [3]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/razrabotka/134988

Ссылки в тексте:

[1] отсюда: http://allrecipes.com/recipe/7754/a-number-1-banana-cake/

[2] википедии: https://en.wikipedia.org/wiki/Variant_type

[3] Источник: https://habrahabr.ru/post/303312/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best