Functions, functions are everywhere: 8 принципів функціонального програмування на прикладах з JavaScript

Які концепції є в JavaScript із математики? Для когось відповідь буде максимально очевидна, а когось може здивувати — це функціональне програмування!

І здавалось би, що ця тема обговорювалась так часто, що всі повинні в ній розбиратись. А от досвід проведення інтерв’ю показав, що багато кандидатів не можуть назвати навіть найпопулярніші з них, на відміну від принципів ООП. Тому пропоную зануритись в тему і розібрати основні принципи функціонального програмування, які варто знати кожному хорошому інженеру.

Трішки про себе — мене звати Інна Іващук, займаю позицію Lead Software Engineer у GlobalLogic. У компанії я близько 9 років і більше 10 років у веброзробці. До того ж, я Trusted Interviewer в компанії і за останніх 5 років провела більше сотні технічних співбесід для різних рівнів (від Junior до Lead) та різних проєктів. А також являюсь частиною програмного комітету конференції Fwdays (JavaScript / React / TS).

У цій статті пропоную розглянути:

  • Що таке функціональне програмування?
  • Основні концепції (pure function, HOF, рекурсія та інші).
  • Практичне застосування.
  • Монади та функтори.

Інтро: що таке функціональне програмування

Перша асоціація, яку у більшості викликає термін «функціональне програмування» — це функції, функції всюди. При обговоренні із колегами більшість згадали про лямбди, монади, рекурсію та дехто — про математику.

Звичайно, що функціональне програмування — це парадигма, коріння якої сягають глибоко в математику, адже саме звідти до нас прийшла концепція компонування та застосування функцій.

Першим, хто це зробив, був американський математик Алонзо Черч у 1930-х роках у своїх роботах над лямбда-численнями. Алонзо працював також як інженер-програміст, тобто застосовував свої математичні принципи вже в програмуванні. Недаремно стрілочні функції, або lambda expression використовуються до цього часу.

Варто згадати і першу функціональну мову програмування — Lisp, що зʼявилась у 1958 році та була натхненна лямбда-численнями.

Ви, мабуть, також бачили багато мемів про Lisp і те, що там настільки багато дужок, що ти не можеш бути впевнений, чи закрив усі, які відкрив на початку. Тут пропоную глянути, а як виглядає реалізація знаходження n! (факторіал числа) з використанням рекурсії у Lisp:

(write-line "Please enter a number...")  
(setq x  (read))
(defun factorial(n)
    (if (= n 1)
            (setq a 1)
    )
    (if (> n 1)
        (setq a (* n (factorial (- n 1))))
    )
    (format t "~D! is ~D" n a)
    a  ;;
)


(factorial x)

Пізніше з’являється мова програмування Meta Language (і ні, вона немає нічого спільного із компанією Meta). Вона вже була розроблена для формальної верифікації програм і саме ця мова програмування зробила значний внесок у розвиток статичної типізації та інференцій типів у функціональних мовах. І давайте теж глянемо приклад реалізації n!, використовуючи рекурсію:

(fun fact n = let
  fun fac 0 acc = acc
    | fac n acc = fac (n - 1) (n * acc)
  in
    if (n < 0) then raise Domain else fac n 1
  end

Починаючи з 1990-х років, разом із появою WWW, функціональні концепції почали активно інтегруватись в інші мови програмування. Навіть такі OOP-орієнтовані мови програмування, як Java, С++, Python і JavaScript.

Чому ж функціональне програмування стало таким популярним і знаходило все більше застосувань?

Чистота і передбачуваність. Ми оперуємо невеликими чистими функціями, відповідно відсутність побічних ефектів робить код більш зрозумілим і легшим для тестування.

Математична основа. Тісний звʼязок з математикою забезпечує строгу семантику та спрощує доведення коректності програм.

Високий рівень абстракції. Високий рівень абстракції став важливим фактором, так як функціональні мови дозволяють написати більш компактний виразний код. Тобто ми чітко бачимо, що взагалі вимагається від тієї чи іншої функції. На відміну від абстракції в ООП, тут вона базується на операціях, функціях, композиціях і тд.

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

Основні концепції, які існують у функціональному програмуванні

Почнемо з того, що в нас є парадигми програмування — імперативні та декларативні. Якщо ми говоримо про імперативні, то це процедурні, більш структурні мови програмування. Якщо про декларативні — це якраз логічні та функціональні.

Декларативний підхід являє собою спосіб описати програму, де ми фокусуємся на тому, що програма має зробити, а не як саме вона це робить. Розглянемо на прикладі, коли нам потрібно відфільтрувати масив із чисел. Перший варіант коду — це імперативний підхід:

const numbers = [1, 2, 3, 4, 5];
const filteredNumbers = [];

for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] > 3) {
    filteredNumbers.push(numbers[i]);
  }
}
console.log(filteredNumbers); // [4, 5]

Другий варіант — це декларативний підхід:

const numbers = [1, 2, 3, 4, 5];
const filteredNumbers = numbers
	.filter(number => number > 3);


console.log(filteredNumbers); // [4, 5]

В першому варіанті ми використовуємо цикл for, а в другому — метод для роботи з масивами filter. Декларативний стиль написання коду для мене особисто став настільки звичною справою, що коли доводиться пописати трішки циклів, то відчуваю, наче переходжу на темну сторону і отримую тотальний контроль над кодом 🤓

Ще один приклад із сортуванням масиву чисел в імперативному та декларативному стилях відповідно:

// Imperative
// Bubble sort
const numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3];


for (let i = 0; i < numbers.length - 1; i++) {
  for (let j = 0; j < numbers.length - i - 1; j++) {
    if (numbers[j] > numbers[j + 1]) {
      // Міняємо місцями елементи
      const temp = numbers[j];
      numbers[j] = numbers[j + 1];
      numbers[j + 1] = temp;
    }
  }
}


console.log(numbers);

// Declarative
const numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3];

const sortedNumbers = numbers
	.sort((a, b) => a - b);

console.log(sortedNumbers);

Варто зазначити, що існує набір основних концепцій декларативного стилю:

Якщо б ви прийшли до мене на інтерв’ю і назвали хоча б п’ять із них — це було б просто чудово. А якщо б ще розповіли, що кожен із цих принципів означає та могли б написати приклад на JS — це взагалі супер 🎉 Тому давайте розбиратись з ними на практиці.

Від теорії до практики

First Class function (first-class citizens)

Це функції, які мають ті ж властивості, що й інші об’єкти. Тобто ми їх можемо зберігати в змінних, передавати і повертати. І оскільки JavaScript досить гнучкий, ми можемо створити зміну, переприсвоїти іншій змінній, викликати її, можемо навіть створити масив із функціями, які ми будемо викликати. Приклад того, як це може виглядати:

// First-class function
const greet = () => "Hello!";
const storeFunction = greet;   // Saved in a variable
storeFunction();  // Call fn using that variable


// Stored in object
const myUtils = {
  sayHi: greet,
  sayBye: () => "Bye!",
}
myUtils.sayBye();


// Stored in array
const listOfFns = [greet, myUtils.sayBye];
listOfFns[0]();

Pure function (чиста функція)

Це функція, яка завжди повертає один і той самий результат для однакових вхідних даних і не має побічних ефектів. Розглянемо приклад коду, де у нас є дві функції — add() та increment():

// Pure function
function add(a, b) {
  return a + b;
}


// Function with side effects (not a pure fn)
let count = 0;
function increment() {
  count++;
  return count;
}

Функція add(), яка очікує отримати два аргумента, а і b і повертає суму вхідних даних, буде чистою, оскільки не використовує зовнішніх змінних і ніщо не впливає на результат. А ось функція increment() - використовує зовнішню змінну і ми не можемо бути впевненим в тому, що результат завжди буде передбачуваний без побічних ефектів (side effects).

Higher-order functions (функції вищого порядку)

Це функція, яка або приймає інші функції як аргументи, або повертає функцію як результат. Розглянемо на прикладах коду:

const numbers = [1, 2, 3, 4, 5];

// map
const doubled = numbers.map(x => x * 2); // [2, 4, 6, 8, 10]

// flatMap
const arr = [[1, 2], [3, 4], [5]];
const flattened = arr.flatMap(subArr => subArr); // [1, 2, 3, 4, 5]

// filter
const evenNumbers = numbers.filter(x => x % 2 === 0); // [2, 4]

// reduce
const sum = numbers.reduce((total, x) => total + x, 0); // 15

// forEach
numbers.forEach(x => console.log(x)); // 1 2 3 4 5

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

Також ми можемо створювати свої власні HOF. Наприклад, у мене це буде функція для фільтування масиву, яка буде приймати масив та callback функцію (predictate):

// our HOF
function myAwesomeFilter(array, predicate) {
  const result = [];

  for (let i = 0; i < array.length; i++) {
    if (predicate(array[i])) {
      result.push(array[i]);
    }
  }
 
  return result;
}

// given data
const arr = [2, 55, 33, 1, 100, 99];

// trigger our HOF
myAwesomeFilter(arr, (item) => item > 10);
// result [55, 33, 100, 99]

Function composition (композиція функцій)

Коли ми говоримо про функціональне програмування, не можна не згадати про композицію функцій — це концепція, яка передбачає об’єднання двох або більше функцій в одну, де результат однієї функції стає вхідними даними для наступної.

Розглянемо приклад, як це може виглядати:

const double = x => x * 2;
const increment = x => x + 1;

// Example 1
const composedFunction = x => increment(double(x));

// Example 2
const numbers = [1, 2, 3, 4, 5];

const updated = numbers.map(x => increment(double(x)));

У нас є дві досить простенькі функції, перша double — збільшує значення на два, а друга increment — додає до значення плюс 1. Відповідно ми можемо робити компонування і об’єднати їх в функцію composedFunction.

Наприклад, у React складні компоненти можна будувати з більш простих, об’єднуючи їх за допомогою композиції.

Immutability (імутабельність)

Це одна із ключових концепцій функціонального програмування, і вона говорить нам про те, що всі дані після створення не повинні бути зміненими, а замість зміни існуючих даних ми створюємо нові копії і оперуємо саме ними.

Чому імутабельність важлива?

  • Відсутність побічних ефектів. Оскільки об’єкти не змінюються після створення, відсутні непередбачувані зміни стану, що ускладнюють відстеження помилок.
  • Простіша логіка. Код стає більш декларативним, оскільки ми описуємо, що потрібно отримати, а не як змінити стан.
  • Оптимізація механізмів кешування. Імутабельні об’єкти легше кешувати, оскільки їх стан не змінюється з часом.
  • Захист від непередбачених побічних ефектів. Зміни в одному місці коду не впливають на інші частини програми.
  • Легше відстежувати зміни. Історія змін даних зберігається в нових об’єктах, що полегшує відновлення попередніх станів.

Давайте розберемо наступний приклад коду:

const moviesList = [{
    title: 'The Lord of the Rings',
    genre: 'fantasy',
    director: 'Peter Jackson',
    year: 2001,
}, {
    title: 'Dune',
    genre: 'Sci-Fi',
    director: 'Denis Villeneuve',
    year: 2021,
}];

// Fn 1: filter by year
function filterByYear(movies, year) {
    return movies.filter(item => item.year === year);
}
filterByYear(moviesList, 2021);

// Fn 2: add field rating with value 10
function addRating(movies) {
    movies.forEach(movie => {
        movie.rating = 10;
    });

    return 'Rating field is added';
}
addRating(moviesList);

Функція filterByYear — є кращою з точки зору імутабельності:

  • створює новий масив, який містить лише фільми, що відповідають заданому року;
  • не модифікує оригінальний масив moviesList, що відповідає принципу імутабельності;
  • більш безпечна, оскільки не впливає на інші частини програми, які можуть використовувати оригінальний масив.

Функція addRating:

  • модифікує оригінальний масив moviesList, додаючи поле rating до кожного об’єкта.
  • порушує принцип імутабельності, оскільки змінює стан існуючого об’єкта.
  • може призвести до непередбачених побічних ефектів, якщо інші частини програми очікують, що оригінальний масив залишиться незмінним.

Recursion

Рекурсія — це метод програмування, при якому функція викликає сама себе всередині свого тіла.

На початку ми з вами розглянули як виглядає реалізація підрахунку n! іншими мовами програмування. Настав тепер час глянути на реалізацію з використанням JavaScript:

function factorial(n) {
  if (n === 0) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}

console.log(factorial(5)); // 120

Тут функція факторіал викликає сама себе до моменту, поки ми не отримаємо необхідний результат. І до речі, це той принцип функціонального програмування, який найбільше бояться новачки, і 90% на інтерв’ю вважають, що це антипатерн 🙂

Дійсно, тут варто згадати про те, що розмір стеку викликів обмежений, тому рекурсивні функції слід використовувати з обережністю. Ітеративні підходи часто виявляються ефективнішими, оскільки вони не вимагають створення додаткових контекстів для кожного виклику.

Currying

Каррінг — це техніка, при якій функція з кількома аргументами перетворюється на послідовність функцій, кожна з яких приймає один аргумент. Зазвичай це виглядає таким чином:

function sum(a) {
    return (b) => {
        return (c) => {
            return a + b + c
        }
    }
}

console.log(sum(1)(2)(3)) // 6

Тобто в нас є функція sum, яка повертає наступну функцію, потім повертає ще одну функцію, і вже в результаті ми отримуємо суму всіх переданих аргументів. Для того, щоб викликати подібну функцію, нам потрібно зробити ось таку комбінацію sum(1)(2)(3), щоб викликати кожну з вкладених, передаючи необхідний аргумент.

Насправді така реалізація не дуже практична. Якщо нам потрібно отримати суму тільки двох аргументів, то в даній реалізації це не спрацює. Адже в нас повернеться просто функція, яка очікує ще один аргумент. Але це можна виправити наступним чином:

function sum(a){
  let result = a; //closure
 
  return function inner(b){ 
    if(!b) {
        return result; 
    }
    result += b; //step 3
 
    return inner;
  }
}
console.log(sum(1)(2)(3)(4)()) // 6 

Давайте розберемо, що тут відбувається. Зовнішня функція sum(a) приймає перше число a і запам’ятовує його в змінній result всередині замикання та повертає внутрішню функцію inner.

Внутрішня функція inner(b) приймає наступне число b, і якщо b не визначено (тобто це останній виклик), повертає накопичений результат result, а інакше додає b до result і повертає себе ж. Це створює рекурсивний виклик.

Кожен рекурсивний виклик inner повертає нову функцію, яка пам’ятає про попередні додавання завдяки замиканню. Це дозволяє виклики sum(1)(2)(3)(4), де кожен наступний виклик додається до попереднього результату.

* Коли вас запитують на інтерв’ю, швидше за все очікують побачити саме простіший варіант — коли у вас функція, яка повертає функції. Просто побачити, чи ви розумієте, як можна застосувати каринг в JavaScript.

Lazy Evaluation

Ліниві обчислення — це коли значення не обчислюється до тих пір, поки воно дійсно не потрібне.

Деякі мови програмування, такі як Haskell, надають можливість використовувати ліниві обчислення, так би мовити, з коробки. А в JavaScript ми можемо реалізувати ліниві обчислення, використовуючи певні конструкції, такі як генератори або замикання. Розглянемо приклад:

function* lazyRange(start, end) {
  let current = start;
  while (current <= end) {
    yield current;
    current++;
  }
}

// Usages
const numbers = lazyRange(1, 5);
numbers.next().value;  // 1 (evaluate only now)
numbers.next().value;  // 2 (evaluate only now)
numbers.next().value;  // 3 (and so on)

Функція-генератор lazyRange — чудовий спосіб генерувати послідовність значень за необхідності. У цьому прикладі вона повертає числа від 1 до 5. Кожен виклик next().value надає наступне число. Коли всі числа вичерпано, генератор повертає undefined. Генератори особливо корисні для тестування. Вони дозволяють легко симулювати складні сценарії, наприклад, послідовність асинхронних запитів, що значно спрощує процес розробки та відлагодження (перевірено на власному досвіді).

Map/Reduce

Операції map і reduce не є «принципами», як чисті функції, але вони є важливими інструментами функціонального програмування. Щоб пояснити дану концепцію, пропоную розібрати ось такий приклад:

У нас є вхідний масив — це чіабата, огірки, томати і шматок сиру. Ми застосовуємо map(), щоб виконати процес нарізання всіх цих інгредієнтів. Потім ми застосуємо reduce(), що зменшити та перетворити всю кількість інгредієнтів на сендвічі.

Для перетворення даної послідовності операцій в JavaScript-код я використала ChatGPT, і ось що отримала в результаті:

function makeSandwiches(ingredients) {
  return ingredients
    .reduce((acc, ingredient, index) => {
      // Group every two ingredients into a single sandwich
      if (index % 2 === 0) {
        // Start a new sandwich with the first ingredient
        acc.push([ingredient]); 
      } else {
         // Add the second ingredient to the current sandwich
        acc[acc.length - 1].push(ingredient);
      }
      return acc;
    }, [])
    .map(filling => ({
      bread: 'ciabatta',
      filling, // Add the grouped ingredients as filling
    }));
}

// Example usage
const ingredients = ['ham', 'cheese', 'tomato', 'lettuce', 'turkey', 'avocado'];
const sandwiches = makeSandwiches(ingredients);
// result [{ bread: "ciabatta", filling: ['ham', 'cheese']}, ...]

ChatGPT пропонує трішки іншу послідовність: спочатку reduce(), потім map(), що є більш логічним.

Варто зазначити, що дану задачу можна виконати використовуючи тільки .reduce(), і тоді у нас буде одне проходження по масиву, тобто Big O(n) замість Big O(2n), і ефективніше по пам’яті, оскільки буде створений тільки один масив (імутабельні методи завжди повертають новий масив).

Монади і функтори

Не можна просто так взяти і не згадати про монади і функтори, коли говоримо про функціональне програмування. Давайте розбиратись 🙂

Функтор і монад — це концепції, запозичені з теорії категорій, тобто з математики, які знайшли широке застосування у функціональному програмуванні.

Функтор

Це об’єкт, який реалізує метод map (можлива інша назва). Тобто метод, який трансформує дані і може бути застосований до іншого об’єкта, трансформуючи його. Наприклад, функтори в Haskell і SKALA виглядатимуть так:

// Scala

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

# Haskell
class Functor f where
    fmap :: (a -> b) -> f a -> f b

Як на рахунок JavaScript? Метод масиву map() є класичним прикладом функтора. Він приймає функцію як аргумент і застосовує її до кожного елемента масиву.

const numbers = [1, 2, 3, 4];

// increase every number by 42
numbers.map(n => n + 42)

Тобто масив можна вважати функтором, досить просто.

Але є один нюанс — в JavaScript, на відміну від Haskell і SKALA, метод map () в callback приймає два аргумента — саме значення і індекс, тобто це дещо порушує класичну реалізацію. Але, думаю, ми можемо зробити виключення і вважати, що це все-таки класичний функтор.

Якщо вам цікаво спробувати попрацювати із класичними функторами, то рекомендую спробувати таку бібліотеку як Ramda, тому що там map() реалізований по всіх канонах функціонального програмування.

import R from 'ramda';

// Функція для глибокого копіювання, 
// використовуючи функтор для обробки масивів та об'єктів
const deepClone = R.map(x => {
  if (R.is(Object, x)) {
    return deepClone(x);
  } else if (R.is(Array, x)) {
    return deepClone(x);
  } else {
    return x;
  }
});

const originalObject = {
  a: 1,
  b: [2, 3],
  c: { d: 4 }
};

const clonedObject = deepClone(originalObject);

console.log(clonedObject); 
// result { a: 1, b: [ 2, 3 ], c: { d: 4 } }

Або ж написати свою власну реалізацію за прикладом:

 const Functor = value => ({
     map: fn => Functor(fn(value)),
     chain: fn => fn(value),
     of: () => value
 });

// Usages 
function randomIntWrapper(max) {
  return Functor(max)
      .map(n => n + 1) // max + 1
      .map(n => n * Math.random()) // Math.random() * (max + 1)
      .map(Math.floor) // Math.floor(Math.random() * (max + 1))
}

const randomNumber = randomIntWrapper(333)

randomNumber.of() // returns number between 0 and 333
randomNumber.chain(n => n - 1) // returns (number between 0 and 333) - 1

Монад

Це абстрактна концепція, яка використовується у функціональному програмуванні для структурування та управління обчисленнями. Її можна уявити як «контейнер», який може містити значення або не містити його взагалі. Цей «контейнер» також надає спеціальні методи для роботи зі значенням, незалежно від того, чи воно присутнє, чи відсутнє.

Концепція монади на прикладі доставки піци. Уявіть собі, що ви замовляєте піцу онлайн. Процес можна розбити на кілька етапів:

  1. Вибір піци — обираємо піцу з меню.
  2. Оформлення замовлення — вказуємо адресу доставки та інші деталі.
  3. Підтвердження замовлення — ресторан підтверджує замовлення.
  4. Доставка — кур’єр доставляє нам піцу.

Кожен з цих етапів може «провалитися». Наприклад, піци може не бути в наявності, ми можемо помилитися адресою, ресторан може бути закритий, або кур’єр може заблукати.

При використанні імперативного підходу нам потрібно постійно перевіряти, чи успішно пройшов кожен етап:

function deliverPizza(order) {
  const pizza = choosePizza(order.pizzaType);
    return "Піци немає в наявності";
  }

  const orderConfirmation = confirmOrder(order);
  if (!orderConfirmation) {
    return "Помилка оформлення замовлення";
  }

  const deliveryStatus = deliver(orderConfirmation.orderId);
  if (deliveryStatus === "Загублено") {
    return "Кур'єр загубився";
  } else if (deliveryStatus === "Доставлено") {
    return "Піца доставлена!";
  }
}

Підхід з Maybe монадою (або схожою абстракцією). Ми можемо створити «контейнер» (монаду), який буде представляти результат кожного етапу. Цей «контейнер» може містити значення (успіх) або ні (невдача).

const Result = {
  Success: (value) => ({
    map: (fn) => Result.Success(fn(value)), // Apply function to value
    bind: (fn) => fn(value), // Chain operations
    getOrElse: (defaultValue) => value, // Get value or default
  }),
  Failure: (error) => ({
    map: (fn) => Result.Failure(error), // Propagate error
    bind: (fn) => Result.Failure(error), // Propagate error
    getOrElse: (defaultValue) => defaultValue, // Return default
  }),
};

function deliverPizza(order) {
  return choosePizza(order.pizzaType) // Choose pizza
    .bind(pizza => confirmOrder(order) // Confirm order
      .map(orderConfirmation => ({ pizza, orderConfirmation }))) // Combine results
    .bind(({ pizza, orderConfirmation }) => deliver(orderConfirmation.orderId) // Deliver pizza
      .map(deliveryStatus => ({ pizza, orderConfirmation, deliveryStatus }))) // Combine results
    .map(({ pizza, orderConfirmation, deliveryStatus }) => { // Handle final result
      if (deliveryStatus === "Delivered") {
        return "Pizza delivered!";
      } else {
        return `Error appear: ${deliveryStatus}`; // Handle error
      }
    })
    .getOrElse("Something goes wrong"); // Handle any error
}

// Functions is use
function choosePizza(pizzaType) { /* ... */ }
function confirmOrder(order) { /* ... */ }
function deliver(orderId) { /* ... */ }

Пояснення:

  • Result.Success(value) — «контейнер» зі значенням (success).
  • Result.Failure(error) — «контейнер» з помилкою (failure).
  • map(fn) — застосовує функцію fn до значення, якщо воно є (success). Якщо «контейнер» містить помилку, map нічого не робить.
  • bind(fn) — схожий на map, але дозволяє повертати інший «контейнер» (інший етап).
  • getOrElse(defaultValue) — повертає значення з «контейнера», якщо воно є (успіх). Якщо «контейнер» містить помилку, повертає значення за замовчуванням.

Переваги:

  • Код стає більш структурованим. Ми бачимо послідовність етапів, і нам не потрібно постійно перевіряти, чи кожен етап успішний.
  • Легше обробляти помилки. Ми можемо обробити всі помилки в кінці, використовуючи getOrElse.
  • Можливість комбінувати етапи. bind дозволяє нам «зв’язувати» результати різних етапів.

Тобто можна вважати, що монад — це коли функції поводяться добре навіть тоді, коли вони хочуть бути хуліганами.

Останній приклад, який ви точно зустрічали в роботі — це Promises. Вони, хоч і не повністю задовольняють критерії монад, явно демонструють вплив монадичних структур даних, що підтверджується їхнім широким застосуванням:

// Приклад 1
Promise.resolve(10)
  .then(x => x + 5)  // 15
  .then(x => x * 2)  // 30
  .then(console.log);  // Output: 30

// Приклад 2
const fetchUser = userId =>
  fetch(`https://api.example.com/users/${userId}`)
    .then(response => response.json())
    .catch(error => Promise.reject(error));

fetchUser(123)
  .then(getUserAddress)
  .then(address => console.log(address))
  .catch(error => console.error(error));

Висновки

Що ж, ми з вами здійснили невеличку подорож у світ функціонального програмування, починаючи з його математичного коріння. Ми розібрали 8 стовпів, на яких тримається функціональне програмування, і подивилися, як вони працюють на практиці в JavaScript — одній з найпопулярніших мов програмування. Гадаю, тепер навіть монади і функтори стали для вас трохи зрозумілішими, і ви переконалися, що це не така вже й страшна звірина.

Важливо пам’ятати, що функціональне програмування не є універсальним рішенням для кожного проєкту. Необхідно враховувати специфіку проєкту, існуючі стандарти та рівень підготовки команди. Однак розуміння принципів функціонального програмування значно розширить ваші можливості та стане цінним інструментом у багатьох ситуаціях.

І наостанок моя рекомендація — слідуйте прикладу сильного Doge і прокачуйте в собі справжнього інженера, якому цікаво досліджувати і розбиратись в OOP, FPP, алгоритмах, структурах даних, патернах проєктування та інших суміжних технологіях.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось19
До обраногоВ обраному9
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Дуже гарна перша частина статті, прекрасні принципи. Мені як скалісту сподобалось.

А от по функторам і монадам дарма не згадали жодного слова про закони. Монада — не контейнер і не коробка. Таке пояснення погіршує ситуацію з розумінням ФП

Ну якщо пояснювати монади через ендофунктори і теорію категорій то ніхто не зрозуміє без багажу знань) Мабуть найкращім рішенням було б пропустити в цій статті і монади і функтори, бо це надто важко для і так перевантаженої принципами статті. Можливо автору треба поглибити знання і написати окремі статті...

Монади можна пояснювати і абсолютно інженерно, прямо з коду. Хороший приклад бачив ось тут youtu.be/...​Rc3aU?si=ugUIwZSJposRzmi3

Дякую. Дуже файна та корисна стаття.

тут много пишут о том, как неприменимо фп в реальной жизни. но читать про фп не обязательно для того чтобы везде начать лепить монады
знакомство с более сложными абстракциями дает чуть лучше организовывать код при работе с более простыми
где-то это подтолкнет оставить функцию чистой и вынести выполнение сайд-эффекта повыше. где-то дает проще понять инструмент, который имеет что-то общее с монадой или thunk-ом. где-то подтолкнет разделить саму логику и логику доступа к какому-то значению внутри контейнера

кровь из глаз, сломался на сравнении «императивного» и «декларативного» подхода — функции filter(), sort() - это не какая то магия и внутри них будет... о боже все тот же «императивный подход».
И «декларативный» подход в приведенных примерах — это всего лишь «повторное использование кода», которое есть вообще во всех парадигмах, подходах, стилях, начиная с ассемблера без исключения.

my_filter :: (a -> Bool) -> [a] -> [a]
my_filter pred [] = []
my_filter pred (a:rs) | pred a == True = a : (my_filter pred rs)
my_filter pred (_:rs) = my_filter pred rs
ага, сама імперативщина

в той части статьи, которую я прочитал, такого кода не было

так його і далі немає. це антитеза до вашого «о боже все тот же „императивный подход“.»

И «декларативный» подход в приведенных примерах — это всего лишь «повторное использование кода», которое есть вообще во всех парадигмах, подходах, стилях, начиная с ассемблера без исключения.

В ПРИВЕДЕННЫХ ПРИМЕРАХ
чукча не читатель, чукча — писатель.

О боже внутрі хаскеля всьо тот же mov rax, rdx

Ноутбук летить у вікно
Завіса

Еще один недочитав стремиться высрать опровержение

що ти там пьорнул? переклади для людей на імператівний язик

стремиться

стремится, неук, запишись на курси свободного языка

отправь на курсы свободного языка автокорректор для айфона :)

шкода, що статей про фп мало, і у народу склалося таке враження, що фп то для еліти.
насправді, ті ж самі монади неявно присутні у абсолютно всіх мейнстримних мовах програмування, просто їх ніхто не бачить.
про монади слабенько написано, так. напевно прийдеться самому щось написати.

Що статей про ФП мало то так, краще щеб були. А взагалі то так і є — просто навіть не думають що це і те щось з фп позичено. Якщо використання саме чисто фп мов на практиці, то тут дуже розмито — де і як.

монади неявно присутні у абсолютно всіх мейнстримних мовах програмування, просто їх ніхто не бачить

ага, як Аллах, приблизно

монада рахмат рахим!

да здравствуєт Категоріальний Талібан!

Код стає більш структурованим

ніт

«fix error» <- абсолютно коректна програма на хаскелі.

Ответ любителям Pure Functional Programming ниже в комментах.

ФП в чистом виде это безполезная херня, нет буквально ни одного крупного, значимого проекта за последние 15 лет, написанном на чистом ФП, кроме Cardano, где фаундер это сын создателя Haskell.

Тем временем весь cloud native и огромная часть blockchain — на Go, весь ML на python, всякие либы и базы на C/C++.

Результаты говорят сами за себя.

ML на python скорее как сценарная прослойка. Все либы на си написаны.

Вообще за функциональное программирование не совсем согласен — вот например есть Go где-то его можно назвать функциональным, так как ООП так такового в нем нет (и это прекрасно)
большое функциональное сильно требовательное к разработчику, нужно вдумчиво резать зоны видимости и и переиспользовать только повторяемые алгоритмы иначе все быстро превратится в нечитаемое спагетти с 100500 методами (раннее php так сказать)

Go функціональний? Вибачте, але ці слова дуже чітко демонструють вашу компетенцію в фп — ви зовсім не розумієте що то таке. Go це процедурне програмування. Як може бути фп мова з такою кількістю поінтерів? Де там чисті функції в Go? Go це один великий сайдеффект! Якщо в мові немає ООП вона не стає функціональною від того)

Важко зрозуміти, як вимірюється значимість. Популярність чи складність? Але, в будь якому разі, розробників на Haskell менше. Серед складних проектів можна взяти сам Haskell як приклад. Дійсно складний проект, мова з лінивими обчисленнями, що включає оптимізації, JIT-компіляцію та вивід типів. Ну а так Nix Package Manager, darcs, pandoc, semantic, cabal.

«ответ любителям математики в коментах ниже.
математика в чистом виде это безполезная херня, нет буквально ни одного крупного, значимого проекта за последние 15 лет, который использует математику, кроме биткоина, где фаудер это сын создателя теоремы пифагора.
тем временем все бухгалтера и инженеры используют калькуляторы и логарфические линейки, даже в ссср любая касирша умела в счеты.
результаты говорят сами за себя.»

математика в чистом виде это безполезная херня ... биткоина ... логарфические линейки ... бухгалтера ... ссср умела в счеты

гги.. сарказму не побачив, а совковий бред «говорит сам за себя» то точно. І взагалі набхуа ту математику вчать якщо кожен мучач може юзати АІ.

В математике точно так же, есть гиперполезные — линейная алгебра, теория чисел, теорвер, которые есть база, основа, фундамент современных технологий, а есть huета для игры в бисер — теория категорий, лол, а ведь перекликается с топиком...

математика це наука про абстракції. а абстракція — неодмінний компонент мислення. так що ваша vuseри, це щось на кшталт «мислення huіслення, молоток — ото сила!».

Якшо з програмування змістити фокус на engineering

То швидко стає зрозуміло, що функціональна парадигма (принаймні, чиста) має ДУЖЕ обмежене застосування.

Ну ж бо не годиться воно для моделювання складних систем зі стейтами і сайд-ефектами.
A real world, нажаль (бгггг), складається в основному з таких.

Те для чого воно придатне — трансформація потоків даних, плюс деякі зручні фішки і паттерни — вже давненько made its way у всі основні мови.

That said, воно таки ставе на місце мізки і прищеплює більше правильного мислення.
Так шо чисто як brain exercise — воно очєнь і очєнь.

ЗІ але з монадкамі в вас — ето провал

Вірно. От хаскель називають чистим фп. І функції там дійсно всі чисті. От тільки весь бруд в монадах твориться. От та ж сама монада ІО яка відповідає за взаємодію з операційною системою)

Ну ж бо не годиться воно для моделювання складних систем зі стейтами і сайд-ефектами.
A real world, нажаль (бгггг), складається в основному з таких.

Не знаю, чи писали ви щось на Haskell чи ні, але не розумію, чому воно не годиться. Як на мене навпаки, Haskell дозволяє контролювати і стани, і сайд-ефекти. Для кожного слайд-ефекту можна визначити монаду, і для кожного виклику буде видно, до яких сайд-ефектів може привести той чи інший виклик: чи то запис у базу, чи то HTTP запит, виконання системної команди і т. п. Був навіть експериментальний проєкт операційної системи на Haskell. Тому мені з цим важко погодитися.

Знову ж таки, Cardano, там децентралізована система з консенсусом Proof of Stake. Це виглядає як купа мережевої взаємодії, а це і стейти, і сайд-ефекти.

Для кожного слайд-ефекту можна визначити монаду

Та можна-то можна.
Я з цим не сперечаюся, бо ж в людства і набагато химерніші культи були і є.

Але от доцільність, сучка така неблагодарна, доцільність...

Та можна-то можна.

То мені писали що воно не годиться, у розумінні що не можна. Але можна, і для цього просто треба обгортувати значення у монаду. Тобто, якщо функція повертає Int, то це функція без сайд-ефектів, якщо функція повертає IO Int, то вона повертає ціле і робить сайд ефекти. Ось і все. Далі можна створювати власні монади для того, щоб розрізняти сайт-ефекти, а можна і не робити цього.

Але от доцільність, сучка така неблагодарна, доцільність...

А це філософське питання, яке можна задавати майже до будь чого, що є в навності. Чи доцільно щось писати на JS, коли є Python? Якийсь сішник може задатися питанням доцільності ООП, коли можна писати програми і без нього. В принципі відповідь на питання «чи доцільно» виключно практична: треба спробувати й подивитися на результат.

Якщо брати Haskell розробників, то для багатьох це доцільно, і саме цього їм не вистачає в інших мовах програмування. За це вони і люблять Haskell. Багато критиків взагалі не пробували, не розібралися як це працює, але критикують.

Далі можна створювати власні монади для того,

Не. Потрібна. Милиця.
Крапка.

А це філософське питання

Ні.
Це дуже практичне питання на багатьох рівнях.
Включно з грошовим

State теж може бути монадою 🤷‍♂️ але ж тому ми і пишемо на фп, щоб від нього спекатись. Дехто полюбляє для стейту акторів використовувати. А комусь ІО монади з головою. Дарма ви так

але ж тому ми і пишемо на фп, щоб від нього спекатись

та мені дуже давно вже ясно шо «ви» «пишете на фп» for the sake of «писать на фп»

до software engineering стосунку то не має

Ще й як має: простіші тести, легший рефакторінг, ідеальний метч з DDD

Дякую за статтю, фундаментально!

1) Функціональне програмування — не декларативне.

2) deepClone у прикладах не дуже іллюстративно: воно, скоріш за все, працює, але основний код там всередені map. Гілки для обʼєкта та масива однакові.

Оце вжарила, так вжарила

Але у мене відношення стійке от що розбиратися в спагеті функціоналки ще менше задоволення ніж в спагеті звичайному

Городити ото що нагорожено про піцу — то вже для естетов нмд

Як борщ — краще його недосолити ніж пересолити

Класна стаття!

Щось в цьому є! Коли код не імперативний, то херпойми де той брейкпоінт чи console.log запхати щоб зрозуміти що там твориться. Коли знайомився з haskell, то тільки тоді цю біль в повній мірі зрозумів)

Теж практичний момент з дебагом справді.

Мені загалом не подобається код де покроково не можна вставити пробіл щоб відокремити їстівний кусочок чи крок

У функціоналці в тебе може бути кусочок який не завжди просто зрозуміти без phd у всяких хитрожопих теоріях

Такий код важко передати комусь, треба вже армію таких клонів як Інна. Це не дуже скейлиться по людям

Є місце для елегантних викрутасів, але є місце й для квадратнокущового коду теж

Більшість CS грунтується на алгоритмах, тобто суть імперативний підхід який просто інтуітивніший для розуміння. FP то вже математика, більший поріг входу й теоретичного знання для якогось такого інтуїтивного розуміння чужого графоманства

Гарна стаття. Єдине що трохи не сподобалось це пояснення монад. В такому пояснені людина яка нічого про монади не знає може подумати що розуміння монад так і закінчується на монаді maybe. Проте ж це не так. Доречі ніколи на проміси під призмою монад не дивився. Цікава думка)

Доречі ніколи на проміси під призмою монад не дивився.

Це ж не монада, там і ліва ідентичність порушується, і асоциативність під питанням. Це що кидається в очі. Як на мене, самий гарний приклад для розуміння монади це список: не занадто складний, плюс видно багато особливостей саме монад.

Ну в повній мірі звичайно promise не є монадою в математичному сенсі, проте ліва ідентичність все ж є у випадку: Promise.resolve(a).then(f) де resolve це return, а then грає роль bind. Проте в більш складних сценаріях це не так. Але думка скоріше про те що створення проміса могло бути результатом знайомства з концепцією монад та її переосмисленням з адаптацією.

Дякую. Дійсно інформативно

Як на мене жахливо. Я не вважаю JS функціональною мовою програмування. Стаття скоріше на тему: давайте виберемо з функціонального програмування хоча б щось, що можна хоч якось написати на JS. Буде цікаво подивитися на приклади таких концептів ФП, як тотальні функції, референтна прозорість, узагальнений алгебраїчний тип даних (GADT), сумарні та добуткові типи, біфунктори, аплікативи, монада стану, вільна монада, комонада, теорія типів, залежні типи, вищі роди типів, екзистенційні типи, ендофунктори, натуральне перетворення, профунктор, алгебраїчні ефекти, напівгрупи, числа Черча, лема Йонеди, ізоморфізм Карі—Говарда, декартово замкнені категорії (CCC), теорема про фіксовану точку.

Далі, я не дуже погоджуюся з тим, щоб приплітати сюди математику. Це мені нагадує ланцюжок: бухгалтери додають числа, додавання чисел це математика, значить бухгалтери це математики. Тому якщо у нас немає теорем, то про математику краще не писати, це зайве, краще не згадувати, бо немає зв’язку.

Далі, лямбда обчислення. Це асемблер функціональних мов програмування, мінімальний набір операцій, з яких будуються усі операції мови. Є відомий лямбда-куб Lambda cube, де вибір тих чи інших властивостей лямбда-обчислення призведе нас до різних функціональних мов програмування. І це не закінчений концепт, бо є, наприклад, QTT Quantitative Type Theory, яке можна розглядати як розширення CoC (Calculus of Constructions, λC) а можна, напевне, і вважати квантифікатор використання окремим виміром. Тут зв’язок зрозумілий, якщо ти не знаєш лямбда обчислення, то ти просто не напишеш компілятор/інтерпретатор для функціональної мови програмування.

А от з JS немає ніякого зв’язку, окрім просто як синтаксичний цукор, який узяли з математики. Тобто, якщо я написав якусь цифру, наприклад 3, то я вже математик? Знову немає зв’язку.

Про монади взагалі не хочеться коментувати, таке враження що авторка сама не розуміє, що це за звір. Навіть визначення немає, бо монада це просто інтерфейс, який має певні властивості. Є приклад, але це приблизно як описувати життя на всій землі, але розказати тільки про амеби. Можливо амеби й важливі, але це мізерна частина та ще й специфічна. Maybe це аплікативний функтор, тобто це навіть не повноцінна монада. І це про потужну парадигму, в якій можна без перебільшення сказати: монади скрізь, усе в програмуванні це монади.

Я не вважаю JS функціональною мовою програмування.

что не мешает продемонстрировать некоторые концепции FP, почему нет?

То і на паскалі можна продемонструвати. Просто якщо демонстрація відбувається на незручному інструменті, то це скоріше відштовхує. Якщо секс спробувати у протигазі та гамаку, то кайф отримати важко.

да, да, надо было сразу писать статью на хаскелле, чтобы ее прочитало два с половиной отмороженных...
... которые итак все знают

Так це і є проблема. Ти або пишеш статтю «як вивчити мову нґуарані за три дні». Вивчити мову вона нікому не допоможе, але її хоча б прочитають. Або пишеш цю статтю на нґуарані, яку прочитають лише ті, хто її вже знає.

А мені подобається Ваш коментар)
А ще більше б сподобалась стаття у якій Ви можете пояснити всі тонкощі монад, аплікативів та функторів та поділитесь своїм досвідом їх використання на практиці.
Теж варто зауважити, що стаття несе більше ознайомчий характер і в першу чергу орієнтована на початківців, які досить часто не знайомі навіть із базовими концепціями функціонального програмування. Відповідно JS використаний тому, що це мультипарадигменна мова програмування і одна із найпопулярніших останні декілька років

Теж варто зауважити, що стаття несе більше ознайомчий характер і в першу чергу орієнтована на початківців, які досить часто не знайомі навіть із базовими концепціями функціонального програмування. Відповідно JS використаний тому, що це мультипарадигменна мова програмування і одна із найпопулярніших останні декілька років

Для мене мультипарадигменна мова програмування це повія, яка намагається задовільнити всіх, тому такі мови не дуже корисні у якості ілюстрації парадигм, особливо тих, які саме обмежують можливості. Наприклад, чисті функціональні мови — відмовляємося від змінних, та отримуємо такі-такі-такі переваги. А тут ніяких переваг, окрім як запудрити мозок.

Тому після таких статей у початківців, та і не тільки, виникає відчуття, що ФП це засіб робити усе через дупу, при тому, що це можна робити нормально.

стаття у якій Ви можете пояснити всі тонкощі монад, аплікативів та функторів

По-перше, таких статей досить багато. По-друге, це нікому не треба. По-третє, концепція доволі складна, тому простого шляху немає. Я пробую максимум зацікавити когось постами, а якщо є бажання, то знайти багато матеріалів не питання.

Тут вже були спроби написати про щось про Haskell хоча б. Більшість коментів було про те, що ФП це мозгойобка, бо я його бачив у JS. Як в анекдоті
— Чув я цього Поваротті, хрипить, картавить, в ноти не попадає...
— А де ви його чули?
— Рабінович наспівав.

Тому після таких статей у початківців, та і не тільки, виникає відчуття, що ФП це засіб робити усе через дупу, при тому, що це можна робити нормально.

Порекомендуйте будь-ласка статті, які на Вашу думку розповідають правильно про ФП для початківців. Вважаю, що конструктивна критика повинна приносити користь :)

Я починав з Real World Haskell. Зараз вона трохи не актуальна, але якщо потроху уважно читати код, виконувати його, практикуватися, та дійти до практичних застосувань, то в принципі починаєш трошечки розуміти, що таке ФП, та які воно надає переваги.

Проблема полягає у тому, що ФП це зовсім інше мислення, зовсім інший погляд на розробку. Як не можна прочитати статтю та навчитися плавати, так не можна й швидко зрозуміти ФП.

Добре ще б знати де і коли (на практиці, не в теорії) в яких випадках є сенс використовувати функціональне програмування. Це я так був трохи поюзавши guix з guile, подумав what a hell is that ...

Ну... якщо подивитися на cabal, то відносна невелика (особливо у порівнянні з python) спільнота створила досить якісний набір пакетів, який ± покриває більшість потреб. Для мене це знак.

На практиці я би задавав скоріше питання не «чи є сенс?», а скоріше «наскільки я дурний?» Так, абстракції це потужно, кожен програміст про це мріє, але... це збільшення вимог до входу.

Те що емоціональне заохочення «наскільки дурний я» в принципі може стимулювати — згоден.
Отой приклад guix+guile це ж вже готове реалізоване рішення, тобто якщо з ним важко працювати (в порівнянні з іншими опціями) — то треба перефразувати відносно кого — чи це інший кейс?

Треба брати до уваги, чи є фанати, тобто ті, кому з ним зручно працювати. Та скільки людей спробували, але знайшли його незручним. Простіше кажучи, чи справа у мені, чи я не один.

Треба брати до уваги, чи є фанати, тобто ті, кому з ним зручно працювати.

Це добре, значить можна то залишити тим кому то зручно, а собі сказати що моя спеціалізація трохи інша та все знати неможливо: — і продовжувати займатись чим зручно і цікаво.

Вас спитали про кейси — ви про Кабал і пакети...

Все точно ок?

Вас спитали про кейси

Питання було, коли є сенс використовувати. Я відповів, що у cabal є пакети, які покривають більшість потреб. Тобто є сенс використовувати як загальну мову програмування. Тобто особливих обмежень я не бачу — проблема не в мові, а в розробниках.

проблема не в мові, а в розробниках

Всеж зв’язано: мова програмування як тулза, плюси/мінуси використання в конкретних випадках, бажання використання мови розробниками виходячи з плюсів/мінусів.
Умовно нп, якщо перфоманс відіграє значну роль, то розробник не обере кульнофічний інструмент який зробить продукт повільним.

1. Я відповів, що у cabal є пакети, які покривають більшість потреб.
2. Тобто є сенс використовувати як загальну мову програмування.

Хибна індукція.

Коли в вас на вході є потік даних який має зазнати певних перетворень.до його виходу.
And that’s pretty much it.

LISP was concieved a LISt Processor for a reason.

Дякую за загальний приклад підходу — коли.
Єдине що — це дуже загально (нп можу сказати що в окремих простих варіантах роботи з текстовим потоком мені достатньо буде sed для його обробки без мов програмування взагалі), а є якийсь яркий приклад обробки потоку підкреслюючий переваги функціонального програмування? — щоб сказати — о це воно, класно.

та хоч будь-який map-reduce case (або схожої парадигми)

будь-який масив даних який треба всмоктать, відфільтрувать, агрегувать, піідсумувать, etc.

але прикол в тому шо всі основні мови вже давно запозичили це з функціональної парадигми і мають ці інструменти

і трахатись із чисто- (або переважно-) функціональними одороблами просто не має сенсу

Добрый день.
В тексте есть опечатка в строке —

console.log(sum(1)(2)(3)(4)()) // 6


Правильное значение в консоли будет — 10.
Спасибо за статью и знания в массы!

Буде цікаво подивитися на приклади таких концептів ФП, як тотальні функції, референтна прозорість, узагальнений алгебраїчний тип даних (GADT), сумарні та добуткові типи, біфунктори, аплікативи, монада стану, вільна монада, комонада, теорія типів, залежні типи, вищі роди типів, екзистенційні типи, ендофунктори, натуральне перетворення, профунктор, алгебраїчні ефекти, напівгрупи, числа Черча, лема Йонеди, ізоморфізм Карі—Говарда, декартово замкнені категорії (CCC), теорема про фіксовану точку.

Це виглядає скоріше як концепти ML-мов/Хаскелю/теоркату, ніж ФП в цілому. Скільки з них підтримуються в тих же ліспах, або в мейнстрімних ФП мовах, типу Скала чи Elixir?

Тут питання, що вважати ФП-мовою. Це питання визначення. Як на мене у цьому питання ще дуже багато піару.

Як на мене, в цій статі розглядаються більше ознаки саме «чистих функціональних мов», як раз з ML-family: Haskell, Agda, Idris, CoQ. Правило просте: якщо є змінні — не функціональна.

Усе перелічене це як раз бонуси, які ми отримуємо за відмову від змінних.

Погоджуюся. Бо як тільки ви відмовляєтесь від змінних то в вас і цикли зникають перетворюючись в рекурсії. І з умовами працювати по іншому доводиться, наприклад частіше використовувати тернарні оператори. Ці фп підходи просто слідують з вищевказаного рестрікшину автоматично

Половина з вказаного в Скалі є на рівні бібліотек. Інша половина з коробки

Чудова стаття!

Підписатись на коментарі