Състав на къри и функция

Димни арт кубчета за пушене - MattysFlicks - (CC BY 2.0)
Забележка: Това е част от серията „Compose Software“ (сега книга!) За изучаване на функционални програми и техники за композиционен софтуер в JavaScriptES6 + от самото начало. Продължавайте да се настройвате. Има много повече от това, което предстои!
Купете книгата | Индекс | <Предишна | Напред>

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

Какво е крива функция?

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

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

// add = a => b => Число
const add = a => b => a + b;

За да го използваме, трябва да приложим двете функции, използвайки синтаксиса на приложението за функция. В JavaScript скобите () след препратката към функцията задействат извикване на функция. Когато функция върне друга функция, върнатата функция може да бъде незабавно извикана чрез добавяне на допълнителен набор от скоби:

const резултат = добавете (2) (3); // => 5

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

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

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

Какво е частично приложение?

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

Каква е разликата?

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

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

Унарното изискване за кривите функции е важна характеристика.

Какво е точков стил?

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

функция foo (/ * параметрите са декларирани тук * /) {
  // ...
}
const foo = (/ * параметрите са декларирани тук * /) => // ...
const foo = функция (/ * параметрите са декларирани тук * /) {
  // ...
}

Как можете да дефинирате функциите в JavaScript, без да посочвате необходимите параметри? Е, не можем да използваме keykey функцията и не можем да използваме функция със стрелка (=>), защото те изискват деклариране на всички формални параметри (което би посочило нейните аргументи). Така че това, което ще трябва да направим вместо това, е да извикаме функция, която връща функция.

Създайте функция, която увеличава каквото и число да минете към нея от един, използвайки стил без точка. Не забравяйте, че вече имаме функция, наречена add, която приема число и връща частично приложена функция с първия си параметър, фиксиран към всичко, което предавате. Можем да я използваме, за да създадем нова функция, наречена inc ():

// inc = n => Число
// Добавя 1 към произволно число.
const inc = добавяне (1);
Inc (3); // => 4

Това става интересно като механизъм за обобщение и специализация. Върнатата функция е само специализирана версия на по-общата функция add (). Можем да използваме add (), за да създадем колкото се може повече специализирани версии:

const inc10 = добавяне (10);
const inc20 = добавяне (20);
inc10 (3); // => 13
inc20 (3); // => 23

И разбира се, всички те имат свои собствени обхвати за затваряне (затварянията се създават по време на създаване на функция - когато се добави add ()), така че оригиналният inc () продължава да работи:

inc (3) // 4

Когато създаваме inc () с функцията add call (1), параметърът add () се фиксира на 1 вътре в върнатата функция, която се присвоява на inc.

Тогава, когато извикваме inc (3), b параметърът add () се заменя със стойността на аргумента 3 и приложението завършва, връщайки сумата от 1 и 3.

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

Защо кърим?

Извитите функции са особено полезни в контекста на състава на функциите.

В алгебрата, имайки две функции, g и f:

g: a -> b
f: b -> c

Можете да съставите тези функции заедно, за да създадете нова функция, h от директно до c:

// Определение на алгебрата, заимстване на оператора на композиция `.`
// от Haskell
h: a -> c
h = f. g = f (g (x))

В JavaScript:

const g = n => n + 1;
const f = n => n * 2;
const h = x => f (g (x));
ч (20); // => 42

Дефиницията на алгебрата:

е. g = f (g (x))

Може да се преведе на JavaScript:

const compose = (f, g) => x => f (g (x));

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

е. ж. з

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

Ето начина, по който обикновено го пиша:

const compose = (... fns) => x => fns.reduceRight ((y, f) => f (y), x);

Тази версия приема произволен брой функции и връща функция, която приема първоначалната стойност, а след това използва ReduRight (), за да повтори отдясно наляво над всяка функция, f, във fns, и да я приложи на свой ред към натрупаната стойност, y , Това, което ние натрупваме с акумулатора, y в тази функция е връщащата стойност за функцията, върната от compose ().

Сега можем да напишем нашия състав така:

const g = n => n + 1;
const f = n => n * 2;
// заменете `x => f (g (x))` с `compose (f, g)`
const h = съставяне (f, g);
ч (20); // => 42

следа

Композицията на функциите, използваща стила без точка, създава много сбит, четим код, но може да дойде с цената на лесното отстраняване на грешки. Ами ако искате да проверите стойностите между функциите? trace () е удобна програма, която ще ви позволи да направите точно това. Приема формата на извита функция:

const track = label => value => {
  console.log (`$ {label}: $ {value}`);
  възвръщаема стойност;
};

Сега можем да проверим тръбопровода:

const compose = (... fns) => x => fns.reduceRight ((y, f) => f (y), x);
const track = label => value => {
  console.log (`$ {label}: $ {value}`);
  възвръщаема стойност;
};
const g = n => n + 1;
const f = n => n * 2;
/ *
Забележка: Поръчката за приложение за функция е
дъното до върха:
* /
const h = съставяне (
  следа („след f“),
  F,
  следа („след g“),
  г
);
ч (20);
/ *
след g: 21
след е: 42
* /

compose () е чудесна полезна програма, но когато трябва да съставим повече от две функции, понякога е полезно, ако можем да ги четем в ред отгоре-надолу. Ние можем да направим това, като променим реда на извикване на функциите. Има още една помощна програма, наречена pipe (), която съставя в обратен ред:

const тръба = (... fns) => x => fns.reduce ((y, f) => f (y), x);

Сега можем да напишем горния код по този начин:

const тръба = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const track = label => value => {
  console.log (`$ {label}: $ {value}`);
  възвръщаема стойност;
};
const g = n => n + 1;
const f = n => n * 2;
/ *
Сега заповедта за приложение на функцията
работи отгоре надолу:
* /
const h = тръба (
  д,
  следа („след g“),
  F,
  следа („след f“),
);
ч (20);
/ *
след g: 21
след е: 42
* /

Състав на къри и функция, заедно

Дори и извън контекста на състава на функциите, кърингът със сигурност е полезна абстракция, която можем да използваме за специализиране на функциите. Например, съкратена версия на map () може да бъде специализирана за извършване на много различни неща:

const map = fn => mappable => mappable.map (fn);
const тръба = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const log = (... args) => console.log (... args);
const arr = [1, 2, 3, 4];
const isEven = n => n% 2 === 0;
const stripe = n => isEven (n)? 'тъмна светлина';
const stripeAll = карта (райе);
const райе = stripeAll (arr);
влизане (ивици);
// => ["светло", "тъмно", "светло", "тъмно"]
const двойно = n => n * 2;
const doubleAll = карта (двойна);
const удвоено = doubleAll (arr);
влезте (удвоява);
// => [2, 4, 6, 8]

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

f: a => b
g: b => c
h: a => c

Ако функцията g над очакваните два параметъра, изходът от f няма да съвпада с входа за g:

f: a => b
g: (x, b) => c
h: a => c

Как да вмъкнем x в g в този сценарий? Обикновено отговорът е да къри g.

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

Ключовите думи в това определение са "една по една". Причината, че кривите функции са толкова удобни за състава на функцията е, че те трансформират функции, които очакват множество параметри, във функции, които могат да вземат един аргумент, позволявайки им да се поберат в тръбопровода за функционална композиция. Вземете като пример функцията trace () от по-рано:

const тръба = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const track = label => value => {
  console.log (`$ {label}: $ {value}`);
  възвръщаема стойност;
};
const g = n => n + 1;
const f = n => n * 2;
const h = тръба (
  д,
  следа („след g“),
  F,
  следа („след f“),
);
ч (20);
/ *
след g: 21
след е: 42
* /

trace () определя два параметъра, но ги взема един по един, което ни позволява да специализираме функцията inline. Ако trace () не беше изкривен, не бихме могли да го използваме по този начин. Ще трябва да напишем тръбопровода така:

const тръба = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const trace = (етикет, стойност) => {
  console.log (`$ {label}: $ {value}`);
  възвръщаема стойност;
};
const g = n => n + 1;
const f = n => n * 2;
const h = тръба (
  д,
  // обажданията trace () вече не са безсмислени,
  // въвеждане на променливата посредник, `x`.
  x => следа ('след g', x),
  F,
  x => следа ('след f', x),
);
ч (20);

Но просто извиването на функция не е достатъчно. Също така трябва да се уверите, че функцията очаква параметрите в правилния ред, за да ги специализирате. Вижте какво ще се случи, ако отново прекъснем trace (), но обърнете реда на параметрите:

const тръба = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const track = value => label => {
  console.log (`$ {label}: $ {value}`);
  възвръщаема стойност;
};
const g = n => n + 1;
const f = n => n * 2;
const h = тръба (
  д,
  // обажданията trace () не могат да бъдат безсмислени,
  // защото се очакват аргументи в грешен ред.
  x => следа (x) ('след g'),
  F,
  x => следа (x) ('след f'),
);
ч (20);

Ако сте в щипка, можете да отстраните проблема с функция, наречена flip (), която просто завърта реда на два параметъра:

const flip = fn => a => b => fn (b) (a);

Сега можем да създадем функция flippedTrace ():

const flippedTrace = флип (следа);

И го използвайте по този начин:

const flip = fn => a => b => fn (b) (a);
const тръба = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const track = value => label => {
  console.log (`$ {label}: $ {value}`);
  възвръщаема стойност;
};
const flippedTrace = флип (следа);
const g = n => n + 1;
const f = n => n * 2;
const h = тръба (
  д,
  flippedTrace ('след g'),
  F,
  flippedTrace ('след f'),
);
ч (20);

Но по-добър подход е да напишете функцията правилно на първо място. Стилът понякога се нарича „data last“, което означава, че първо трябва да вземете специализираните параметри и да вземете данните, функцията ще действа последно. Това ни дава оригиналната форма на функцията:

const track = label => value => {
  console.log (`$ {label}: $ {value}`);
  възвръщаема стойност;
};

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

const track = label => value => {
  console.log (`$ {label}: $ {value}`);
  възвръщаема стойност;
};
const traceAfterG = trace ('след g');

… Е еквивалентно на това:

const traceAfterG = value => {
  const label = 'след g';
  console.log (`$ {label}: $ {value}`);
  възвръщаема стойност;
};

Ако сменихме trace ('след g') за traceAfterG, това ще означава същото:

const тръба = (... fns) => x => fns.reduce ((y, f) => f (y), x);
const track = label => value => {
  console.log (`$ {label}: $ {value}`);
  възвръщаема стойност;
};
// Изкривената версия на trace ()
// спестява ни да напишем целия този код ...
const traceAfterG = value => {
  const label = 'след g';
  console.log (`$ {label}: $ {value}`);
  възвръщаема стойност;
};
const g = n => n + 1;
const f = n => n * 2;
const h = тръба (
  д,
  traceAfterG,
  F,
  следа („след f“),
);
ч (20);

заключение

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

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

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

Извитите функции са чудесни за състава на функциите, тъй като ви позволяват лесно да преобразувате n-ary функция във форма на унарната функция, необходима за тръбопроводите за състава на функциите: Функциите в тръбопровода трябва да очакват точно един аргумент.

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

Купете книгата | Индекс | <Предишна | Напред>

Научете повече на EricElliottJS.com

Видео уроци с предизвикателства с интерактивен код са на разположение за членове на EricElliottJS.com. Ако не сте член, регистрирайте се днес.

Ерик Елиът е експерт по разпределени системи и автор на книгите „Съставяне на софтуер“ и „Програмиране на JavaScript приложения“. Като съосновател на DevAnywhere.io, той обучава разработчиците на уменията, от които се нуждаят, за да работят отдалечено и да приемат баланс между работа и живот. Той създава и съветва екипи за разработка на крипто проекти и допринася за софтуерни преживявания за Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC и най-добрите звукозаписни артисти, включително Usher, Frank Ocean, Metallica и много други.

Той се радва на отдалечен начин на живот с най-красивата жена в света.