FP и JavaScript

Абсолютно обичам функционалното програмиране (FP). За мен това е като Vim — след като се ангажирам да го науча, ми е трудно да си представя живота по различен начин. Много хора обаче възприемат гледна точка „всичко или нищо“, която може да бъде както объркваща, така и непосилна за начинаещия.

Исторически има два лагера, на които FP общността се е разделила. Чисти езици: като Miranda и Haskell, които са чисти реализации на ламбда смятане; инечисти езици: като Scheme и Standard ML, които допълват ламбда смятането с различни ефекти, включително присвояване, изключения и продължения (Walder 1995). Това може да звучи объркващо или дори богохулно, ако сте запознати с FP; но бих отдал това до голяма степен на начина, по който обикновено се преподава FP.

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

В крайна сметка езиците за програмиране са човешката семантика на компютърната логика и FP е избор, който програмистът прави и може да се разшири в цялото приложение или за толкова малка част от приложението, колкото искате, когато работите с нечисти езици. Да се ​​твърди, че FP може да се направи правилно само с чисти езици, е като да се твърди, че една машина може да бъде правилно изградена само с метрични гайки и болтове.

Това не означава, че някои езици няма да превърнат живота ви в ад, ако се опитате да ги пишете по функционални начини, или че някои няма да имат директни ограничения, които няма да можете да избегнете. Не бива обаче да изхвърляме бебето заедно с водата за баня, а JavaScript (който е базиран на Scheme) е страхотен език за изучаване на концепции на FP, ако вече сте средно напреднал JavaScript разработчик. Всъщност JavaScript може да предложи някои предимства в ефективността, с които някои чисти FP езици могат да се борят. Например писането на композиции без точки в JavaScript е детска игра, но в някои чисти FP езици това може да бъде толкова трудно, колкото да осмислиш Одисей на гърба на механичен бик, завъртян до нелепа скорост. Достатъчно е да се каже, че JavaScript е отличен език за функционално писане и ако сте хейтър, добре, това е ваш прерогатив.

Преосмисляне на комуналните услуги

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

Много разработчици на JavaScript ще започнат всеки нов проект, като инсталират библиотека като Lodash, за да имат удобен набор от помощни програми, написани, тествани и готови да излязат от кутията. След като даден проект започне да се усложнява, обичайно е да се използват тези управлявани от библиотеки помощни програми при създаването на нови помощни програми за обработка на общи, но специфични за проекта функции (напр. getUserEmail или isApplicationValid и т.н.).

Lodash е страхотна библиотека, но проблемът е, че всички функции имат своите параметри наобратно. Например, map приема списък като свой първи аргумент и функция като втори, но винаги е гарантирано, че функцията е известна преди списъка, с който ще оперира, тъй като функцията трябва да бъде дефинирана преди изпълнението на програмата - по този начин ни предпазва от използване на частично приложение. Така че, ако сте заключени в проект, който използва Lodashили друга библиотека, която също използва обратни параметри, може да се наложи да си направите файл с FP-съвместими еквиваленти. След време Ramda е алтернативна библиотека, която може да предостави почти същите готови за работа помощни програми, но написани с параметрите в правилния ред. Засега бих насърчил да не разчитате прекалено на Ramda, докато не се почувствате супер уверени как кърито и композицията работят на заден план за вас.

С това нека да започнем. Кодът и упражненията са достъпни в това repo.

Наборът с инструменти за начинаещи

За да започнете да пренареждате този свой императивен мозък, приемете и овладейте тези три функции:

  • Къри
  • Композиране и/или глас
  • Карта

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

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

Къри

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

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

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

„Защо“ да къри има просто и не толкова просто обяснение. Простото обяснение е, че понякога все още не знаете всички аргументи. Не толкова простото обяснение е, че FP моделите са майсторски в повторното използване и абстрахирането на код чрез използването на частично приложение и комбинаторна логика. След време това ще има повече смисъл, но засега не е нужно да разбирате как работи едно бутало, просто трябва да натиснете педала на газта и да отидете в стаята.

Обикновено ще видите curry да обвива дефиниция на функция (т.е. ред 6), но можете също така просто да вземете всяка функция, която искате, и да я предадете на curry (т.е. ред 2) и готово, имате функция, която може да бъде нарязана на кубчета и разделена в множество функции. Можем да извикаме add(2), която връща нова функция, която добавя 2 към аргумент, и след това да извикаме тази функция с add2(1), която връща 3.

Обектите са също толкова важни за FP, колкото и OO, така че едно изключително полезно приложение на curry е с функцията prop:

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

Предложена практика:

  • изваждане, деление, умножение, повдигане на квадрат (математически неща)
  • getPropertyPerson: взема свойство като първи аргумент и лице като втори аргумент и връща свойството от този обект човек.
  • getPersonProperty: същото като по-горе, но приема обекта person като първи параметър и името на свойството като второ. Поиграйте си как това променя резултата от частичното му прилагане с обекти вместо свойства.

В много функционални езици всички функции се подреждат автоматично по подразбиране, което е страхотно, но ако все още не разбирате как работи, може да бъде и супер объркващо. Забавен факт е, че кърито е кръстено на „Haskell Curry“, където Haskell получава и своя съименник. Всъщност създателите на Haskell гласуваха да нарекат езика „Къри“, но поради популярността на актьора Тим Къри по онова време, те спаха върху него и решиха да го кръстят Haskell вместо това.

Когато се чувствате комфортно с функциите за къри, преминете към Compose / Pipe.

Съставяне / тръбопровод

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

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

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

Тук add15 е една единствена функция, която е комбинация от 5 функции за добавяне, които ще се задействат последователно отдясно наляво (или отдолу нагоре) и е еквивалент на предаване функциите като аргументи една към друга на ред 11. Освен ако не сте извънземен, писането на вложени извиквания на функции е доста ужасно за четене, но се надяваме, че дава известна представа за това как работи композирането. Pipe е абсолютно същият, но ще се изпълнява отляво надясно/отгоре надолу:

Compose / pipe може да бъде пословично трудно за начинаещите да отстраняват грешки, защото е трудно да се конзолират различни стъпки от процеса. Една ефективна стратегия за отстраняване на грешки в съставени функции е поставянето на оператори на журнал в извикваните функции. Това работи много добре, ако функцията не се извиква толкова често, но ако се извиква стотици пъти над списък с данни, тогава ще ни остане да се опитваме да разберем кой оператор на журнала е уместният виновник за срив по време на изпълнение. Алтернативата е използването на функция trace:

trace ще ни позволи да проверим резултата между всяко домино:

Това ще произведе следния изход при извикване с add15(5):

Поиграйте си с него, съставете/изведете някои къри изваждания, деления, умножения и също така хвърлете някои обекти в микса:

Най-важната концепция, която ще се появи, докато играете с curry и композирате, е, че по същество аргументът „неизвестен“ ще бъде данните, върху които ще работи функцията. Разглеждайки printAge по-горе, човекът е неизвестните данни и така нашият състав е настроен да получава данните като аргумент, който преобръща първото домино. Това ще стане по-естествено, докато играете с map, но друг начин да мислите за това е, че данните, които ще дойдат от DB или API, обикновено ще бъдат дефинираният параметър последно.

Карта

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

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

Да кажем, че работим с API или DB и ще ни бъде върнат списък с потребители:

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

Вградена карта на JavaScript

Тъй катоprop('name') връща частично приложена функция, която взема само един обект като оставащ аргумент, сега можем да трансформираме нашите потребители от масив от потребители в масив от низове. Това е еквивалентно на:

users.map(user => user.name);

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

const map = curry((f, list) => list.map(f));

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

Ето една по-сложна трансформация, която ще използва две общи помощни програми, split и head:

Не само, че използваме повторно код по-лесно, но нашият код вече е декларативен вместо императивен — което означава, че кодът изразява това, което прави на обикновен език и те са толкова лесни за игра, колкото и парчетата на Lego.

Сега не се обезсърчавайте, ако имате функция, която трябва да създадете, която надхвърля нивото ви на комфорт за изграждане от по-малки парчета. Да кажем, че искам да създам функция, която проверява дали има имейл и ако е налице, връща true, в противен случай false. Ето три различни версии на нещо, което върши работата:

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

В крайна сметка, ако работи, изпратете го – но можем също така да преосмислим логиката на hasEmail. По същество имейлът е низ и така, ако нашата цел е да разберем дали всеки потребител има имейл низ с дължина до него, тогава hasEmail всъщност е комбинацията от други по-малки функции:

Има купища начини за безопасно изграждане на по-сложна логика на потока на управление в по-многократно използваеми FP начини, но това не означава, че оригиналната версия на hasEmail е „лоша“ — всъщност тези малки стъпки ще ви помогнат да започнете пътуването си към монадите, моноидите, функторите, секвенсорите и всички видове кокетни играчки, които са едновременно въображаеми и (най-важното) забавни!

Ако FP в JavaScript е път, който искате да проучите по-нататък, тогава трябва да разгледате феноменалното „„Повечето адекватно ръководство за функционално програмиране на професор Фрисби““. Ако стане разочароващо и объркващо, оставете го и се върнете по-късно. Може да отнеме време, докато FP начините на мислене се затвърдят в мозък, който е бил строго обучен да мисли и пише императивно с цикли и мутация – но за повечето наградата си струва пътуването.

Така че, ако търсите начини да опитате и започнете да използвате FP, разгледайте repo и опитайте да пренапишете някои основни помощни програми, в противен случай продължавайте!