Функціональне програмування непопулярно, тому що воно дивне

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

Тим не менш, факт залишається фактом: цих аргументів не вистачило для того, щоб функціональне програмування отримало широке поширення. Отже, ми повинні укласти, що головний недолік функціонального програмування—це зворотна сторона його головної переваги, а саме, що проблеми виникають, коли (як це часто буває) проектована система повинна підтримувати якогось роду стан.
А я думаю, що причина недостатньої популярності набагато простіше: програмування в функціональному стилі часто відбувається «задом наперед» і виглядає більше як рішення головоломок, ніж пояснення завдання комп'ютера. Часто, коли я пишу на функціональному мовою, я знаю, що хочу сказати, але в підсумку я вирішую головоломки, щоб висловити це засобами мови. Коротше, функціональне програмування просто занадто неприродне.

Щоб далі обговорювати функціональне програмування, давайте спробуємо спекти пиріг. Візьмемо рецепт звідси. Приблизно так ми будемо пекти імперативний пиріг:
  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(пухнастий, 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. Таке значення є і в вікіпедії (останній параграф перед змістом). В реальному коді імена у цих типів трохи краще, але вони для нас не важливі. В цьому прикладі є очевидна дупликация: ми чотири рази виконуємо абсолютно однаковий фрагмент коду з чотирма різними парсерами. 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), ви повинні знати, що одне це перетворює будь-код в ребус.

Так чи інакше, мій посил такий: у мене був простий імперативний код, який робив одне й те саме кілька разів. Щоб переписати його через шаблони, не можна просто так взяти і обернути повторюваний фрагмент цикл. Замість цього треба повністю поміняти логіку програми. І для цього треба вирішити дуже багато головоломок. Більше того, я навіть і не вирішив їх з першого разу. У першій спробі я зупинився на чомусь надто складному та так і залишив імперативний варіант. І тільки після повернення до проблеми через кілька днів я придумав більш просте рішення вище. Переписування коду через шаблони не повинно бути таким складним. Коли основна проблема не в тому, щоб зрозуміти, що повинна робити програма, а в тому, щоб зрозуміти, як змусити її це робити.

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

З урахуванням всього вищесказаного, вважаю я, що функціональні мови погані? Зовсім ні. Користь від функціональних мов цілком відчутна. Кожен повинен вивчити хоча б один функціональний мову і пробувати застосовувати отримані знання в інших мовах. Але якщо функціональні мови хочуть стати популярними, вони повинні бути менше пов'язані з розгадуванням ребусів.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.