Въведение в основните принципи на функционалното програмиране

След дълго учене и работа с обектно-ориентирано програмиране направих крачка назад, за да помисля за сложността на системата.

" Complexity is anything that makes software hard to understand or to modify." - Джон Outerhout

Правейки някои изследвания, открих концепции за функционално програмиране като неизменност и чиста функция. Тези концепции са големи предимства за изграждането на функции без странични ефекти, така че е по-лесно да се поддържат системи - с някои други предимства.

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

Тази статия използва 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) имаме различен резултат. Нека да го оправим!

TA-DA ?! Сега винаги ще предаваме I стойността P като параметър на функцията. Така че сега просто имаме достъп до параметри, предадени на функцията. Не еxternal 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.

По принцип, ако дадена функция постоянно дава един и същ резултат за същия вход, тя е референтно прозрачна.

чисти функции + неизменяеми данни = референтна прозрачност

С тази концепция страхотно нещо, което можем да направим, е да запомним функцията. Представете си, че имаме тази функция:

The (+ 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 Path, беше проблемът с филтърния масив . Идеята на проблема е да филтрирате даден масив от цели числа и да изведете само тези стойности, които са по-малки от определена стойност 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 е да трансформира колекция.

В mapметода превръща колекция чрез прилагане на функцията на всички свои елементи и изграждане на нова колекция от върнатите стойности.

Нека вземем същата peopleколекция по-горе. Сега не искаме да филтрираме по „над възрастта“. Ние просто искаме списък с низове, нещо като TK is 26 years old. Така че последният низ може да бъде :name is :age years oldкъде :nameи :ageса атрибути от всеки елемент в peopleколекцията.

По императивен Javascript начин това би било:

По декларативен начин Clojure това би било:

Цялата идея е да се трансформира дадена колекция в нова колекция.

Друг интересен проблем с ранга на хакер е проблемът със списъка с актуализации . Ние просто искаме да актуализираме стойностите на дадена колекция с техните абсолютни стойности.

Например входът се [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функция се използва за reduceпо shopping-cartс помощта на sum-amountи като се излиза от 0.

Друг начин да получите общата сума е да съставите mapи reduce. Какво имам предвид под това? Можем да използваме mapза трансформиране на shopping-cartв колекция от amountстойности и след това просто да използваме reduceфункцията с +функция.

Най get-amountполучава обекта продукт и се връща само amountстойността. И така, това, което имаме тук, е [10 30 20 60]. И тогава reduceкомбинира всички елементи чрез събиране. Красив!

Разгледахме как работи всяка функция от по-висок ред. Искам да ви покажа пример за това как можем да съставим и трите функции в един прост пример.

Говорейки за това shopping cart, представете си, че имаме този списък с продукти в нашата поръчка:

Искаме общата сума на всички книги в нашата количка за пазаруване. Просто като това. Алгоритъмът?

  • филтриране по тип книга
  • трансформирайте пазарската количка в колекция от суми с помощта на карта
  • комбинирайте всички елементи, като ги съберете с намаляване

Свършен! ?

Ресурси

Организирах някои ресурси, които четох и изучавах. Споделям тези, които ми се сториха наистина интересни. За повече ресурси посетете моето хранилище на Github за функционално програмиране .

  • Специфични ресурси за Ruby
  • Специфични ресурси за Javascript
  • Специфични ресурси на Clojure

Въведения

  • Изучаване на FP в JS
  • Въведение в FP с Python
  • Преглед на FP
  • Бързо въведение във функционалния JS
  • Какво е FP?
  • Функционално програмиране Жаргон

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

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

Неизменяеми данни

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

Функции от по-висок ред

  • Красноречив JS: Функции от по-висок ред
  • Забавна забавна функция Филтър
  • Забавна забавна функция Карта
  • Забавна забавна функция Basic Reduce
  • Забавна забавна функция Advanced Reduce
  • Функции на по-висок ред на Clojure
  • Чисто функционален филтър
  • Чисто функционална карта
  • Чисто функционално намаляване

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

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

Това е!

Хей, хора, надявам се да сте се забавлявали да четете тази публикация и дано сте научили много тук! Това беше опитът ми да споделя това, което научавам.

Ето хранилището с всички кодове от тази статия.

Ела да се учиш с мен. Споделям ресурси и кода си в това хранилище за функционално програмиране за обучение .

Надявам се, че тук видяхте нещо полезно за вас. И до нови срещи! :)

Моят Twitter & Github. ☺

TK.