Вступ до основних принципів функціонального програмування

Після довгого навчання та роботи з об’єктно-орієнтованим програмуванням я зробив крок назад, задумавшись про складність системи.

" Complexity is anything that makes software hard to understand or to modify." - Джон Оутерхаут

Проводячи деякі дослідження, я знайшов такі концепції функціонального програмування, як незмінність і чиста функція. Ці концепції є великими перевагами для створення функцій без побічних ефектів, тому простіше підтримувати системи - з деякими іншими перевагами.

У цьому дописі я розповім вам більше про функціональне програмування та деякі важливі концепції з великою кількістю прикладів коду.

Ця стаття використовує Clojure як приклад мови програмування для пояснення функціонального програмування. Якщо вам не подобається мова типу LISP, я також опублікував той самий пост у JavaScript. Погляньте: Принципи функціонального програмування в Javascript

Що таке функціональне програмування?

Функціональне програмування - це парадигма програмування - стиль побудови структури та елементів комп’ютерних програм, який розглядає обчислення як оцінку математичних функцій та уникає змінних станів та змінних даних - Wikipedia

Чисті функції

Перше фундаментальне поняття, яке ми вивчаємо, коли хочемо зрозуміти функціональне програмування, - це чисті функції . Але що це насправді означає? Що робить функцію чистою?

То як ми можемо дізнатись, є функція pureчи ні? Ось дуже суворе визначення чистоти:

  • Він повертає той самий результат, якщо дано однакові аргументи (він також називається deterministic)
  • Це не викликає будь-яких помітних побічних ефектів

Він повертає той самий результат, якщо даються однакові аргументи

Уявіть, що ми хочемо реалізувати функцію, яка обчислює площу кола. Нечиста функція отримає radiusяк параметр, а потім обчислить radius * radius * PI. У Clojure оператор стоїть першим, тому radius * radius * PIстає (* radius radius PI):

Чому це нечиста функція? Просто тому, що він використовує глобальний об'єкт, який не був переданий як параметр функції.

А тепер уявіть, деякі математики стверджують, що PIзначення насправді є 42і змінюють значення глобального об'єкта.

Наша нечиста функція тепер отримає 10 * 10 * 42= 4200. Для того самого параметра ( radius = 10) ми маємо інший результат. Давайте це виправимо!

ТА-ДА ?! Тепер ми завжди передаватимемо функцію I значення P як параметр. Отже, ми просто отримуємо доступ до параметрів, переданих функції. Ні external object.

  • Для параметрів radius = 10& PI = 3.14ми завжди матимемо однаковий результат:314.0
  • Для параметрів radius = 10& PI = 42ми завжди матимемо однаковий результат:4200

Читання файлів

Якщо наша функція читає зовнішні файли, це не є чистою функцією - вміст файлу може змінюватися.

Генерація випадкових чисел

Будь-яка функція, яка покладається на генератор випадкових чисел, не може бути чистою.

Це не викликає будь-яких помітних побічних ефектів

Приклади спостережуваних побічних ефектів включають модифікацію глобального об'єкта або параметра, переданого за посиланням.

Тепер ми хочемо реалізувати функцію для отримання цілочисельного значення і повернення значення, збільшеного на 1.

Ми маємо counterцінність. Наша нечиста функція отримує це значення і повторно призначає лічильник зі значенням, збільшеним на 1.

Спостереження : у функціональному програмуванні не рекомендується змінювати.

Ми модифікуємо глобальний об’єкт. Але як би ми це зробили pure? Просто поверніть значення, збільшене на 1. Як просто.

Зверніть увагу, що наша чиста функція increase-counterповертає 2, але counterзначення все одно. Функція повертає збільшене значення, не змінюючи значення змінної.

Якщо ми дотримуємося цих двох простих правил, стає легше зрозуміти наші програми. Тепер кожна функція ізольована і не може впливати на інші частини нашої системи.

Чисті функції є стабільними, послідовними та передбачуваними. Враховуючи однакові параметри, чисті функції завжди повертатимуть однаковий результат. Нам не потрібно думати про ситуації, коли один і той же параметр має різні результати - адже цього ніколи не станеться.

Переваги чистих функцій

Код, безумовно, легше перевірити. Нам не потрібно нічого глузувати. Тож ми можемо одинично тестувати чисті функції з різним контекстом:

  • Даний параметр A→ очікуйте, що функція поверне значенняB
  • Даний параметр C→ очікуйте, що функція поверне значенняD

Простим прикладом може бути функція для отримання колекції чисел і очікування, що вона збільшить кожен елемент цієї колекції.

Ми отримуємо numbersколекцію, використовуємо mapз incфункцією збільшення кожного числа і повертаємо новий список збільшених чисел.

Для input[1 2 3 4 5]очікуваного outputбуде [2 3 4 5 6].

Незмінність

Незмінна з часом або не може бути змінена.

Коли дані незмінні, їх стан не може змінитисяпісля його створення. Якщо ви хочете змінити незмінний об’єкт, ви не можете. Натомість ви створюєте новий об’єкт з новим значенням.

У Javascript ми зазвичай використовуємо forцикл. Це наступне forтвердження містить кілька змінних змінних.

На кожній ітерації ми змінюємо iі sumOfValueстан . Але як ми впораємося із змінністю в ітерації? Рекурсія! Назад до Clojure!

Отже, тут ми маємо sumфункцію, яка отримує вектор числових значень. recurСтрибає назад в , loopпоки ми не отримаємо вектор порожній (наша рекурсія base case). Для кожної "ітерації" ми додамо значення до totalнакопичувача.

З рекурсією ми зберігаємо свої зміннінезмінний.

Спостереження : Так! Ми можемо використовувати reduceдля реалізації цієї функції. Це ми побачимо в Higher Order Functionsтемі.

Також дуже часто формується кінцевий стан об’єкта. Уявіть, що у нас є рядок, і ми хочемо перетворити цей рядок на url slug.

В ООП в Ruby, ми створимо клас, скажімо, UrlSlugify. І цей клас матиме slugify!метод перетворення введеного рядка в a url slug.

Гарний! Це реалізовано! Тут ми маємо імперативне програмування, в якому точно сказано, що ми хочемо робити в кожному slugifyпроцесі - спочатку малими літерами, потім видаляємо непотрібні пробіли і, нарешті, замінюємо залишилися пробіли дефісами.

Але ми мутуємо вхідний стан у цьому процесі.

Ми можемо впоратися з цією мутацією, створивши функціональну композицію або ланцюжок функцій. Іншими словами, результат функції буде використовуватися як вхід для наступної функції, не змінюючи початковий вхідний рядок.

Ось ми маємо:

  • trim: видаляє пробіли з обох кінців рядка
  • lower-case: перетворює рядок у всі малі регістри
  • replace: замінює всі екземпляри збігу на заміну в заданому рядку

Ми поєднуємо всі три функції і можемо використовувати "slugify"наш рядок.

Говорячи про поєднання функцій , ми можемо використовувати compфункцію для складання всіх трьох функцій. Давайте подивимось:

Прозора довідка

Давайте реалізуємо square function:

Ця (чиста) функція завжди матиме однакові вихідні дані, якщо враховувати однакові дані.

Передача “2” як параметра square functionзаповіту завжди повертає 4. Тож тепер ми можемо замінити на (square 2)4. Ось і все! Наша функція полягає referentially transparent.

В основному, якщо функція послідовно дає однаковий результат для одного і того ж введення, вона є посилально прозорою.

чисті функції + незмінні дані = посилальна прозорість

З цією концепцією, круто, що ми можемо зробити, це запам’ятати функцію. Уявіть, у нас є така функція:

(+ 5 8)Так само 13. Ця функція завжди призведе до 13. Отже, ми можемо зробити це:

І цей вираз завжди призведе до 16. Ми можемо замінити весь вираз числовою константою і запам'ятати його.

Функціонує як першокласні сутності

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

У Clojure це звичайно використовувати defnдля визначення функцій, але це просто синтаксичний цукор для (def foo (fn ...)). fnповертає саму функцію. defnповертає a, varякий вказує на об'єкт функції.

Функції як першокласні сутності можуть:

  • посилаються на нього з констант і змінних
  • передавати його як параметр іншим функціям
  • повернути його як результат від інших функцій

Ідея полягає в тому, щоб розглядати функції як значення і передавати такі функції, як дані. Таким чином ми можемо поєднувати різні функції для створення нових функцій з новою поведінкою.

Уявімо, у нас є функція, яка підсумовує два значення, а потім подвоює значення. Щось на зразок цього:

Тепер функція, яка віднімає значення і повертає подвійне:

Ці функції мають схожу логіку, але різниця полягає в функціях операторів. Якщо ми можемо розглядати функції як значення і передавати їх як аргументи, ми можемо створити функцію, яка отримує функцію оператора, і використовувати її всередині нашої функції. Давайте його побудуємо!

Готово! Тепер ми маємо fаргумент, і використовуємо його для обробки aта b. Ми передали функції +і -для складання з double-operatorфункцією та створення нової поведінки.

Функції вищого порядку

Коли ми говоримо про функції вищого порядку, ми маємо на увазі функцію, яка:

  • приймає одну або кілька функцій як аргументи, або
  • повертає функцію як результат

double-operatorФункцією ми реалізовували вище , є функцією вищого порядку , оскільки вона займає операційну функцію в якості аргументу і використовує його.

Ви , напевно , вже чули про filter, mapі reduce. Давайте подивимось на них.

Фільтр

Отримавши колекцію, ми хочемо відфільтрувати за атрибутом. Функція фільтра очікує, що значення a trueабо falseзначення визначатиме, чи повинен елемент включатись у колекцію результатів чи ні. В основному, якщо вираз зворотного виклику є true, функція фільтра включатиме елемент у колекцію результатів. Інакше не буде.

Простий приклад - це коли ми маємо набір цілих чисел, і нам потрібні лише парні числа.

Імперативний підхід

Обов’язковим способом зробити це за допомогою Javascript є:

  • створити порожній вектор evenNumbers
  • ітерація над numbersвектором
  • подати парні числа до evenNumbersвектора

Ми можемо використовувати функцію filterвищого порядку для отримання even?функції та повернення списку парних чисел:

Однією цікавою проблемою, яку я вирішив на шляху Hacker Rank FP, була проблема масиву фільтрів . Ідея проблеми полягає у фільтруванні заданого масиву цілих чисел і виведенні лише тих значень, які менше заданого значення X.

Обов’язкове вирішення цієї проблеми Javascript є приблизно таким:

Ми точно говоримо, що потрібно зробити нашій функції - перебирати колекцію, порівнювати поточний елемент колекції xта resultArrayнадсилати цей елемент до, якщо він передає умову.

Декларативний підхід

Але ми хочемо більш декларативного способу вирішення цієї проблеми, а також використання функції filterвищого порядку.

Декларативне рішення Clojure буде приблизно таким:

Спочатку цей синтаксис здається дещо дивним, але його легко зрозуміти.

#(> x%) - це просто анонімна функція, яка отримує es x, і порівнює її з кожним елементом колекції n. % представляє параметр анонімної функції - у цьому випадку поточний елемент всередині t he filter.

Це ми також можемо зробити за допомогою карт. Уявіть, у нас є карта людей зі своїми nameі age. І ми хочемо відфільтрувати лише людей старше певного віку, у цьому прикладі людей, яким більше 21 року.

Короткий зміст коду:

  • у нас є список людей (з nameі age).
  • маємо анонімну функцію #(< 21 (:age %)). Пам'ятаєте, що t he% представляє поточний елемент із колекції? Що ж, елементом колекції є карта людей. Якщо ми do (:age {:name "TK" :age 26}), то e,в цьому випадку він повертає вікове значення 26.
  • ми фільтруємо всіх людей на основі цієї анонімної функції.

Карта

Ідея карти полягає у перетворенні колекції.

mapМетод перетворює колекцію шляхом застосування функції для всіх її елементів і побудова нової колекції з повертаються значень.

Отримаємо ту саму peopleколекцію вище. Зараз ми не хочемо фільтрувати за категорією "за віком". Ми просто хочемо список рядків, щось на зразок TK is 26 years old. Отже, кінцевий рядок може бути :name is :age years oldде :nameі :ageатрибутами кожного елемента в peopleколекції.

Імперативним способом Javascript це буде:

Декларативним способом Clojure це буде:

Вся ідея полягає в перетворенні даної колекції в нову колекцію.

Ще однією цікавою проблемою Hacker Rank була проблема зі списком оновлення . Ми просто хочемо оновити значення даної колекції з їх абсолютними значеннями.

Наприклад, для вхідного сигналу [1 2 3 -4 5]потрібен вихідний результат [1 2 3 4 5]. Абсолютне значення -4є 4.

Простим рішенням буде оновлення на місці для кожного значення колекції.

Ми використовуємо Math.absфункцію для перетворення значення в його абсолютне значення та виконуємо оновлення на місці.

Це не функціональний спосіб реалізації цього рішення.

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

По-друге, чому б не використовувати mapтут для "перетворення" всіх даних?

Моєю першою ідеєю було побудувати to-absoluteфункцію, яка оброблятиме лише одне значення.

Якщо воно від’ємне, ми хочемо перетворити його в додатне значення (абсолютне значення). В іншому випадку нам не потрібно його трансформувати.

Тепер, коли ми знаємо, як зробити absoluteдля одного значення, ми можемо використовувати цю функцію для передачі як аргумент mapфункції. Ви пам'ятаєте, що a higher order functionможе отримати функцію як аргумент і використовувати її? Так, карта може це зробити!

Ого. Так гарно! ?

Зменшити

Ідея зменшення полягає в отриманні функції та колекції та поверненні значення, створеного комбінуванням елементів.

Типовим прикладом, про який говорять люди, є отримання загальної суми замовлення. Уявіть, що ви були на веб-сайті магазину. Ви додали Product 1, Product 2, Product 3і Product 4до кошику (порядку). Тепер ми хочемо розрахувати загальну суму кошика для покупок.

В обов’язковому порядку ми б повторили список замовлень і підсумували кожну суму товару до загальної суми.

Використовуючи reduce, ми можемо побудувати функцію для обробки amount sumі передати її як аргумент reduceфункції.

Ось ми маємо shopping-cartфункцію, sum-amountяка приймає струм total-amount, і current-productоб’єкт до sumних.

get-total-amountФункція використовується для , використовуючи і , починаючи з .reduceshopping-cartsum-amount0

Інший спосіб отримати загальну суму - скласти mapі reduce. Що я маю на увазі під цим? Ми можемо використовувати, mapщоб перетворити shopping-cartв набір amountзначень, а потім просто використовувати reduceфункцію з +функцією.

get-amountОтримує об'єкт продукту і повертає тільки amountзначення. Отже, що ми маємо тут [10 30 20 60]. А потім reduceоб’єднує всі предмети, складаючи. Гарний!

Ми подивились, як працює кожна функція вищого порядку. Я хочу показати вам приклад того, як ми можемо скласти всі три функції на простому прикладі.

Говорячи про це shopping cart, уявіть, що у нас є такий список продуктів у нашому порядку:

Ми хочемо загальну кількість усіх книг у нашому кошику для покупок. Просто як це. Алгоритм?

  • відфільтрувати за типом книги
  • перетворіть кошик для покупок у колекцію суми за допомогою карти
  • об'єднати всі предмети, склавши їх із зменшити

Готово! ?

Ресурси

Я організував деякі ресурси, які читав і вивчав. Я ділюсь тими, які мені здались справді цікавими. Щоб отримати додаткові ресурси, відвідайте мій репозиторій функціонального програмування Github .

  • Рубінові ресурси
  • Спеціальні ресурси Javascript
  • Специфічні ресурси Clojure

Вступ

  • Навчання FP в JS
  • Вступ до FP з Python
  • Огляд FP
  • Короткий вступ до функціональної JS
  • Що таке FP?
  • Жаргон функціонального програмування

Чисті функції

  • Що таке чиста функція?
  • Чисто функціональне програмування 1
  • Чисто функціональне програмування 2

Незмінні дані

  • Незмінна DS для функціонального програмування
  • Чому спільна змінна держава - корінь усього зла
  • Структурний обмін у Clojure: Частина 1
  • Структурний обмін у Clojure: Частина 2
  • Структурний обмін у Clojure: Частина 3
  • Структурний обмін у Clojure: Заключна частина

Функції вищого порядку

  • Красномовний JS: Функції вищого порядку
  • Функція розваги Fun
  • Fun fun fun function Map
  • Функція розваги fun Basic Basic Reduce
  • Функція розваги Fun Advanced Advanced
  • Функції вищого порядку Clojure
  • Суто функціональний фільтр
  • Чисто функціональна карта
  • Чисто функціональне зменшення

Декларативне програмування

  • Декларативне програмування проти імперативного

Це воно!

Гей, люди, я сподіваюся, вам було цікаво читати цю публікацію, і я сподіваюся, ви тут багато чого дізналися! Це була моя спроба поділитися тим, що я вивчаю.

Ось сховище з усіма кодами з цієї статті.

Приходьте вчитися зі мною. Я ділюсь ресурсами та своїм кодом у цьому сховищі функціонального програмування навчання .

Сподіваюсь, ви побачили тут щось корисне для вас. І до зустрічі наступного разу! :)

Мій Twitter і Github. ☺

TK.