Функціональне програмування має стати нашим головним пріоритетом в 2015 році

10 хв. читання

Можливо вам доводилося чути такі слова як "Clojure", "Scala" або "Erlang", а може навіть фрази на кшталт "В Java тепер з'явилися лямбда-функції". І можливо ви навіть знаєте, що все це пов'язано з якимось функціональним програмуванням. Якщо ви є частим відвідувачем будь-яких програмістських спільнот, то ці теми повинні були підніматися останнім часом все частіше. Однак, якщо ви введете в гуглі "функціональне програмування", то не побачите нічого нового. Швидше за все більшість результатів так чи інакше будуть пов'язані з Lisp - мовою, яку винайшли аж у 50-их роках минулого століття. Чому ж інтерес до функціонального програмування став виявлятися тільки останнім часом, 60 років потому?

Давним-давно, коли комп'ютери були дуже повільними ...

...навіть повільнішими, ніж їх власні жорсткі диски, існувало два основних погляди на те, яким має бути внутрішній устрій комп'ютера і якою має бути мова програмування:

  1. взяти архітектуру Фон Неймана і додати абстрактність;
  2. взяти математику і прибрати абстрактність

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

Як розпочати?

У мережі купа контенту присвяченого ФП, але ось кілька ресурсів, які ви не повинні пропустити:

  1. Principles of Functional Programming in Scala
  2. Introduction to Functional Programming (Contents)
  3. Paradigms of Computer Programming — Fundamentals

На жаль, курси починаються під кінець року, але ви можете пошукати відео на Youtube. Якщо ж ви віддаєте перевагу тексту, то можу порекомендувати вам:

  1. Structure and Interpretation of Computer Programs
  2. How to Design Programs
  3. Concepts, Techniques, and Models of Computer Programming Бажаю успіхів і щасливого функціонального року.
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація