Можливо вам доводилося чути такі слова як "Clojure", "Scala" або "Erlang", а може навіть фрази на кшталт "В Java тепер з'явилися лямбда-функції". І можливо ви навіть знаєте, що все це пов'язано з якимось функціональним програмуванням. Якщо ви є частим відвідувачем будь-яких програмістських спільнот, то ці теми повинні були підніматися останнім часом все частіше. Однак, якщо ви введете в гуглі "функціональне програмування", то не побачите нічого нового. Швидше за все більшість результатів так чи інакше будуть пов'язані з Lisp - мовою, яку винайшли аж у 50-их роках минулого століття. Чому ж інтерес до функціонального програмування став виявлятися тільки останнім часом, 60 років потому?
Давним-давно, коли комп'ютери були дуже повільними ...
...навіть повільнішими, ніж їх власні жорсткі диски, існувало два основних погляди на те, яким має бути внутрішній устрій комп'ютера і якою має бути мова програмування:
- взяти архітектуру Фон Неймана і додати абстрактність;
- взяти математику і прибрати абстрактність
Потужності комп'ютерів того часу не дозволяли повною мірою відчути переваги рівня абстрактності, який надає функціональне програмування. Тому Lisp відтоді так і не набрав обертів, а навпаки - повільно, але впевнено покривався забуттям. У той час як імперативне програмування почало сходити на п'єдестал, особливо починаючи з появи C.
Але часи змінилися
Еволюція комп'ютерів і розвиток засобів віртуалізації призвели до того, що тепер ми можемо навіть не задаватися питанням, якою мовою написана та чи інша програма. Нарешті у функціонального програмування з'явився другий шанс.
Функціональне програмування 50.5
У цьому розділі я не буду знайомити вас з функціональним програмуванням або щось на зразок того. Прочитавши до кінця ви самі повинні будете вирішити, що це таке, добре це чи погано і як почати програмувати функціонально. Припустимо ви вперше чуєте словосполучення "функціональне програмування". Вдумайтеся в назву, може здатися, що це якесь програмування, яке якось пов'язано з функціями, і ви не помилитесь, ви можете тільки недооцінити те, наскільки сильно це програмування пов'язано з функціями. Тут функція на функції і функцією поганяють. Пам'ятаєте конструкції типу "f ∘ g" з вищої математики? От якось так тут все і робиться. Далеко неповний список ключових понять ФП:
- функції першого класу;
- функції вищого порядку;
- чисті функції;
- замикання;
- незмінний стан.
Але вам зараз важливіше не запам'ятати гарні назви, а зрозуміти, що означає те чи інше поняття.
Функції першого класу - означає, що ви можете зберігати функції в змінних. Думаю вам доводилося робити щось на зразок цього в JS:
var add = function(a, b){
return a + b
}
Ви зберігаєте функцію, яка отримує a і b, а повертає a + b, в змінну add.
Функції вищого порядку - функції, які можуть повертати або приймати інші функції в якості свого аргументу. Знову ж JS:
document.querySelector('#button')
.addEventListener('click', function(){
alert('yay, i got clicked')
})
або:
var add = function(a){
return function(b){
return a + b
}
}
var add2 = add(2)
add2(3) // => 5
Функції в обох прикладах є функціями вищого порядку, і навіть якщо вам не доводилося писати щось таке, ви могли неодноразово бачити подібний прийом в чужому коді.
Чисті функції - функція, яка не змінює ніякі дані, а просто бере і повертає якісь значення, як наші улюблені функції в математиці. Тобто якщо ви передаєте функції f число 2, а вона повертає вам 10, це означає, що вона повертає 10 завжди в подібній ситуації. Результати, які видає така функція, ніяк не залежать від оточення або порядку обчислень. Також використання чистих функцій не тягне за собою побічних ефектів в різних частинах програми, тому це дуже потужний інструмент.
Замикання - це, коли ви зберігаєте деякі дані у функції і робите їх доступними тільки для особливої функції повернення, тобто функція повернення зберігає свою середу виконання:
var add = function(a){
return function(b){
return a + b
}
}
var add2 = add(2)
add2(3) // => 5
Тепер поверніться до другого прикладу функцій вищого порядку. Змінна a замкнута і доступна тільки для функції повернення. Взагалі-то замикання це не особливість ФП і використовується для оптимізації роботи програм.
Незмінний стан - це, коли ви не можете змінити жоден зі станів (навіть якщо можете задати новий). У наступному прикладі (мова - OCaml), x та 5 еквівалентні і взаємно замінні в будь-якій частині програми - x завжди дорівнює 5-ти.
let x = 5;;
x = 6;;
print_int x;; (* prints 5 *)
Нічого особливого, правда? Ви здивуєтеся, але я скажу, що це неодноразово врятує вам життя.
ООП більше нас не врятує
Довгоочікуваний час, коли програми виконуються одночасно на безлічі машин, настав. Але ми відстаємо. Те, як ми вирішуємо питання паралелізму і розподілених обчислень, лише додає складності програмуванню. Щоб виправити ситуацію потрібен більш простий і надійний спосіб, ніж парадигма ООП. Ще не забули ключові поняття ФП, які ми розглянули трохи раніше? Чисті функції, незмінне стан, ось це все? Чудово. А що якщо я скажу вам, що не обов'язково обробляти тисячі результатів, запускаючи одну і ту ж саму функцію на тисячі ядер? Щоб зробити теж саме з допомогою ФП достатньо одного. Життя вже не буде таким, як раніше.
Чому не ООП?
ООП не може змагатися з ФП в підході до реалізації паралелізму і розподілених обчислень, тому що весь ООП ґрунтується на понятті змінності стану (взагалі-то це особливість імперативних мов, але суть в тому, що переважна більшість з них є об'єктно-орієнтованими). Справа в об'єктних методах. Проблема виникає, коли від одного і того ж методу вимагають синхронності виконання на безлічі ядер, що в підсумку виливається в неосяжні потоки додаткового коду, а це аж ніяк не додає простоти або гнучкості. Я не намагаюся переманити вас на сторону ФП (хоча дехто це робить), але вдумайтеся: Java і С ++ 11 вже товаришують з Лямба-обчисленням. Тобто мейнстрімові мови вже починають переодягатися в ФП і я гарантую, що скоро до них підключаться їх менш популярні брати. Важливо відзначити, що вам не доведеться відмовлятися від змінних станів, головна ідея ФП полягає в тому, щоб використовувати їх тільки, коли це дійсно необхідно.
Я не працюю з хмарами, чи потрібен мені ваш ФП?
Так. Ви ж хочете писати краще, а проблеми вирішувати простіше?
Я намагався, але вийшло якось занадто складно і нечитабельно.
Спочатку все важко. Думаю, вам доводилося опанувавши однією мовою, братися за іншу і ви з усіх сил намагалися в ній розібратися. Швидше за все це були об'єктно-орієнтовані мови, так що перед початком вивчення нової ви вже знали основні ідіоми, прийоми і володіли розумінням того, що таке умова, а що таке цикл. Вивчення ФП - це вивчення програмування з нуля, з азів. Часто доводиться чути, що код, який написаний на мові ФП, важко читати. Якщо ви тільки й робили, що програмували на імперативних мовах, то код на ФП вам здасться шифрограмою. Але справа не в тому, що там все зашифровано від гріха подалі, а в тому, що ви просто не знаєте основних ідіом. Що ж спробуйте дізнатися, а потім подивитися на той же код знову. Подивіться на код однієї і тієї ж програми, написані на Haskell та JS в стилі імперативного програмування:
guess :: Int -> [Char]
guess 7 = "Much 7 very wow."
guess x = "Ooops, try again."
function guess(x){
if(x == 7){
return "Much 7 very wow."
}
else {
return "Oops, try again."
}
}
Це проста програма, яка видає вітальне повідомлення, якщо користувач вводить цифру 7 або видає помилку в іншому випадку. Дивно, але для Haskell достатньо 2 рядки (не рахуючи першого - це анотація до типів). Ви зможете так само, якщо розберетеся з таким поняттям як "зіставлення зі зразком", яке, до речі, не є особливістю ФП, але саме в ньому використовується повсюди. Що робить Haskell в коді вище: О, схоже у нас тут сімка, що ж привітаю користувача з його маленькою перемогою, чого б я не зробив, якби це була не сімка. Взагалі так само думає і JS, але Haskell працює методом порівняння з патернами, які надав програміст. Переваги такого методу перед традиційними if-else рішеннями розкриваються у всій красі, коли виникає необхідність працювати з великими обсягами структурованих даних.
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, використовуючи ООП, в два рядки і зберегти той же рівень читабельності.
Як розпочати?
У мережі купа контенту присвяченого ФП, але ось кілька ресурсів, які ви не повинні пропустити:
- Principles of Functional Programming in Scala
- Introduction to Functional Programming (Contents)
- Paradigms of Computer Programming — Fundamentals
На жаль, курси починаються під кінець року, але ви можете пошукати відео на Youtube. Якщо ж ви віддаєте перевагу тексту, то можу порекомендувати вам:
- Structure and Interpretation of Computer Programs
- How to Design Programs
- Concepts, Techniques, and Models of Computer Programming Бажаю успіхів і щасливого функціонального року.
Ще немає коментарів