Принципи на функционалното програмиране в Javascript

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

„Сложността е всичко, което прави софтуера трудно разбираем или модифициран.“ - Джон Outerhout

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

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

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

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

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

„Капка вода“ от Мохан Муругесан на Unsplash

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

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

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

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

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

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

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

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

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

Сега винаги ще предаваме стойността на PI като параметър на функцията. Така че сега ние просто имаме достъп до параметри, предадени на функцията. Няма външен обект.

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

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

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

Генериране на произволни числа

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

Не предизвиква наблюдавани странични ефекти

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

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

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

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

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

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

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

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

Ползите от чистите функции

Кодът определено е по-лесен за тестване. Не е нужно да се подиграваме с нищо. Така че можем да единични тестови чисти функции с различен контекст:

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

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

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

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

неизменност

Непроменен във времето или не може да бъде променен.
„Промяна на неонова светлинна табела“ от Рос Фийдън на Unsplash

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

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

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

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

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

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

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

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

Прилага се!

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

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

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

Тук имаме:

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

Ние комбинираме всички тези 4 функции и можем да "слугираме" низа си.

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

„Човек, който държи очила“ от Джош Калабрезе на Unsplash

Да приложим квадратна функция:

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

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

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

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

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

И ние го наричаме с тези параметри:

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

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

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

„Първокласен“ от Андрю Нийл от Unsplash

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

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

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

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

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

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

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

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

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

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

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

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

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

филтър

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

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

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

Императивен начин да го направите с JavaScript е:

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

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

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

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

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

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

Но ние искаме по-декларативен начин за решаване на този проблем и с помощта на филтъра по-висок ред.

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

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

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

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

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

Обобщение на кода:

  • имаме списък с хора (с име и възраст).
  • имаме функция по-стараThan21. В този случай, за всеки човек от масива от хора, ние искаме да получим достъп до възрастта и да видим дали е по-стара от 21 години.
  • филтрираме всички хора въз основа на тази функция.

карта

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

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

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

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

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

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

Друг интересен проблем с Hacker Rank беше проблемът със списъка за актуализиране. Просто искаме да актуализираме стойностите на даден масив с техните абсолютни стойности.

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

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

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

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

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

Второ, защо да не използвате картата тук, за да "преобразувате" всички данни?

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

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

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

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

Намалете

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

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

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

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

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

Функцията getTotalAmount се използва за намаляване на ShoppingCart, като се използва sumAmount и започва от 0.

Друг начин да получите общата сума е да съставите карта и да намалите. Какво искам да кажа с това? Можем да използваме карта, за да трансформираме shoppingCart в колекция от стойности на сумата, а след това просто използваме функцията за намаляване с функцията sumAmount.

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

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

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

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

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

Свършен!

ресурси

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

  • Курс EcmaScript 6 от Уес Бос
  • JavaScript от OneMonth
  • Разтривайте специфични ресурси
  • Специфични ресурси за Javascript
  • Конкретни ресурси

Представянията

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

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

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

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

  • Неизменна DS за функционално програмиране
  • Защо споделеното мутационно състояние е коренът на всяко зло

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

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

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

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

Това е!

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

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

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

Написах и пост на FP, но използвайки основно Clojure.

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

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

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

Моят Twitter & Github.

TK.