- PVSM.RU - https://www.pvsm.ru -
Я знаю людей, которые искренне недоумевают по поводу того, что функциональное программирование не очень популярно. К примеру, сейчас я читаю книжку «Из смоляной ямы» (Out of the Tar Pit), в которой авторы после аргументов в пользу функционального программирования говорят:
Тем не менее, факт остается фактом: этих аргументов не хватило для того, чтоб функциональное программирование получило широкое распространение. Следовательно, мы должны заключить, что главный недостаток функционального программирования—это обратная сторона его главного достоинства, а именно, что проблемы возникают, когда (как это часто бывает) проектируемая система должна поддерживать какого-то рода состояние.
А я думаю, что причина недостаточной популярности намного проще: программирование в функциональном стиле часто происходит «задом наперед» и выглядит больше как решение головоломок, чем объяснение задачи компьютеру. Часто, когда я пишу на функциональном языке, я знаю, что хочу сказать, но в итоге я решаю головоломки, чтоб выразить это средствами языка. Короче, функциональное программирование просто слишком неестественное.
Чтоб дальше обсуждать функциональное программирование, давайте попробуем испечь пирог. Возьмем рецепт отсюда [1]. Примерно так мы будем печь императивный пирог:
Я позволил себе несколько вольностей с нумерацией (очевидно, каждый шаг—это на самом деле несколько шагов), но давайте лучше посмотрим, как мы будем печь функциональный пирог:
А, ну его к черту—я не могу это закончить! (прим. перев. на самом деле, если следовать логике, даже приведенные пункты должен быть еще сложнее). Я не знаю, как перенести эти шаги в функциональный стиль без использования изменяемого состояния. Либо теряется последовательность шагов, либо надо писать «добавьте бананы», но тогда изменяется текущее состояние. Может, кто-нибудь в комментариях закончит? Хотелось бы посмотреть на версии с использованием монад и без использования монад.
Без использования 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.
В реальном коде имена у этих типов получше, но они для нас не важны. В этом примере есть очевидная дупликация: мы четыре раза выполняем совершенно одинаковый фрагмент кода с четырьмя разными парсерами. 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
Нажмите здесь для печати.