Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Реалізація compose». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`compose()` - це функція-комбінатор, яка збирає кілька функцій в одну: `compose(f, g, h)(x)` еквівалентно `f(g(h(x)))`, тобто виконує їх справа наліво.** Найкоротша реалізація тримається на `reduceRight`: акумулятор проходить крізь список функцій від останньої до першої. Якщо всі функції унарні, достатньо однорядкового варіанта; якщо остання функція має приймати кілька аргументів, акумулятор загортають у масив і розпаковують через spread. ```javascript const composeUnary = (...fns) => x => fns.reduceRight((v, fn) => fn(v), x); const trim = s => s.trim(); const toInt = s => parseInt(s, 10); const inc = n => n + 1; composeUnary(inc, toInt, trim)(' 41 '); // 42 ``` **Ключове:** `compose` будується на `reduceRight` і виконує функції справа наліво, `pipe` на `reduce` і зліва направо.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`compose()` - це функція-комбінатор, яка збирає кілька функцій в одну: `compose(f, g, h)(x)` еквівалентно `f(g(h(x)))` і виконує їх справа наліво.** Уся реалізація зводиться до згортки списку функцій: `reduceRight` для `compose` і `reduce` для дзеркального `pipe`. ## Теорія ### TL;DR - `compose(f, g, h)(x)` дорівнює `f(g(h(x)))`, порядок виконання справа наліво. - Базова реалізація - це `fns.reduceRight((v, fn) => fn(v), x)`. - Щоб остання функція в ланцюжку могла прийняти кілька аргументів, акумулятор тримають у масиві і розпаковують через spread. - Асинхронний варіант загортає перший результат у `Promise.resolve` і зшиває решту через `.then`. - Для TypeScript пишуть перевантаження на кожну кількість функцій, бо загальний тип змінної арності вивести складно. - У продакшені зручно мати обидва варіанти: `compose` (справа наліво) і `pipe` (зліва направо), плюс їхні асинхронні версії. ### Швидкий приклад ```javascript const compose = (...fns) => x => fns.reduceRight((v, fn) => fn(v), x); const trim = s => s.trim(); const toInt = s => parseInt(s, 10); const inc = n => n + 1; const parseAndInc = compose(inc, toInt, trim); console.log(parseAndInc(' 41 ')); // 42 ``` Рядок спершу обрізається, потім перетворюється на число, і лише тоді збільшується на одиницю: функції читаються справа наліво. ### Мінімальна реалізація (ES6) Універсальна версія, у якій останній обробник може прийняти багато аргументів: ```javascript // Універсальна версія: останній обробник може прийняти багато аргументів const compose = (...fns) => (...args) => fns.reduceRight( (acc, fn) => [fn(...acc)], args )[0]; // Приклад const trim = s => s.trim(); const toInt = s => parseInt(s, 10); const inc = n => n + 1; const parseAndInc = compose(inc, toInt, trim); parseAndInc(' 41 '); // 42 ``` Трюк тут у тому, що акумулятор завжди є **масивом аргументів**. Початкове значення - це `args`, а кожен крок повертає `[fn(...acc)]`, тобто знову масив з одного елемента. Наприкінці залишається взяти `[0]`. ### Варіант для unary-функцій Якщо всі функції приймають рівно один аргумент, обгортка в масив зайва, і реалізація стає коротшою та трохи швидшою: ```javascript const composeUnary = (...fns) => x => fns.reduceRight((v, fn) => fn(v), x); ``` Це найпоширеніша форма: у функціональному коді ланцюжки майже завжди складаються з унарних перетворень. ### Promise-aware compose Асинхронна версія працює і з sync, і з async функціями: ```javascript const composeAsync = (...fns) => (...args) => fns .slice(0, -1) .reduceRight( (p, fn) => p.then(res => fn(res)), Promise.resolve(fns[fns.length - 1](...args)) ); // Приклад const fetchUser = async id => ({ id, name: 'Tim' }); const getName = u => u.name; const shout = s => s.toUpperCase(); const getUserNameLoud = composeAsync(shout, getName, fetchUser); getUserNameLoud(7).then(console.log); // 'TIM' ``` Остання функція списку (`fetchUser`) викликається окремо і її результат одразу нормалізується через `Promise.resolve`. Решта функцій підчіплюються через `.then`, тому кожна отримує вже розгорнуте значення, незалежно від того, синхронна вона чи ні. ### Типобезпечна версія для TypeScript Загальний тип для функції змінної арності вивести складно, тому на практиці пишуть перевантаження. Наведені покривають до 4 функцій, далі розширюється за аналогією. ```typescript type Unary<A, R> = (a: A) => R; export function compose<A, R>(f1: Unary<A, R>): Unary<A, R>; export function compose<A, B, R>( f1: Unary<B, R>, f2: Unary<A, B> ): Unary<A, R>; export function compose<A, B, C, R>( f1: Unary<C, R>, f2: Unary<B, C>, f3: Unary<A, B> ): Unary<A, R>; export function compose<A, B, C, D, R>( f1: Unary<D, R>, f2: Unary<C, D>, f3: Unary<B, C>, f4: Unary<A, B> ): Unary<A, R>; export function compose(...fns: Function[]) { return (x: unknown) => fns.reduceRight((v, f) => f(v), x); } ``` Перевантаження зшивають типи сусідніх функцій: вихід `f2` має збігатися зі входом `f1`, інакше компілятор одразу покаже помилку. ### pipe і корисні нотатки Часто потрібен і `pipe()`, який виконує функції зліва направо: ```javascript const pipe = (...fns) => (...args) => fns.reduce((acc, fn) => [fn(...acc)], args)[0]; const parseAndInc2 = pipe(trim, toInt, inc); parseAndInc2(' 41 '); // 42 ``` - Якщо компонуєте **методи об'єктів**, не забудьте про `this`: зв'язуйте їх через `fn.bind(obj)`. - Намагайтеся, щоб функції були **чистими** та **унарними**: композиція буде простішою і передбачуванішою. - Для продакшену зручно мати обидва варіанти: `compose` (справа наліво) і `pipe` (зліва направо), плюс `composeAsync` / `pipeAsync`. ### Типові помилки - **Взяти `reduce` замість `reduceRight`.** Тоді ви отримаєте `pipe`, а не `compose`, і ланцюжок виконається у зворотному порядку. - **Забути початкове значення у `reduce`.** Без нього першим акумулятором стане сама функція зі списку, а не вхідні дані. - **Очікувати, що звичайний `compose` розгорне Promise.** Синхронна версія передасть далі сам Promise; потрібен `composeAsync`. - **Втратити `this` у методів.** `compose(obj.method)` викличе метод без контексту; треба `obj.method.bind(obj)`. - **Розраховувати на кілька аргументів у середині ланцюжка.** Кілька аргументів приймає лише остання (найправіша) функція, усі наступні отримують рівно одне значення. - **Композувати нечисті функції.** Побічні ефекти в середині ланцюжка роблять результат залежним від порядку і часу виклику.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.