Возможно вам приходилось слышать такие слова как «Clojure», «Scala» или «Erlang», а может даже фразы вроде «В Java теперь появились лямбда-функции». И возможно вы даже знаете, что все это связано с каким-то функциональным программированием. Если вы частым посетителем любых программистских сообществ, то эти темы должны были подниматься в последнее время все чаще. Однако, если вы введете в гугле «функциональное программирование», то не увидите
ничего нового. Скорее всего большинство результатов так или иначе будут связаны с Lisp — языке, который изобрели только в пятидесятых годах прошлого века. Почему же интерес к функционального программирования стал проявляться только в последнее время, 60 лет спустя?
Давным-давно, когда компьютеры были очень медленными
Даже медленнее, чем их собственные жесткие диски. В те времена существовало два основных взгляда на то, каким должен быть внутреннее устройство компьютера и какой должен быть язык программирования:
- Взять архитектуру Фон Неймана и добавить абстрактность;
- Взять математику и убрать абстрактность
Мощности компьютеров того времени не позволяли в полной мере ощутить преимущества уровня абстрактности, который предоставляет функциональное программирование. Поэтому Lisp тех пор так и не набрал обороты, а наоборот — медленно, но уверенно покрывался забвением. В то время как императивное программирование начало сходить на пьедестал, особенно начиная с появлением C.
Но времена изменились
Эволюция компьютеров и развитие средств виртуализации привели к тому, что теперь мы можем даже не задаваться вопросом, на каком языке написана та или иная программа. Наконец в функционального программирования появился второй шанс.
Функциональное программирование 50.5
В этом разделе я не буду знакомить вас с функциональным программированием или что-то вроде того. Прочитав до конца вы сами должны будете решить, что это такое, хорошо это или плохо и как начать программировать функционально. Допустим вы впервые слышите словосочетание «функциональное программирование». Вдумайтесь в название, может показаться, что это какое-программирования, которое как-то связано с функциями, и вы не ошибетесь, вы можете только недооценить то, насколько сильно это программирование связано с функциями. Здесь функция на функции и функцией погоняют. Помните конструкции типа «f ∘ g» по высшей математике? Вот как-то так тут все и делается. Далеко неполный список ключевых понятий ФП:
- функции первого класса;
- функции более высокого порядка;
- чистые функции;
- замыкания;
- неизменное состояние.
Но вам сейчас важнее не запомнить красивые названия, а понять, что означает то или иное понятие.
Функции первого класса — означает, что вы можете хранить функции в переменных. Думаю вам приходилось делать что-то вроде этого на JS:
1 2 3 |
var add = function(a, b){ return a + b } |
Вы сохраняете функцию, которая получает a и b, а возвращает a + b, в переменную add.
Функции высшего порядка — функции, которые могут возвращать или принимать другие функции в качестве своего аргумента. Опять же JS:
1 2 3 4 |
document.querySelector('#button') .addEventListener('click', function(){ alert('yay, i got clicked') }) |
или:
1 2 3 4 5 6 7 8 |
var add = function(a){ return function(b){ return a + b } } var add2 = add(2) add2(3) // => 5 |
Функции в обоих примерах являются функциями высшего порядка и даже если вам не приходилось писать что-то подобное, вы могли неоднократно видеть подобный прием в чужом коде.
Чистые функции — функция, которая не изменяет никакие данные, а просто берет и возвращает какие-то значения, как наши любимые функции в математике. То есть если вы передаете функции f число 2, а она возвращает вам 10, это означает, что она возвращает 10 всегда в подобной ситуации. Результаты, выдаваемые такой функцией, никак не зависят от окружения или порядка вычислений. Также использование чистых функций не влечет за собой побочных эффектов в различных частях программы, поэтому это очень мощный инструмент.
Замыкания — это, когда вы сохраняете некоторые данные в функции и делаете их доступными только для особой функции возврата, то есть функция возврата сохраняет свою среду выполнения:
1 2 3 4 5 6 7 8 |
var add = function(a){ return function(b){ return a + b } } var add2 = add(2) add2(3) // => 5 |
Теперь вернитесь ко второму примеру функций высшего порядка. Переменная a замкнутая и доступна только для функции возврата. Вообще-то замыкания это не особенность функционального программирования, она используется для оптимизации работы программ.
Неизменное состояние — это, когда вы не можете изменить ни одно из состояний (даже если можете задать новое). В следующем примере (язык — OCaml), x и 5 эквивалентны и взаимно заменимы в любой части программы — x всегда равна 5-ти.
1 2 3 4 |
let x = 5;; x = 6;; print_int x;; (* prints 5 *) |
Ничего особенного, правда? Вы удивитесь, но я скажу, что это неоднократно спасет вам жизнь.
ООП больше нас не спасет
Долгожданное время, когда программы выполняются одновременно на множестве машин, наступило. Но мы отстаем. То, как мы решаем вопрос параллелизма и распределенных вычислений, только добавляет сложности программированию. Чтобы исправить ситуацию нужно более простой и надежный способ, чем парадигма ООП. Еще не забыли ключевые понятия ФП, которые мы рассмотрели чуть раньше? Чистые функции, неизменное состояние, вот это все? Прекрасно. А что если я скажу вам, что не обязательно обрабатывать тысячи результатов, запуская одну и ту же функцию на тысячи ядер? Чтобы сделать то же самое с помощью функционального программирования достаточно одного. Жизнь уже не будет такой, как раньше.
Почему ООП?
ООП не может соперничать с ФП в подходе к реализации параллелизма и распределенных вычислений, так как весь ООП основывается на понятии сменности состояния (вообще-то это особенность императивных языков, но суть в том, что подавляющее большинство являются объектно-ориентированными). Дело в объектных методах. Проблема возникает, когда от одного и того же метода требуют синхронности выполнения на множестве ядер, что в итоге выливается в необъятные потоки дополнительного кода, а это отнюдь не добавляет простоты или гибкости. Я не пытаюсь переманить вас на сторону функционального программирования, но вдумайтесь: Java и С ++ 11 уже дружат с Лямба-вычислениям. То есть мейнстримные языки уже начинают переодеваться в ФП и я гарантирую, что скоро к ним подключатся их менее популярные братья. Важно отметить, что вам не придется отказываться от переменных состояния, главная идея функционального программирования заключается в том, чтобы использовать их только тогда, когда это действительно необходимо.
Я не работаю с облаками, нужен мне ваш ФП?
Да. Вы же хотите писать лучше, а проблемы решать проще?
Я пытался, но получилось как-то слишком сложно и нечитаемым.
Сначала все трудно. Думаю, вам приходилось овладев одным языком, браться за другой и вы изо всех сил пытались в нем разобраться. Скорее всего это были объектно-ориентированные языки, так что перед началом изучения нового вы уже знали основные идиомы, приемы и владели пониманием того, что такое условие, а что такое цикл. Изучение функционального программирования — это изучение программирования с нуля, с азов. Часто приходится слышать, что код, который написан на языке функционального программирования, трудно читать. Если вы только и делали, что программировали на императивных языках, то код на ФП вам покажется шифрограммой. Но дело не в том, что там все зашифровано от греха подальше, а в том, что вы просто не знаете основных идиом. Что же попробуйте узнать, а затем посмотреть на тот же код снова. Посмотрите на код одной и той же программы, написанные на Haskell и JS в стиле императивного программирования:
1 2 3 |
guess :: Int -> [Char] guess 7 = "Much 7 very wow." guess x = "Ooops, try again." |
1 2 3 4 5 6 7 8 |
function guess(x){ if(x == 7){ return "Much 7 very wow." } else { return "Oops, try again." } } |
Это простая программа, которая выдает приветственное сообщение, если пользователь вводит цифру 7 или выдает ошибку в противном случае. Удивительно, но для Haskell достаточно 2 строки (не считая первого — это аннотация к типам). Вы сможете так же, если разберетесь с таким понятием как «сопоставление с образцом», которое, кстати, не является особенностью ФП, но именно в нем используется повсеместно. Что делает Haskell в коде выше: О, похоже у нас тут семерка, что же поздравлю пользователя с его маленькой победой, чего бы я не сделал, если бы это была не семерка. Вообще-то также думает и JS, но Haskell работает методом сравнения с паттернами , которые предоставил программист. Преимущества такого метода перед традиционными if-else решениями раскрываются во всей красе, когда возникает необходимость работать с большими объемами структурированных данных.
1 2 3 4 5 6 |
plus1 :: [Int] -> [Int] plus1 [] = [] plus1 (x:xs) = x + 1 : plus1 xs -- plus1 [0,1,2,3] -- > [1,2,3,4] |
В этом примере функция plus1 принимает список int-значений и добавляет к каждому единицу. Сопоставление происходит с пустым списком [] и непустое согласно паттерна: первому элементу дается имя x, а остальному списку — xs, затем производится добавление и объединение с рекурсивным вызовом. Думаю, что было бы сложно написать функцию plus1, используя ООП, в две строки и сохранить тот же уровень читабельности.
Вот такая получилась большая статья о функциональном программировании. Следите за обновлениями Prologistic.com.ua
Сейчас изучаю clojure и эта статья многое поставила на свои места. Спасибо!
(def add
(fn [a]
(fn [b] (+ a b))))
(def add2 (add 2))
(add2 5) => 5 😉
(def func
(fn [vector] (map inc vector)))
(func [0 1 2 3]) => (1 2 3 4)