Что такое функциональное программирование

В про­грам­ми­ро­ва­нии есть два боль­ших под­хо­да — импе­ра­тив­ное и функ­ци­о­наль­ное. Они суще­ствен­но отли­ча­ют­ся логи­кой рабо­ты, ещё и созда­ют пута­ни­цу в назва­ни­ях. Сей­час объ­яс­ним.

🤔 Функциональное — это про функции?

❌ Нет. Функ­ци­о­наль­ное — это не про функ­ции. Функ­ции есть почти в любых язы­ках про­грам­ми­ро­ва­ния: и в функ­ци­о­наль­ных, и в импе­ра­тив­ных. Отли­чие функ­ци­о­наль­но­го про­грам­ми­ро­ва­ния от импе­ра­тив­но­го — в общем под­хо­де.

Метафора: инструкция или книга правил

Пред­ставь­те, что вы откры­ва­е­те кафе-столовую. Сей­час у вас там два типа сотруд­ни­ков: пова­ра и адми­ни­стра­то­ры.

Для пова­ров вы пише­те чёт­кие поша­го­вые инструк­ции для каж­до­го блю­да. Напри­мер:

  1. Налить воды в кастрю­лю
  2. Поста­вить кастрю­лю с водой на огонь
  3. Доба­вить в кастрю­лю с водой столько-то соли
  4. Если нуж­но при­го­то­вить 10 пор­ций, взять одну свёк­лу. Если нуж­но при­го­то­вить 20 пор­ций, взять две свёк­лы.
  5. Почи­стить всю свёк­лу, кото­рую вы взя­ли

Повар дол­жен сле­до­вать этим инструк­ци­ям ров­но в той после­до­ва­тель­но­сти, в кото­рой вы их напи­са­ли. Нель­зя сна­ча­ла почи­стить свёк­лу, а потом взять её. Нель­зя посо­лить кастрю­лю, в кото­рой нет воды. Поря­док дей­ствий важен и опре­де­ля­ет­ся вами. Это при­мер импе­ра­тив­но­го про­грам­ми­ро­ва­ния. Вы пове­ле­ва­е­те испол­ни­те­лем. Мож­но ска­зать, что испол­ни­те­ли выпол­ня­ют ваши зада­ния.

Для адми­ни­стра­то­ра вы пише­те не инструк­цию, а как бы кни­гу пра­вил:

  • У нас нель­зя со сво­им. Если гости при­шли со сво­им, то сде­лать им заме­ча­ние такое-то.
  • В зале долж­но быть чисто. Если в зале гряз­но, вызвать убор­щи­ка.
  • Если обра­зо­ва­лась оче­редь, открыть допол­ни­тель­ную кас­су.

Это тоже коман­ды, но испол­нять их адми­ни­стра­тор будет не в этой после­до­ва­тель­но­сти, а в любой на своё усмот­ре­ние. Мож­но ска­зать, что зада­ча это­го чело­ве­ка — испол­нять функ­ции адми­ни­стра­то­ра, и мы опи­са­ли пра­ви­ла, по кото­рым эти функ­ции испол­нять. Это при­мер функ­ци­о­наль­но­го про­грам­ми­ро­ва­ния.

❌ Про­грам­ми­сты, не бом­би­те

Конеч­но же, это упро­ще­но для пони­ма­ния. Вы сами попро­буй­те это нор­маль­но объ­яс­нить (мож­но пря­мо в ком­мен­тах).

Императивное программирование

При­ме­ры язы­ков: C, С++, Go, Pascal, Java, Python, Ruby

Импе­ра­тив­ное про­грам­ми­ро­ва­ние устро­е­но так:

В язы­ке есть коман­ды, кото­рые этот язык может выпол­нять. Эти коман­ды мож­но собрать в под­про­грам­мы, что­бы авто­ма­ти­зи­ро­вать неко­то­рые одно­тип­ные вычис­ле­ния. В каком поряд­ке запи­са­ны коман­ды внут­ри под­про­грам­мы, в том же поряд­ке они и будут выпол­нять­ся.

Есть пере­мен­ные, кото­рые могут хра­нить дан­ные и изме­нять­ся во вре­мя рабо­ты про­грам­мы. Пере­мен­ная — это ячей­ка для дан­ных. Мы можем создать пере­мен­ную нуж­но­го нам типа, поло­жить туда какое-то зна­че­ние, а потом поме­нять его на дру­гое.

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

Полу­ча­ет­ся, что в раз­ные дни функ­ция полу­ча­ет на вход 1000 ₽, но воз­вра­ща­ет раз­ные зна­че­ния — так рабо­та­ет импе­ра­тив­ное про­грам­ми­ро­ва­ние, когда всё зави­сит от дру­гих пере­мен­ных.

После­до­ва­тель­ность выпол­не­ния под­про­грамм регу­ли­ру­ет­ся про­грам­ми­стом. Он зада­ёт нуж­ные усло­вия, по кото­рым дви­жет­ся про­грам­ма. Вся логи­ка пол­но­стью про­ду­мы­ва­ет­ся про­грам­ми­стом — как он ска­жет, так и будет. Это зна­чит, что раз­ра­бот­чик может точ­но пред­ска­зать, в какой момент какой кусок кода выпол­нит­ся — код полу­ча­ет­ся пред­ска­зу­е­мым, с понят­ной логи­кой рабо­ты.

Если у нас код, кото­рый счи­та­ет скид­ку, дол­жен вызы­вать­ся толь­ко при финаль­ном оформ­ле­нии зака­за, то он выпол­нит­ся имен­но в этот момент. Он не посчи­та­ет скид­ку зара­нее и не про­пу­стит момент оформ­ле­ния.

👉 Суть импе­ра­тив­но­го про­грам­ми­ро­ва­ния в том, что про­грам­мист опи­сы­ва­ет чёт­кие шаги, кото­рые долж­ны при­ве­сти код к нуж­ной цели.

Зву­чит логич­но, и боль­шин­ство про­грам­ми­стов при­вык­ли имен­но к тако­му пове­де­нию кода. Но функ­ци­о­наль­ное про­грам­ми­ро­ва­ние рабо­та­ет совер­шен­но ина­че.

Функциональное программирование

При­ме­ры язы­ков: Haskell, Lisp, Erlang, Clojure, F#

Смысл функ­ци­о­наль­но­го про­грам­ми­ро­ва­ния в том, что мы зада­ём не после­до­ва­тель­ность нуж­ных нам команд, а опи­сы­ва­ем вза­и­мо­дей­ствие меж­ду ними и под­про­грам­ма­ми. Это похо­же на то, как рабо­та­ют объ­ек­ты в объектно-ориентированном про­грам­ми­ро­ва­нии, толь­ко здесь это реа­ли­зу­ет­ся на уровне всей про­грам­мы.

Напри­мер, в ООП нуж­но задать объ­ек­ты и пра­ви­ла их вза­и­мо­дей­ствия меж­ду собой, но так­же мож­но и напи­сать про­сто код, кото­рый не при­вя­зан к объ­ек­там. Он как бы сто­ит в сто­роне и вли­я­ет на рабо­ту про­грам­мы в целом — отправ­ля­ет одни объ­ек­ты вза­и­мо­дей­ство­вать с дру­ги­ми, обра­ба­ты­ва­ет какие-то резуль­та­ты и так далее.

Функ­ци­о­наль­ное про­грам­ми­ро­ва­ние здесь идёт ещё даль­ше. В нём весь код — это пра­ви­ла рабо­ты с дан­ны­ми. Вы про­сто зада­ё­те нуж­ные пра­ви­ла, а код сам раз­би­ра­ет­ся, как их при­ме­нять.

Если мы срав­ним прин­ци­пы функ­ци­о­наль­но­го под­хо­да с импе­ра­тив­ным, то един­ствен­ное, что сов­па­дёт, — и там, и там есть коман­ды, кото­рые язык может выпол­нять. Всё осталь­ное — раз­ное.

Коман­ды мож­но соби­рать в под­про­грам­мы, но их после­до­ва­тель­ность не име­ет зна­че­ния. Нет раз­ни­цы, в каком поряд­ке вы напи­ше­те под­про­грам­мы — это же про­сто пра­ви­ла, а пра­ви­ла при­ме­ня­ют­ся тогда, когда нуж­но, а не когда про них ска­за­ли.

Пере­мен­ных нет. Вер­нее, они есть, но не в том виде, к кото­ро­му мы при­вык­ли. В функ­ци­о­наль­ном язы­ке мы можем объ­явить пере­мен­ную толь­ко один раз, и после это­го зна­че­ние пере­мен­ной изме­нить­ся не может. Это как кон­стан­ты — запи­са­ли и всё, теперь мож­но толь­ко про­чи­тать. Сами же про­ме­жу­точ­ные резуль­та­ты хра­нят­ся в функ­ци­ях — обра­тив­шись к нуж­ной, вы все­гда полу­чи­те иско­мый резуль­тат.

Функ­ции все­гда воз­вра­ща­ют одно и то же зна­че­ние, если на вход посту­па­ют одни и те же дан­ные. Если в про­шлом при­ме­ре мы отда­ва­ли в функ­цию сум­му в 1000 ₽, а на выхо­де полу­ча­ли скид­ку в зави­си­мо­сти от дня неде­ли, то в функ­ци­о­наль­ном про­грам­ми­ро­ва­нии если функ­ция полу­чит в каче­стве пара­мет­ра 1000 ₽, то она все­гда вер­нёт одну и ту же скид­ку неза­ви­си­мо от дру­гих пере­мен­ных.

Мож­но про­ве­сти ана­ло­гию с мате­ма­ти­кой и сину­са­ми: синус 90 гра­ду­сов все­гда равен еди­ни­це, в какой бы момент мы его ни посчи­та­ли или какие бы углы у нас ещё ни были в зада­че. То же самое и здесь — всё пред­ска­зу­е­мо и зави­сит толь­ко от вход­ных пара­мет­ров.

После­до­ва­тель­ность выпол­не­ния под­про­грамм опре­де­ля­ет сам код и ком­пи­ля­тор, а не про­грам­мист. Каж­дая коман­да — это какое-то пра­ви­ло, поэто­му нет раз­ни­цы, когда мы запи­шем это пра­ви­ло, в нача­ле или в кон­це кода. Глав­ное, что­бы у нас это пра­ви­ло было, а ком­пи­ля­тор сам раз­бе­рёт­ся, в какой момент его при­ме­нять.

В рус­ском язы­ке всё рабо­та­ет точ­но так же: есть пра­ви­ла пра­во­пи­са­ния и грам­ма­ти­ки. Нам неваж­но, в каком поряд­ке мы их изу­чи­ли, глав­ное — что­бы мы их вовре­мя при­ме­ня­ли при напи­са­нии тек­ста или в уст­ной речи. Напри­мер, мы можем сна­ча­ла прой­ти пра­ви­ло «жи-ши», а потом пра­ви­ло про «не с гла­го­ла­ми», но при­ме­нять мы их будем в том поряд­ке, какой тре­бу­ет­ся в тек­сте.

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