Ось compose
:
const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];
... Не лякайтеся! Це level-9000-super-Saiyan-form від compose. Заради міркувань, давайте відмовимося від варіативної реалізації та розглянемо спрощену форму, яка може компонувати дві функції разом. Як тільки ви це зрозумієте, ви зможете розвинути абстракцію далі і зрозуміти, що це просто працює для будь-якої кількості функцій (ми навіть могли б це довести)! Ось більш дружня compose для вас, мої дорогі читачі:
const compose2 = (f, g) => x => f(g(x));
f
та g
- функції і x
це значення яке "проженеться" через них.
Композиція нагадує функціональне господарство. Ви, заводчик функцій, обрали дві функції з рисами, які ви хотіли б поєднати, щоб створити нову. Використання композиції полягає в наступному:
const toUpperCase = x => x.toUpperCase();
const exclaim = x => `${x}!`;
const shout = compose(exclaim, toUpperCase);
shout('send in the clowns'); // "SEND IN THE CLOWNS!"
Композиція двох функцій повертає нову функцію. У цілком має сенс: поєднання двох одиниць якогось типу (у цьому фипадку функція) має призвести до появи нової одиниці того ж типу. Ви не з'єднуєте дві детальки Lego, щоб отримати Lincoln. Є одна теорія, основний закон, який ми відкриємо свого часу.
У нашому визначенні функції compose
функція g
буде виконана перед функцією f
, утворюючи напрямок предачі даних зправа наліво. Так набагато зручніше читати, ніж вкладання низки викликів функцій. Без compose
попередній код можна зобразити так:
const shout = x => exclaim(toUpperCase(x));
Замість руху зсередини назовні ми рухаємось зправа наліво, що, як мені здається, є кроком у напрямку "на ліво" (Буу!). Давайте розглянемо приклад, де послідовність важлива:
const head = x => x[0];
const reverse = reduce((acc, x) => [x, ...acc], []);
const last = compose(head, reverse);
last(['jumpkick', 'roundhouse', 'uppercut']); // 'uppercut'
Функція reverse
поверне список в зворотньому порядку, в той час як head
витягає лише початковий елемент. Послідовність функцій у цій композиції має бути очевидною. Ми можемо створити версію зліва направо, однак, ми відображаємо математичну версію набагато чіткіше у тому вигляді, в якому вона наведена вище. Правильно, композиція прямо з математичних книг. Насправді, вже, можливо, пора подивитися на властивість, яка зберігається для будь-якої композиції.
// associativity
compose(f, compose(g, h)) === compose(compose(f, g), h);
Композиція - асоціативна. Це значить, що не важливо, як ви поєднаєте дві композиції одна з одною. Тому, якщо ми вирішили перевести строку у верхній регістр, ми можемо написати так:
compose(toUpperCase, compose(head, reverse));
// or
compose(compose(toUpperCase, head), reverse);
А оскільки немає значення, як ми поєднуємо наші виклики compose
- результат буде тим самим. Це дозволяє нам писати різноманітні композиції і використовувати їх, як наприклад ось тут:
// раніше нам потрібно було писати дві композиції, але оскільки композиція асоціативна,
// ми можемо передавати в композицію стільки функцій скільки нам заманеться і дозволяти їй вирішувати як їх групувати.
const arg = ['jumpkick', 'roundhouse', 'uppercut'];
const lastUpper = compose(toUpperCase, head, reverse);
const loudLastUpper = compose(exclaim, toUpperCase, head, reverse);
lastUpper(arg); // 'UPPERCUT'
loudLastUpper(arg); // 'UPPERCUT!'
Застовування асоціативної властивості композиції дає нам гнучкість і впевненість, що результат лишиться однаковим. Трохи сткалніше визначення композиції включено у допоміжні бібліотеки для цієї книги і є звичайним визначенням яке ви зможете зустріти у таких бібліотеках як lodash, underscore, та ramda.
Одна приємна перевага асоціативності це те, що будь-яка група функцій може бути витягнута і згрупована разом у їхню особисту композицію. Давайте трохи пограємось з переробкою нашого попереднього прикладу:
const loudLastUpper = compose(exclaim, toUpperCase, head, reverse);
// -- або ---------------------------------------------------------------
const last = compose(head, reverse);
const loudLastUpper = compose(exclaim, toUpperCase, last);
// -- або ---------------------------------------------------------------
const last = compose(head, reverse);
const angry = compose(exclaim, toUpperCase);
const loudLastUpper = compose(angry, last);
// більше варіацій...
Тут немає ні правильних ні хибних відповідей - ми просто поєднуємо наші детальки lego таким чином, яким нам хочеться. Зазвичай, найкраще групувати речі таким чином, щоб їх можна було перевикористати в подальшому, наприклад last
і angry
. Хто знайомий з книгою Фаулера(пер.: Fowler) "Refactoring", той може впізнати у цьому процесі "функція вилучення"...окрім того, що не потрібно хвилюватись про стан програми.
Безточечний стиль означає - ніколи не потрібно повідомляти ваші дані. Перепрошую. Це означає, що функції ніколи не згадують дані над якими вони працюють. Функції першого класу, каррування та композиція, співпрацюють над створенням цього стилю.
Підказка: Безточкові версії
replace
таtoLowerCase
зазначені в Додатку C - Безточкові Утиліти. Не соромтеся заглянути!
// не безточечна, бо ми згадуємо дані: word
const snakeCase = word => word.toLowerCase().replace(/\s+/ig, '_');
// безточечна
const snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
Бачите як ми частково застосували replace
? Що ми робимо, так це проганяємо наші дані через кожну функцію з одного аргументу. Каррування дозволяє нам підготувати кожну функцію лише на прийом її даних, виконання певних маніпуляцій над ними та їх передачу далі. Ще на що варто звернути увагу - це те, що нам не потрібні дані, щоб побудувати нашу функцію у безточечній версії, в той час як в точечній версії ми повинні мати наші дані(word
) перед тим, як почати щось робити.
Давайте розглянемо інший приклад.
// не безточечно, бо ми згадуємо дані: name
const initials = name => name.split(' ').map(compose(toUpperCase, head)).join('. ');
// безточечно
// УВАГА: ми використовуємо 'intercalate' з додатка замість 'join', який був представлений у Розділі 09!
const initials = compose(intercalate('. '), map(compose(toUpperCase, head)), split(' '));
initials('hunter stockton thompson'); // 'H. S. T'
Безточечний код, знову ж таки, може допомогти нам прибрати непотрібні імена та бути нам більш лаконічними та загальними. Безточечність - це гарний лакмусовий папірець, щоб перевіриьти код на наявність функціонального підходу, оскільки це дозволяє нам знати, що у нас є невеликі функції, які перетворюють вхідні величини на вихідні. Наприклад, не можливо побудувати композицію з while
циклом. Однак, будьте обачні, безточечність - це лезо з твома загостреними сторонами, які можуть ввести в оману. Не весь функціональний код безточечний, і це абсолютно нормально. Проте ми намагатимемось використовувати безточечність усюди де тільки можливо, а де не зможемо - використовуватимемо звичайні функції.
Звичайнісінька помилка робити композицію з чимось як от map
, функцією двох аргументів, без попереднього часткового застосування.
// невірно - ми в кінцевому результаті передаємо масив і частково застосовуємо map з Бог зна чим.
const latin = compose(map, angry, reverse);
latin(['frog', 'eyes']); // error
// вірно - кожна функція очікує на один аргумент.
const latin = compose(map(angry), reverse);
latin(['frog', 'eyes']); // ['EYES!', 'FROG!'])
Якщо у вас виникли складнощі з відлагодженням(пер.: debugging) композиції, ми можемо скористатись цією допоміжною, але нечистою функцією відстеження, щоб побачити, що відбувається.
const trace = curry((tag, x) => {
console.log(tag, x);
return x;
});
const dasherize = compose(
intercalate('-'),
toLower,
split(' '),
replace(/\s{2,}/ig, ' '),
);
dasherize('The world is a vampire');
// TypeError: Cannot read property 'apply' of undefined
Тут щось не так, давайте відслідкуємо за допомогою trace
const dasherize = compose(
intercalate('-'),
toLower,
trace('after split'),
split(' '),
replace(/\s{2,}/ig, ' '),
);
dasherize('The world is a vampire');
// після розподілу [ 'The', 'world', 'is', 'a', 'vampire' ]
Ааа! Ми повинні використати map
щоб пройтись по toLower
, оскільки воно працює з масивом.
const dasherize = compose(
intercalate('-'),
map(toLower),
split(' '),
replace(/\s{2,}/ig, ' '),
);
dasherize('The world is a vampire'); // 'the-world-is-a-vampire'
Функція trace
дозволяє нам побачити дані на певному етапі нашого відлагодження. Тікі мови програмування як Haskell та PureScript мають схожі функції для полегшення процесу розробки.
Композиція буде нашим знаряддям для побудови програм, і, на щастя, вона буде підтримана потужною теорією, яка гарантує, що у нас все спрацює. Давайте розглянемо цю теорію.
Теорія категорій - це абстрактна гілка математики, яка може формалізувати поняття з декількох різних галузей, таких як теорія множин, теорія типів, теорія груп, логіка тощо. Це, перш за все, стосується об'єктів, морфізмів та перетворень, що досить тісно відображає програмування. Ось схема тих самих понять, що розглядаються з кожної окремої теорії.
Перепрошую, я жодним чином не хотів налякати вас. Я не очікую, що ви будете тісно знайомі з усіма цими поняттями. Я лише хочу показати як багато ми маємо дублюваннь, щоб ви могли зрозуміти, чому теорія категорій має на меті об'єднати ці речі.
У теорії категорій у нас є дещо, що називається... категорія. Вона визначається, як колекція з наступними властивостями:
- Колекція об'єктів
- Колекція морфізмів
- Поняття композиції з морфізмами
- Морфізм, який відрізняється - називається індивідуальністю (пер.: identity)
Теорія категорій є достатньо абстрактною, щоб моделювати багато речей, але давайте застосуємо це до типів і функцій, про які ми зараз турбуємося.
Колекція об'єктів.
Об'єктами будуть типи даних. Наприклад, String
(строка), Boolean
(логічне значення), Number
(число), Object
(об'єкт) і т.д. Ми часто розглядаємо типи даних як набори всіх можливих значень. Можна розглянути Boolean
як набір [true, false]
та Number
як набір усіх можливих числових значень. Обробка типів як наборів корисна, оскільки ми можемо використовувати теорію груп для роботи з ними.
Колекція морфізмів. Морфізми будуть нашими стандартними щоденними чистими функціями.
Поняття композицї з морфізмами.
Це, як ви вже можливо здогадались, наша абсолютно нова іграшка - compose
. Ми вже обговорили, що наша функція compose
- асоціативна, що не є випадковістю, оскільки це властивість, яка повинна виконуватись для будь-якої композиції у теорії категорій.
Ось зображення, яке демонструє композицію:
А ось і конкретний приклад в коді:
const g = x => x.length;
const f = x => x === 4;
const isFourLetterWord = compose(f, g);
Морфізм, що відрізняється - називається ідивідуальністю.
Давайте познайомимось з корисною функцією, що називаться id
. Ця функція просто бере якусь вхідну величину і видає вам її назад. Погляньте:
const id = x => x;
Ви можете себе запитати "На якого дідька воно нам потрібно?". Ми будемо широко застосовувати цю функцію в наступних розділах, але зараз розглянемо її, як функцію, яка маскується як повсякденні дані.
id
повинно дуже гарно вписатись в композицію. Ось властивість, яка завжди виконується для унарної(унарна: функція з одним аргументом) функції f:
// identity
compose(id, f) === compose(f, id) === f;
// true
Агов, та це ж точнісінько як властивість ідентичності для чисел! Якщо це не одразу помітно, то придивіться трохи краще. Усвідомте марність. Ми дуже скоро будемо бачити використання id
усюди, але, покищо, ми бачимо, що це - функція, яка лише повертає передане в неї значення. Це досить корисно при написанні коду в безточечному стилі.
Отже, категорія типів і функцій. Якщо це ваше перше знайомство, то я уявляю, що ви все ще трохи заплутані щодо того, що таке категорія і чому вона корисна. Ми будемо спиратися на ці знання в усій книзі. На даний момент, в цьому розділі, на цьому рядку, ви можете принаймні побачити, що вона надає нам певної мудрості щодо композиції, а саме - асоціативності та властивостей ідентичності.
Ви можете спитати, а які інші категорії? Ну, ми можемо визначити, що вузли(пер.: nodes) є об'єктами, грані є морфізмами, а композиція - просто об'єднання шляху. Ми можемо визначити, що числа - об'єкти, а >=
- морфізми(взагалі будь-який частковий або загальний порядок може бути категорією). Є багато категорій, але для цілей цієї книги ми будемо займатися лише тим, що визначено вище. Ми достатньо поверхнево ознайомились і повинні рухатися далі.
Композиція поєднує наші функції разом як послідовність труб. Дані потечуть крізь нашу програму як слід - чисті функції це лише, врешті решт, ввід та вивід, так що порушення цього ланцюга негативно відобразиться на нашому результаті, що робить наше програмне забезпечення марним.
Ми ставимо композицію, як принцип дизайну, на щабель вище від усіх інших. Це пояснюється тим, що це композиція робить нашу програму простою та зрозумілою. Теорія категорій відіграватиме велику роль у архітектурі додатків, моделюванні побічних ефектів та забезпеченні правильності результатів.
Наразі, ми знаходимось в точці, коли нам буде корисним побачити це все на практиці. Давайте зробимо приклад програми.
Розділ 06: Приклад застосування
У кожній наступній вправі ми розглянемо об'єкти Car
наступної форми:
{
name: 'Aston Martin One-77',
horsepower: 750,
dollar_value: 1850000,
in_stock: true,
}
{% exercise %}
Використайте compose()
щоб переписати наступну функцію.
{% initial src="./exercises/ch05/exercise_a.js#L12;" %}
const isLastInStock = (cars) => {
const lastCar = last(cars);
return prop('in_stock', lastCar);
};
{% solution src="./exercises/ch05/solution_a.js" %}
{% validation src="./exercises/ch05/validation_a.js" %}
{% context src="./exercises/support.js" %}
{% endexercise %}
Враховуючи наступну функцію:
const average = xs => reduce(add, 0, xs) / xs.length;
{% exercise %}
Використайте допоміжну функцію average
, щоб написати averageDollarValue
як композицію.
{% initial src="./exercises/ch05/exercise_b.js#L7;" %}
const averageDollarValue = (cars) => {
const dollarValues = map(c => c.dollar_value, cars);
return average(dollarValues);
};
{% solution src="./exercises/ch05/solution_b.js" %}
{% validation src="./exercises/ch05/validation_b.js" %}
{% context src="./exercises/support.js" %}
{% endexercise %}
{% exercise %}
Змініть fastestCar
за допомогою compose()
та інших функцій у безточковому стилі. Підказка, функція append
може стати в нагоді.
{% initial src="./exercises/ch05/exercise_c.js#L4;" %}
const fastestCar = (cars) => {
const sorted = sortBy(car => car.horsepower, cars);
const fastest = last(sorted);
return concat(fastest.name, ' is the fastest');
};
{% solution src="./exercises/ch05/solution_c.js" %}
{% validation src="./exercises/ch05/validation_c.js" %}
{% context src="./exercises/support.js" %}
{% endexercise %}