Тази публикация в блога е публикувана за първи път в Блогът на TK.

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

“Complexity is anything that makes software hard to understand or to modify.' — Джон Аутърхаут

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

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

Какво е функционално програмиране?

„Функционалното програмиране е програмна парадигма — стил на изграждане на структурата и елементите на компютърните програми — който третира изчислението като оценка на математически функции и избягва променящо се състояние и променливи данни“ — Wikipedia

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

Първата фундаментална концепция, която научаваме, когато искаме да разберем функционалното програмиране, е чистите функции. Но какво всъщност означава това? Какво прави една функция чиста?

Как да разберем дали дадена функция е pure или не? Ето едно много строго определение за чистота:

  • Той връща същия резултат, ако му бъдат дадени същите аргументи (нарича се също като deterministic).
  • Не предизвиква видими странични ефекти.

Същият резултат, ако са дадени същите аргументи

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

Нечиста функция ще получи radius като параметър и след това ще изчисли radius * radius * PI:

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

Сега си представете, че някои математици твърдят, че стойността PI всъщност е 42 и променят стойността на глобалния обект.

Нашата нечиста функция сега ще доведе до 10 * 10 * 42 = 4200. За същия параметър (radius = 10) имаме различен резултат.

Нека го поправим!

Винаги ще предаваме стойността на PI като параметър на функцията. И така, сега имаме достъп само до параметри, предадени на функцията. № external object.

  • За параметрите radius = 10 иPI = 3.14 винаги ще имаме един и същ резултат: 314.0
  • За параметрите radius = 10 иPI = 42 винаги ще имаме един и същ резултат: 4200

Четене на файлове

Ако нашата функция чете външни файлове, това не е чиста функция - съдържанието на файла може да се промени.

Генериране на случайни числа

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

Без видими странични ефекти

Не предизвиква видими странични ефекти. Примери за наблюдавани странични ефекти включват модифициране на глобален обект или параметър, предаван чрез препратка.

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

Имаме стойността counter. Нашата нечиста функция получава тази стойност и присвоява отново counter със стойността, увеличена с 1.

Наблюдение: променливостта не се препоръчва във функционалното програмиране.

Променяме глобалния обект. Но как да го направим pure?

Просто върнете стойността, увеличена с 1.

Вижте, че нашата чиста функция increaseCounter връща 2, но стойността counter е все същата. Функцията връща увеличената стойност, без да променя стойността на променливата.

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

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

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

Предимства на чистата функция

Кодът определено е по-лесен за тестване. Не е нужно да се подиграваме на нищо.

Така че можем да тестваме единици чисти функции с различни контексти:

  • При даден параметър A → очаквайте функцията да върне стойност B
  • При даден параметър C → очаквайте функцията да върне стойност D

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

Получаваме масива numbers, използваме map за увеличаване на всяко число и връщаме нов списък с увеличени числа.

За входа [1, 2, 3, 4, 5], очакваният изход ще бъде [2, 3, 4, 5, 6].

Неизменност

Не се променя във времето или не може да бъде променена.

Когато данните са неизменни, тяхнотосъстояние не може да се променислед като бъдат създадени.

Ако искате да промените неизменен обект, не можете. Вместо товасъздавате нов обект с новата стойност.

В JavaScript обикновено използваме цикъла for. Този следващ for израз има някои променливи променливи.

За всяка итерация променяме състоянието i и sumOfValue. Но как да се справим с променливостта в итерация? Рекурсия.

И така, тук имаме функцията sum, която получава вектор от числови стойности. Функцията се извиква сама, докато не изпразним списъка („базовия ни случай на рекурсия“). За всяка „итерация“ ще добавим стойността към total accumulator.

С рекурсията ние поддържаме нашите променливинеизменни. Променливите list и accumulator не се променят. Запазва същата стойност.

Наблюдение: Можем да използваме reduce, за да реализираме тази функция. Ще разгледаме това в темата за функции от по-висок ред.

Също така е обичайно да се изгради крайното състояние на обект. Представете си, че имаме низ и искаме да трансформираме този низ в url slug.

В обектно-ориентираното програмиране в Ruby ще създадем клас, да кажем, UrlSlugify. И този клас ще има slugify метод за трансформиране на входния низ в url slug.

Приложено е!

Тук имаме императивно програмиране, казващо точно какво искаме да правим във всеки slugify процес — първо с малки букви, след това премахнете безполезните бели интервали и накрая заменете останалите бели интервали с тирета.

Но ние променяме входното състояние в този процес.

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

Тук имаме:

  • toLowerCase: преобразува низа изцяло в малки букви.
  • trim: премахва празното пространство от двата края на низ.
  • split и join: замества всички случаи на съвпадение със замяна в даден низ.

Комбинираме тези четири функции и можем да slugify нашия низ.

Референтна прозрачност

Нека внедрим функция square:

Тази чиста функция винаги ще има един и същ изход при един и същ вход.

Предаването на 2 като параметър на функцията square винаги ще връща 4. Така че сега можем да заменим square(2) с 4. Нашата функция е референтно прозрачна.

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

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

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

И го извикваме с тези параметри:

sum(5, 8) е равно на 13. Тази функция винаги ще води до 13. И така, можем да направим това:

И този израз винаги ще води до 16. Можем да заменим целия израз с числова константа и да го запомним.

Функционира като първокласни субекти

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

Функции като първокласни обекти могат:

  • Обърнете се към него от константи и променливи.
  • Предайте го като параметър на други функции.
  • Върнете го като резултат от други функции.

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

Представете си, че имаме функция, която сумира две стойности и след това удвоява стойността. Нещо като това:

Сега функция, която изважда стойности и след това връща двойното:

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

Сега имаме аргумент f и го използваме за обработка на a и b. Предадохме функциите sum и subtraction, за да композираме с функцията doubleOperator и да създадем ново поведение.

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

Когато говорим за функции от по-висок ред, имаме предвид функция, която или:

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

Функцията doubleOperator, която внедрихме по-горе, е функция от по-висок ред, защото приема операторна функция като аргумент и я използва.

Вероятно вече сте чували за filter, map и reduce. Нека да разгледаме тези.

Филтър

При дадена колекция искаме да филтрираме по атрибут. Филтърната функция очаква стойност true или false, за да определи дали елементът трябва или не трябва да бъде включен в колекцията от резултати.

По принцип, ако изразът за обратно извикване е true, филтърната функция ще включи елемента в колекцията от резултати. В противен случай няма да стане.

Прост пример е, когато имаме колекция от цели числа и искаме само четните числа.

Императивен подход

Задължителен начин да го направите с JavaScript е да:

  • Създайте празен масив evenNumbers.
  • Итерация върху масива numbers.
  • Преместете четните числа в масива evenNumbers.

Можем също да използваме функцията filter от по-висок ред, за да получим функцията even и да върнем списък с четни числа:

Един интересен проблем, който реших по пътя „Hacker Rank FP“, беше „Проблемът с филтърния масив“. Идеята на проблема е да се филтрира даден масив от цели числа и да се изведат само онези стойности, които са по-малки от определена стойност X.

Наложително JavaScript решение на този проблем е нещо като:

Ние казваме какво точно трябва да направи нашата функция — итериране на колекцията, сравняване на текущия елемент на колекцията с x и избутване на този елемент към resultArray, ако премине условието.

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

Но ние искаме по-декларативен начин за решаване на този проблем и искаме да използваме и функцията filter от по-висок ред.

Декларативно решение на JavaScript би било нещо подобно:

Използването на this във функцията smaller първоначално изглежда малко странно, но е лесно за разбиране.

this ще бъде вторият параметър във функцията filter. В този случай 3 (x) е представено от this. Това е.

Можем да направим това и с карти. Представете си, че имаме карта на хора с техните name и age.

И искаме да филтрираме само хора над определена възраст, в този пример хора, които са над 21 години.

Резюме на кода:

  • Имаме списък с хора (с name и age).
  • Имаме функция olderThan21. В този случай за всеки човек в масива хора искаме да получим достъп до age и да видим дали е по-стар от 21.
  • Ние филтрираме всички хора въз основа на тази функция.

Карта

Идеята на функцията map е да трансформира колекция.

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

Нека вземем същата колекция people от по-горе. Сега не искаме да филтрираме по „над възраст“. Ние просто искаме списък от низове; нещо като TK is 26 years old.

И така, крайният низ може да бъде :name is :age years old, където :name и :age са атрибути от всеки елемент в колекцията people.

По наложителен начин на JavaScript би било:

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

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

Друг интересен проблем с HackerRank беше „проблемът със списъка за актуализиране“. Искаме да актуализираме стойностите на даден масив с техните абсолютни стойности.

Например, входът [1, 2, 3, -4, 5] се нуждае от изхода да бъде [1, 2, 3, 4, 5]. Абсолютната стойност на -4 е 4.

Едно просто решение би било актуализация на място за всяка стойност на колекцията.

Използваме функцията Math.abs, за да трансформираме стойността в нейната абсолютна стойност и да направим актуализацията на място.

Това енефункционален начин за прилагане на това решение.

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

Второ, защо не използвате map тук, за да „трансформирате“ всички данни?

Първата ми идея беше да тествам функцията Math.abs да обработва само една стойност.

Искаме да трансформираме всяка стойност в положителна стойност (абсолютната стойност).

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

Спомняте ли си, че функция от по-висок ред може да получи функция като аргумент и да я използва? Да, функцията карта може да го направи!

Еха. Толкова красива!

Намалете

Идеята на функцията reduce е да получи функция и колекция и да върне стойност, създадена чрез комбиниране на елементите.

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

Представете си, че сте в уебсайт за пазаруване. Добавихте Product 1, Product 2, Product 3 и Product 4 към вашата пазарска количка (поръчка). Сега искаме да изчислим общата сума на пазарската количка.

По императивен начин ще повторим списъка с поръчки и ще сумираме сумата на всеки продукт към общата сума.

Използвайки reduce, можем да изградим функция, която да обработва amount sum и да я предадем като аргумент на функцията reduce.

Тук имаме shoppingCart, функцията sumAmount, която получава текущия currentTotalAmount, и обекта order към sum тях.

Функцията getTotalAmount се използва за reduce shoppingCart чрез използване на sumAmount и започване от 0.

Друг начин да получите общата сума е да съставите map и reduce.

Какво имам предвид с това? Можем да използваме map, за да трансформираме shoppingCart в колекция от amount стойности и след това да използваме функцията reduce с функцията sumAmount.

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

Пример за трите функции

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

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

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

  • Филтрирайте по тип книга.
  • Преобразувайте пазарската количка в колекция от amount с помощта на map.
  • Комбинирайте всички елементи, като ги добавите с reduce.

Свършен!

Ресурси

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

въведение

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

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

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

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

Ако искате пълен курс по Javascript, да научите повече умения за кодиране в реалния свят и да създавате проекти, опитайте Едномесечен тренировъчен лагер за Javascript. Ще се видим там ☺

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

„Надявам се, че сте харесали това съдържание. Подкрепете работата ми по Ko-Fi»

Заключение

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

„Ето хранилището с целия код“ от тази статия.

До следващия път!

Моят Twitter и Github. ☺