Функціональне програмування в JavaScript: зрозуміла альтернатива ООП

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

Привіт! Я — Дар’я Чернявська, JavaScript Developer в NIX.

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

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

Імперативний і декларативний підходи у програмуванні

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

Що це означає на практиці?

Розглянемо приклади, які демонструють відмінність імперативного підходу від декларативного у написанні коду:

Приклад 1: Імперативний підхід

Припустимо, існує масив чисел. Наше завдання — отримати суму його елементів, зведену у квадрат. Чому цей код можна вважати імперативним? По-перше — це цикли, набір дій, поступальне виконання яких веде до вирішення задачі. Цикли дуже характерні для імперативного підходу у програмуванні. Ще одна відмінність — це мутації, які передбачають зміни вхідних аргументів у коді. Наведений відрізок коду орієнтується на чітке та послідовне виконання команд для отримання підсумкового результату.

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

Приклад 2: Декларативний підхід

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

Чисті функції, або «Згинь нечистий код»

Чисті функції — це основні блоки функціонального програмування.

  1. Першочергова концепція чистих функцій: Same input = Same output. Це означає, що при одних і тих же вхідних параметрах функція завжди буде повертати той самий результат. Саме тому чисті функції передбачувані та стабільні.
  2. Друге — у чистих функцій немає побічних ефектів. У чистої функції має бути лише одна відповідальність. Якщо ваша функція повертає суму чисел, вона повинна робити тільки це і не відповідати за виконання сторонніх задач.
  3. Третє — чисті функції не залежать від зовнішніх даних. У цих функціях ми ніколи не використовуємо глобальних змінних.

Наведу кілька прикладів, щоб зрозуміти відмінність чистих функцій від нечистих.

Приклад 3: Нечиста функція

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

Для порівняння подивимося, як виглядає код чистої функції для цього завдання:

Приклад 4: Чиста функція

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

Розглянемо ще один приклад:

Приклад 5: Код нечистої функції

У нас є глобальне значення initialValue, оголошене в рядку 1. Крім цього, у нас є функція add, яка приймає за вхідний аргумент деяке число (а) і, як результат, здійснює підсумовування цього числа з нашою глобальною змінною (5 рядок коду). Після цього в рядках коду 8, 9 і 10 ми викликаємо цю функцію тричі, але щоразу вона видає нам різний результат. Це відбувається тому, що використовується глобальна змінна initialValue, внаслідок чого функція викликана з тими самими аргументами повертає різний результат.

Перетворимо цей код на код чистої функції:

Приклад 6: Чиста функція

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

Побічні ефекти та композиція функцій

В теорії чисті функції не повинні містити побічні ефекти (Side Effects). Однак на практиці ми часто стикаємося з необхідністю робити, наприклад, асинхронні запити. Функціональне програмування пропонує своє рішення — монади (Monad). Тут ажливо відзначити, що при роботі, скажімо, з UI-частиною, часто неможливо повністю дотримуватися парадигми функціонального програмування, тому доводиться вдаватися до компромісних рішень.

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

Розглянемо на прикладі, що це значить:

Приклад 7: Композиція функцій

Припустимо, у нас є дві функції: одна зводить змінну х в квадрат, а інша — додає до неї 2. Ми можемо викликати дві ці функції й отримати результат. Відповідно до законів математики, спочатку буде викликана функція addTwo, а потім — результат буде зведений у квадрат за допомогою square. Ми також можемо створити більш абстрактну функцію, яка зможе композувати дві і більше функцій:

Приклад 8: Compose

Функція compose бере на себе N-кількість інших функцій. Потім функція повертатиме функцію, до якої був застосований вхідний аргумент, і так до всіх функцій по черзі. Зауважимо, що в 11 рядку коду (де видно виклик кінцевого результату) дії з виклику функцій виконуються справа-наліво: спочатку виконується дія з додаванням до слова hello знака оклику, а потім дублюється слово, в результаті чого на екран виводиться: hello! hello!

Якщо ви бажаєте інвертувати цей процес виклику функцій, можете скористатися функцією pipe:

Приклад 9: Pipe

Ця функція виконує аналогічне завдання, не перевертаючи масив функцій і звично виконуючи його дії в послідовності зліва-направо: спочатку застосовується функція repeatTwice (повторення) і лише потім addExclamation (додавання знака вигуку).

Імутабельність і бібліотеки

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

Щоб уникнути непередбачуваної поведінки в коді, слід не мутувати (не змінювати) дані, а перезаписувати їх. Це стосується як масивів, так і об’єктів.

Приклад 10: Імутабельність

Якщо ми створимо константу JavaScript (1 рядок), а потім спробуємо її перезаписати (2), то з’являється помилка. Однак, якщо створити константу об’єкта (4) і спробувати перезаписати поле об’єкта (8), ми не отримаємо помилку.

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

Рекурсивна функція і функції вищого порядку

Рекурсивна функція — це функція, що викликає сама себе. Простіше кажучи, це заміна циклам у функціональному програмуванні. Слід пам’ятати, що рекурсія завжди повинна мати умови для виходу. В іншому випадку при досягненні максимальної глибини рекурсії заповниться Call Stack, що загрожує зациклюванням і збоєм.

Розглянемо приклад коду рекурсивної функції:

Приклад 11: Рекурсивна функція

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

Різновидом функцій вищого порядку (Higher-order Functions або HOF) також є комбіновані функції, які можуть приймати в себе функції у вигляді вхідного аргументу, повертати їх як результат або робити те й інше одночасно. До цих функцій належать map, reduce, filter.

У React також існує досить популярний концепт Higher-order Component (HOC), який дозволяє додавати функціонал до готових компонентів.

Карування

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

Порівняємо JavaScript код функцій з каруванням та без нього:

Приклад 12: Функція без карування

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

Приклад 13: Функція з каруванням

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

Особливості нефункціонального підходу у програмуванні

Давайте підсумуємо. Функціональний підхід у програмуванні НЕ повинен включати наступні атрибути:

  • Цикли (while, do...while, for, for...of, for...in). Цикли вважаються імперативними, тому при використанні функціонального підходу необхідно змінювати їх на рекурсію.
  • Let чи var. Ці змінні забезпечують мутації, а у функціональному програмуванні ми в першу чергу керуємося принципом імутабельності.
  • Void функції. Вони є побічними ефектами, тому у функціональному програмуванні від них доводиться відмовитися.
  • Мутація об’єктів. Вони не використовуються в методах ФП. Замість зміни об’єкта необхідно створювати на його основі новий (перезаписати).
  • Мутуючі методи для масивів (copyWithin, fill, pop, push, reverse, shift, unshift, sort, splice). Всі ці методи призводять до мутації масиву вхідних аргументів, тому у ФП їм краще знайти заміну серед map, reduce та інших.

Fantasy Land

Говорячи про функціональне програмування на JS, неможливо не розповісти про Fantasy Land — специфікацію JavaScript, яка описує алгебраїчні структури.

Розберемо найпростіший приклад алгебраїчної структури — Functor:

Приклад 14: Functor

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

Алгебраїчні структури

Бібліотеки Ramda та Sanctuary

Ще одна бібліотека, яка спрощує написання коду у функціональному стилі — Ramda.js. Вона включає в себе різні функції і є найзручнішою для початку ознайомлення з функціональним програмуванням на JS, адже містить безліч функцій-помічників (хелперів), які істотно полегшують написання коду. Ramda не реалізує специфікацію Fantasy Land, тому ця бібліотека не містить специфікації алгебраїчні специфікації даних, лише набір карірованих функцій.

Розглянемо роботу в Ramda на прикладі:

Приклад 15: R.Compose

На 3 та 4 рядках коду можна побачити функцію classyGreating, яка конструює вітання залежно від імені та прізвища. На 6 рядку коду зображено виклик функції R.compose, яка спочатку викликає функцію classyGreeting, а потім використовує функцію-хелпер R.toUpper, яка дозволяє внести результат у upper case. На 8 рядку можна побачити вже інший, складніший приклад використання R.compose з каруванням.

Приклад 16: R._

Бібліотека Ramda дозволяє навіть пропускати аргументи, використовуючи замість них R. Безліч інших реальних прикладів використання Ramda можна знайти в Cookbook за посиланням.

Sanctuary — це бібліотека, яка реалізує специфікації Fantasy Land (на відміну від Ramda). У ній представлені всі необхідні алгебраїчні структури та специфікації, що трохи ускладнює поріг входження при використанні бібліотеки.

Розглянемо, як реалізується задача щодо повернення кількості слів на прикладі Sanctuary:

Приклад 17: Повернення кількості слів у тексті Sanctuary

На 6 рядку коду можна побачити застосування функції S.pipe, яка містить 2 helper-функції. Вониу відповідають за підрахунок слів у тексті.

Основні переваги і недоліки функціонального програмування

Отже, маємо такі переваги:

  • Код, що читається. Це безперечно для кожного річ дуже суб’єктивна. Проте слід визнати, що математична логіка коду, написаного в декларативному стилі, набагато простіша для сприйняття.
  • Спрощена процедура тестування. Для тестування коду у ФП не потрібно створювати додаткових умов. Ви просто передаєте вхідні аргументи, а потім перевіряєте отриманий результат. Внаслідок цього процес тестування займає набагато менше часу.
  • Спрощене налагодження. Чисті функції набагато простіше дебатувати.
  • Передбачуваний код. Методи ФП гарантують передбачувану поведінку коду, що зводить до мінімуму можливість раптової появи помилок.

Крім цього, у функціонального програмування є кілька недоліків:

  • Високий поріг входження. ФП досить велика тема, для глибокого занурення у яку потрібен час. Це часто і відлякує багатьох програмістів-початківців.
  • Менш продуктивний код. JavaScript за замовчуванням не є повністю функціональною мовою програмування. Тому перед тим, як писати код у стилі ФП, необхідно зрозуміти, чи дійсно вам це необхідно. Часто золота середина — це вміле комбінування методів ФП та ООП.

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

Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU

👍ПодобаєтьсяСподобалось16
До обраногоВ обраному8
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

а якщо так зробити compose: const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);

Рекомендую глянути доповідь Uncle Bob’а — www.youtube.com/watch?v=7Zlp9rKHGD4

Нажаль тема монад та алгебраїчних функцій не розкрита (дерево функторів)

Монады без do-нотации... Не знаю, не уверен... Или есть что-то в JS?

В чем проблема отсутствия do-нотации?
Разве что в читабельности и привычке?

Куча вложенных лямбд, сравни

do
  a <- f
  b <- g
  c <- h
  return (a, b, c)

или

f >>= (\a -> g >>= (\b -> h >>= (\c -> return (a, b, c))))

ну можно так написать, но всё равно...

f >>= \a ->
  g >>= \b ->
    h >>= \c ->
      return (a, b, c)

А ещё добавь отсутствие перегрузки операторов, нету же >>=, наверняка это какой-нить bind, вот и получается мои фантазии

bind(f, a =>
  bind(g, b =>
    bind(h, c =>
      pure([a, b, c]))))

Ну не знаю...

В примера кода этого хаскелль, в котором сильная типизация. Однако в джаваскрипте такого нет, поэтому, иы можешь обычное тело функции рассматривать как сахар do-нотации. Ты в любом случае поддержку на уровне типов не получишь, а если использовать здесь тайпскрипт то либо fp-ts либо типы будут ломаться и все сведется к any. Так как они не настолько выразительные и структурные(прости, если где-то напутал тут).
Sanctuary — либа, которая позволит пофиксить такие штуки и в принципе еще свой тайп-чекер предлагает, но зачем, если уже проще использовать PureScript

Пока что я не могу представить, как тут сработает типизация. Да, по типу возвращаемого начения (например Maybe (a, b, c) или IO (a, b, c) компилятор Haskell может узнать, какую монаду или стек монад следует применить для выбора реализации для bind и return. В JS аналог скорее всего должен выглядеть так

m.bind(f, a =>
  m.bind(g, b =>
    m.bind(h, c =>
      m.pure([a, b, c]))))

тут нам явно надо указывать монаду m, которая связывает последовательность действий, потому что нет механизма вывода типов. Но больше типизация ничего не даёт, и мне в принципе непонятно, как можно вместо неё взять обычное тело функции, получить её AST и выполнить его так, как требует монада...

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

Подобные абстракции мы вряд ли увидим в скором времени в джаваскрипте, там с пропозалом пайплайн оператора все тяжко

С пропоузалом пайплайн оператора все было тяжко ещё год назад, но с тех пор как определились что он будет в hack стиле, дальше только ждём пока остальные аналогичные пропоузалы подтянутся, типа второй инкарнации bind, partial application и пары новых, чтобы решить кого брать а кого выкидывать, по тому что очевидно что что-то нужно очень, но все вместе они нафиг ненужны. Так что то что он будет — уже решено, но немного подождать ещё придется

Ради интереса, «чистая» функция это терминология js? Помнится в Oracle их звали детерминированными и даже спец.определение deterministic в сигнатуре метода используют.

Да, а потом у oracle это позаимвствовали в математику, cs и js.

Межпрочим, pure/deterministic разные понятия.

Я посмеялся, спасибо. Но я об этом как раз и спрашивал — это же или нет.
p.s. погуглил pure functions. Говорят, при одинаковых параметрах выдает одинаковый результат и no side effects. Иду в доку оракла читаю про deterministic, а они как-будто списали определение. Расскажите про разницу, пожалуйста. Буду искренне благодарен.

Pure — то, что вы написали. Детерминированной функция является, если на один и тот же параметр выдаёт всегда один и тот же результат, при этом функция может иметь полочные эффекты

Спасибо. Может они в Oracle смешали понятия, не знаю..вот определение из доки:
docs.oracle.com/...​CC-4334-9F43-0FBE88F92654

pure и total это терминология из функционального программирования больше, теория типов, всё такое. Остальные называют как хотят.

Внедрение элементов ФП в JS это, обычно, хорошо и приятно. Спасибо что способствуете распространению такого стиля

По статье есть пара замечаний:
1. Тема ООП vs ФП не раскрыта вообще. Почему-то подразумевается что ООП = императивный подход. Но это совсем не одно и тоже
2. Рекурсией, конечно, можно заменять циклы, но практически это очень плохая идея, которую вы-же сами не поддержание рекомендуя использовать map, reduce и filter, никакой рекурсии не подразумевающие

Тема ООП vs ФП не раскрыта вообще

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

потому что ООП это про структурирование знания предметной области, его, знания, декомпозиция.
а ФП это о кодинге вычислений.
они находятся на разном уровне абстракций, чтобы их можно было сравнить.
(именно поэтому в простых задачах и примерах ООП выглядит как болезнь «ООП головного мозга»)

Почему-то подразумевается что ООП = императивный подход.

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

ООП ж появился поверх императивного подхода, а не как его замена.
Подход создания DSL находится где-то на том же уровне, что и ООП

итого можно сравнивать
1. декларативный vs императивный
2. ООП vs DSL

ФП — частный случай декларативного, и не имеет отношения к уровню 2

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

Фп и ооп это разный способ решать одни и те же задачи. Стейтменты(ооп) — выражения(фп), Код блоки(ооп) — функции(фп), взаимодествие код блоков, обьектов в ооп(программа/модуль) — композиция функций.

Какого еще уровня абстракции не хватает? Или по твоему до ооп во времена структурного и функционального программирования не могли решить каких-то задач до появления ооп?

Фп и ооп это разный способ решать одни и те же задачи

нет, не одни и те же. о чем и написал.

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

Код блоки(ооп)

вы забыли расшифровать абревиатуру — ООП.
ни о каких «блоках» в ООП речи нет.
разве что в некоторых реализаций для удобства кодирования

взаимодествие код блоков,

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

очень разные уровни абстракций

Какого еще уровня абстракции не хватает?

того ради которого появился ООП

утрировано — расскажите о наработке в архитектуре приложений на — ФП
на ООП — полно. DSL — хоть не взлетел, но тоже предоставляет

ФП не предоставляет — ничего. Уже лет 15 как прошу от адептов — предоставить.
Чаще всего да, даже не врубаются о чем речь.

Или по твоему до ооп во времена структурного и функционального программирования не могли решить каких-то задач до появления ооп?

по моему любые задачи можно решить машинным кодом.

когда адепты ФП поймут причины появления и востребованности ООП — тогда может поймут что предложить

а пока на отличия французкой кухни от китайской они недоуменно спрашивают
так нет же разницы, молекулы одни и те же,
то да

ФП — не стал, и не станет мейнстримом

утрировано — расскажите о наработке в архитектуре приложений на — ФП
на ООП — полно. DSL — хоть не взлетел, но тоже предоставляет
ФП не предоставляет — ничего.Уже 15 лет как прошу от адептов — предоставить.
Чаще всего да, даже не врубаются о чем речь.

вот спутся 5 мин гугления, дальше можно имееть представление как организовать ФП программу и искать смежные материалы.
github.com/...​oftware-design-in-haskell
graninas.com/...​gn-and-architecture-book
fsharpforfunandprofit.com/...​s-of-looking-at-a-turtle
increment.com/...​-functional-architecture
blog.ploeh.dk/...​rchitecture-a-definition

вообще я думаю, что если бы ты хотел то и сам мог найти..

вообще я думаю

вообще, я думаю, если бы ты думал, то не бросал бы ссылки которые не отвечают поставленным мной вопросам, за 15 лет хайпа маргиналов «из высокой академической науки2 о победе ФП

и вместо классического демагогического приема — завалить ссылками на книги в сотни страниц — дал бы эскизно самостоятельно изложенный ответ.

обычно то заваливающие ссылками — сами из предложенного или не читали почти ничего, или ничего не поняли

P.S.
тем более что бегло глянув по тем что не встречал ранее
там
все тоже ООП, и все та же диспетчеризация вызовов
или как в одной из предложенных книг, после глав о базовых вещах
Part 3: Domain-driven design:
Embedded domain-specific languages

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

а то что предлагается на уровне кодинга — довольно быстро заимствуется мейнстрим языками, остающимися при этом — императивными.
Одерски потому и возмущается когда Scala обвиняют в недостотаточной функциональной чистоте:
«Да гибридный ЯП круче чистого ФЯ! Зачем делать этот шаг назад??»

Потому что ООП может надстраиваться как над императивным ЯП, так и декларативным.
Ему, ООП — пофик этот базис.
Еще раз, потому что оно, как и DSL подход находится на другом уровне — проектирования, декомпозиции, а не кодинга.

ООП поэтому даже пофик даже вид типизации, статическая или динамическая, строгая или мягкая.

Ты противоречишь сам себе:

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

->

P.S.
тем более что бегло глянув по тем что не встречал ранее
там
все тоже ООП, и все та же диспетчеризация вызовов

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

У тебя неправильные ожидание и восприятие инструментария, если ты можешь из него получить пользу — ты его используешь, там где он может быть удобен.
ООП в том виде в коим он фундаметльно описан встречаеться на практике не намного чаще чем ФП. Как правило нынче ООП называют все подряд — структурное программирование на классах, описание UI разметкы, обработку структур данных в императивных или функциональных алгоритмах и т.д. это и есть мейнстрим, а не ООП, где есть инкапсуляция состояния и взаимодействие через паблик методы обьектов.

и вместо классического демагогического приема — завалить ссылками на книги в сотни страниц — дали бы эскизно самостоятельно изложенный ответ.

Мы с этого и начали — но ты начал манипулировать и тебе такой ответ не подошел(но скорее всего ты даже непонял).
Я тебе ответил в первом посте:

Фп и ооп это разный способ решать одни и те же задачи.
Стейтменты(ооп) — выражения(фп);
Код блоки(ооп) — функции(фп);
Взаимодествие код блоков, обьектов в ооп(программа/модуль) — композиция функций(фп);

Этого обьясняет максимально простым языком как строиться и структурируеться ФП программа по аналогии с ООП по ссылкам эти вещи детализированны на уровне отдельных подходов.

обычно то заваливающие ссылками — сами из предложенного или не читали почти ничего, или ничего не поняли

Норм ссылки, отлично колбасят различные data flow на проде на протяении 2х лет уже для 100к юзеров(вполне рабочие можешь поверить).

Ты противоречишь сам себе

«ясно. понятно.»

отлично колбасят различные data flow

вопросов больше нет.

Потому что ООП может надстраиваться как над императивным ЯП, так и декларативным.

Интересно посмотреть, как ООП можно надстроить над SQL, над Prolog, над Haskell...

Вообще, создаутся такое ощущение, что ООП это всё хорошее, и всякий раз, когда речь идёт про выделение некоторой сущности, то мы употребляем термин ООП.

Я же всё-таки считаю, что обязательным атрибутом ООП есть наследование классов, и даже Go и Rust уже не будут чистыми ООП языками. Ну и в целом, если полиморфизм реализован суммой типов, то ООП в таком языке будет выглядеть уже сбоку припёку.

Ну и в целом, если полиморфизм реализован суммой типов, то ООП в таком языке будет выглядеть уже сбоку припёку.

Базовое требование ООП:
Что бы класс ментейнил валидное состояние обьекта всегда и его можно было изменить только через набор публичных контрактов(методов), что инкапсулируют набор логики в рамках экземпляра.

Базовое требование ФП:
Что бы фукнция получала инпут и давала детерминированный атупут всегда, без сайд эффектов.

Я бы не воспринимал тот бред, что пишет Сергей в серьез — если человек не понимает , где в корне эти требования расходятся и через одно нельзя смоделировать другое. Куда там рассаждуть уже о более высоком уровне моделирания — как например апликейшина.

Когда я читал Греди Буча, то три кита ООП были инкапсуляция, наследование, полиморфизм. А твоё требование всё сводит к инкапсуляции. С позиции этого определения, какой-нить Haskell будет ООП, потому как там просто невоможно сконструировать невалидное состояние данных благодаря алгебраическим типам. И вообще, если мы не можем изменять объект, но заботится о его валидности надо только в момент создания. Также любой модуль и любая либа может рассматриваться как ООП :)

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

когда речь идёт про выделение некоторой сущности, то мы употребляем термин ООП.

вот именно.

в «data flow» которыми хвалятся функциональщики, по самой задаче — сущностей нет
есть преобразования примитивных данных.
в таких задачах выделять сущности, неважно по какой методологии — это оверпроектирование

Интересно посмотреть, как ООП можно надстроить над SQL, над Prolog, над Haskell...

ну вот они и — нишевые :) А Prolog так вообще скорее мертв чем жив.

создаутся такое ощущение, что ООП это всё хорошее

оно не хорошее. оно просто безальтернативное на сейчас.

может функциональщикам еще лет 15 дать, что-то выродят.
Хотя OCaml и F# - уже неплохи — потому что там ООП надстроили :)

Я же всё-таки считаю, что обязательным атрибутом ООП есть наследование классов

а я так — не считаю.
про это много кем и по разному написано.

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

тут все презрение к философии(=невежество) и сказывается, люди не врубаются в смысл того что зазубрили.

и даже Go и Rust уже не будут чистыми ООП языками

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

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

Дауж, Серега как обычно. Учился училчся и недоучился.
ФП — это тебе не фибоначи посчитать. Новички всегда путают ФП с обычными функциями. Это всегда попытка найти минимально необходимый мат аппарат функциональных преобразований для решения в интуитивных композициях сложнейших задач. Там где такой матаппарат найден и построен, код писать одно удовольствие. Дебажить ничего не надо, учебники в пару страниц где и школьник разберется, все работает с первого раза. Пример — тотже LINQ, основа SQL, подходы в нейросетях.

ООП — это просто набор корзин, зачастую с плохо пахнущим императивным кодом. Там где не смогли найти лаконичный мат аппарат, лепят эти модули с красивыми названиями и вкраплениями полиморфизма.

ФП всегда решает задачу на более высоком уровне, пытается разложить задачу математически правильно.
ООП, это просто императивный стиль написания кода, где основным прорывом стала всетаки инкапсуляция. Благодаря ей стало возможно собирать пятиголовых монстров с армией быдлокодеров и высокой текучкой кадров на производстве. В ООП есть конечно еще наследование, но сначала поняли что множественное наследование маст дай и убрали его почти со всех языков, а потом и обычное наследование начали рекомендовать заменять композицией.

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

В большинстве случаев ФП это не нужный оверхед в проекте.
Сложнее дебажить, растет порог входа.

Когда-то тоже дрочил на ФП, но потом понял что это просто казалось круто)
А на самом деле круто это то что простое как лист бумаги.

ООПшные шаблоны проектирования тоже требуют изучения, прежде чем писать код. Подскажите, в каком месте вам сложнее дебажить чистые функции или их композиции?
В JS фп это как солид, если вы его нарушите — мир не рухнет. Однако плюсов будет больше

ООПшные шаблоны проектирования

Никто их в здравом уме не применяет на реальных проектах, максимум один-два на весь проект.

Это ты базируясь на какой выборке вывод сделал? На беке часто применяют. Я не пишу на ангуляре, но подозреваю, что там тоже

Ну... если у нас какой-то проект на JS, и вдруг кто-то начинает активно использовать фукнциональные вещи, то тут в общем-то будет напряг для многих. Но если разрабатывать проект, например, на Haskell, это будет как-бы не оверхед, а суть.

Статья о ФП в ЖС, потому относительно ЖС проектов и говорю)

Ключевое слово «вдруг». Если «вдруг», то да — проблема для всех.

Я постоянно имею дело с Java, начиная с 8-ой версии. Так и я, и сотрудники (на разных работах) пишем много кусочков кода именно в функциональном стиле, т.к. читать его легче, да и выглядит он приятней.

Функциональный стиль, и без монад и do-нотации???

главное — бирочка :)

я встречал людей которые уверенно называют функциональным — процедурное программирование :)
«у нас же функции, верно? значит это функциональное программирование!»

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

Єдине запитання: Які проблеми, задачі чи класи задач ФП вирішує краще ніж ООП?

Добавляет к пипке +20 сантиметров, и +99 к самооценке

обробка даних, обчислення

Тобто алгоритм написаний на ФП реально швидше працює з великими наборами даних ніж той самий алгоритм реалізований за допомогою ООП?

И да, и нет, там есть свои ограничения. Всё зависит от выбраного ЯП. Функциональные языки они же узкоспециализированые изначально, то что их начали сунуть везде, это случайно получилось. Для high-load систем оно конечно рулит и даёт плюшки, но для рядовых задач, это понты. Ладно для себя изучать, но пихать в прод такое себе, ведь это придётся потом поддерживать другим людям.

Процесс програмування більш ефективний. Наприклад, простіше тести писати. Також про ФП є вислів: «easy to reason about», тобто програмуючи або дивлячись на окремі функції можна зрозуміти, що буде в результаті, а чого не може бути. Про Haskell взагалі кажуть, що якщо скомпілилось то скоріш за все програма працює

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

Подивіться на SQL. Це дуже хороший приклад функціонального програмування, який було би дуже важко відтворити в процедурному (імперативному) коді.

Скоріше за усе це приклад декларативного підходу, коли ми описуємо що ми бажаємо отримати, та є досить агато шляхів отримати це. Цікаво, що якщо брати SQL, то оптимізація запросів це часто езотеричний скіл імперативного програмування у мові SQL, коли ми знаємо як імперативно треба виконикувати запрос, й нам треба ці знання донести до оптимізатора, який створить потріний нам план. Але, як на мене, до функціонального програмування SQL відношення не має ніякого, бо фунції за звичай не є first-class громадянами у SQL: ми не можемо їх створювати, не можемо їх повертати, таке інше.

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

Це дуже провокативне запитання, особливо тому що треба розтлумачити що таке «краще», ФП, ООП, ... З’ясувати що краще це часто взагалі питання смаку. Багато холіварів на тему, яка мова програмування «краще», але майже немає ніяких об’єктивних даних на цю тему.

ООП є багато різних видів, від Simula-like ООП з таблицями віртуальних функцій до стиля Go де наслідування замінено на інкапсуляцію та інтерфейсами, до качкового ООП, це не кажучи про Smalltalk, ... Чим JS не ООП, якщо усе це об’єкт?

ФП теж розуміється досить широко. Від можливості використовувати фукнції у якості значень, до чистих функціональних мов, або навіть до мов з залежними типами. Якщо ФП розуміти досить широко, то на JS можна писати и ООП і ФП одночасно, тому незрозуміло про що запитання? Чим краще їсти, ножем чи виделкою?

Якщо брати ООП, то це для мене більше організація даних, і тут альтернатива, скоріше за усе, не ФП, а алгебраїчні типи (Rust, Haskell, Idris, Agda). Тут, як раз, два різних підходи до поліморфізму, або сума типів, або інтерфейси. Мої симпатії більше на боці алгебраїчних типів, як на мене вони дають більше контролю, багато несумісних станів просто неможливо створити.

Якщо дивитися на Hackage, то там є майже усе, при цьому розробляється досить невеличким ком’юніні, на відміну від python. Це натягає якби на те, що розглядати чисте ФП можна для будь якої задачі, можливо окрім low level (хоча є операційні системи на Haskell), та великого performance, але там й питання ООП під питанням.

Складні обчислення — дуже зручно, т.я. зазвичай можна відкидати цілі гілки обчислень. Паралельні процеси — взагалі без варіантів. Он та сама функціональна мова Erlang виникла, коли Ericsson запарився ловити помилки в коді свого багатопоточного обладнання.

Он та сама функціональна мова Erlang виникла

при цьому там був обраний — підхід акторів, тобто пращур ООП :)
Дуже схожий на Smalltalk

Ericsson запарився ловити помилки в коді свого багатопоточного обладнання.

Ericsson насаперед вирішували зовсім іншу проблему — оновлення коду систем без їх зупинки

А потом говорят что JS медленный и прожорливый

Так про всё говорят, где принимают участие виртуальные машины, интерпретаторы или v8. Но пусть говорят шо хотят, потому что Java жива, javascript в каждой микроволновке, питон тоже в каждой жопе затычка, ну и главное: «Ruby — жив, но потерял часть популярности:)»

Просто неймовірна стаття, дякую за корисну інформацію!

Просто неймовірна практика реєструвати екаунт щоб себе похвалити.

Очепятки, которые заметил
В блоке

Бібліотеки Ramda та Sanctuary

тут лишний раз используется слово специфікації

тому ця бібліотека не містить специфікації алгебраїчні специфікації даних

Под Прикладом 17

Вониу відповідають ...

Аксиома Эскобара ©

Одно замечание к статье: Функциональный код не читается и не дебажится от слова вообще

Прекрасно читається і дебажиться, просто нормально писати треба ;)
«Програму на Фортрані можна написати будь-якою мовою програмування»

Ну покажи как поставь бряк или лог/ассерт вывести в фильтре перед редьюсом

Читаемый функционаыный код для дебагинга, без дикого рефакторинга покажи, а не теорию нам проповедуй :)

О каком диком рефакторинге речь? И где я теорию проповедую? У тебя, видимо, представление о функциональном коде ошибочное. Оно мало чем отличается от императивного, для реальных задач

об обычном рефакторинге под названием «cделать внутренюю переменную и брякнуть ее значение» в цепочке офигенно читаемых pure functions©

Монада Writer, которая помогает сопроводить последовательность действий каким-то выводом

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

Тим і живемо :). ФП для того і вигадали щоб було цікавіше в інеті сратися.

Может ооп? Фп помнит мир без классов..

класы — это способ типизации, а не обязательный признак ООП.

акторы — вот главный признак.
Если систему декомпозируют на акторы, их взаимодействие — то это и есть главный признак что мы имеем дело с ООП.
правила же этого взаимодействия могут быть разными.

это как с обычными языками
есть аналитические, есть синтетические

хотя на вид кажется что между языками одной группы ничего общего

Якщо достатньо складну систему декомпозувати тільки на актори — то вийде непідтрімувана та невідлагоджувана купа кода. Бо мало того, що кожен актор мейнтейнить свій стейт, так воно все ще й обмінюється месагами асинхронно (і відповідно може міняти свиій стейт — теж асинхронно). «Happy debugging, motherfuckers»!

Хто не вірить — akka у руки і вперед ліпити актори замість класів. А потім все це ще й покрити тестами. Я вже не каже про якісь формальні докази над таким кодом — воно можливо, але асинхронність тільки додає складності, не спрощує.

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

Власне, як і класи/об’єкти. Теж корисні в досить обмеженому контексті. І спроби працювати в стилі «а давайте спробуємо вдати, що все є об’єкт і моедлювати сістему за цим» нагадують «а давайте уявимо, що з усіх інструментів в нас є тільки молоток» — результат буде відповідним.

Отже досить тривіальна штука, яку в ФП мовах використовують лише за потреби, і яка не має ніякого _формального_ визначення — якось розрослось до вундервафлі яку можна використати для усього.

Якщо достатньо складну систему декомпозувати тільки на актори

а навіщо її декомпозувати тільки на актори — одного рівня/типу?

Хто не вірить — akka у руки і вперед ліпити актори замість класів

Що такє — клас?
Чим клас відрізняється від типу?
А що такє — тип?

Актор — це досить низкорінева штука

так.
функція — теж, досить низкорінева штука.

І спроби працювати в стилі «а давайте спробуємо вдати, що все є об’єкт і моедлювати сістему за цим»

Тобто ви взяли за аргументацію:
Доказ солом’яної людини
Оманливе доведення, схоже на доведення до абсурду, яке часто зустрічається в полемічній дискусії — це «опудало» — логічна помилка
(вікіпедія)

Так, в цілому в холіварах демагогія головний інструмент:

Отже досить тривіальна штука

Тому й не цікаво спростовувати, ту «солом’яну людину» що ви досліджуєте.

функція — теж, досить низкорінева штука.

Але на основі цієї штуки побудовані формальні штуки, з якими вже можно працювати. Усе ще напрацьовано математиками за всю історію людства.

Що в нас є на основі ООП? Патерни? Чи є визначення хоча б одного нвписне так, щоб можна було однозначно сказати щось типу «якщо це патерн, скажем фасад чи відвідувач — то з цього слідує ...». Чи навпаки — «якщо об’єкт має такі і такі властивості — то це патерн XYZ».

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

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

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

А коли ми вже не так обмежені в ресурсах — то питання «А нафіга мені юзать нізькорівневий ООП та самому відловлювати проблеми сайд ефектів та мутабельного стейту» — вже не буде мати відповідлі типу «бо в цей контролер не лізе». Особливо враховуючі додадкові кошти на якийсь Пітон чі Рубі — які все одно використовують не зважаючи не ці кошті.

Нуво программисты не пишут логи и не дебажат код? :-)

Ну... assert это императивная пакость, обычно если брать алгебраическую систему типов, и зависимые типы, то часто просто невозможно добиться того, чтобы возникала такая ситуация, когда в параменной хранится невозможное значение или комбинация значений. Если у тебя тотальная функция, то в принципе ты уверен, что (1) она что-то вернёт для любых входных данных без всяких непридвиденных ситуаций и (2) она не зациклится.

Логирование... опять же, если у нас развитая система типов, то в целом потребности в отладке резко снижается, вместо отладки ты просто делаешь компилятор счастливым, и после этого всё часто работает без отладки. Но в целом что мешает тебе сделать?

test :: MonadLog (WithSeverity Doc) m => m ()
test = do
  let xs = filter (>2) [1, 2, 3, 4, 5]
  logMessage $ WithSeverity Informational $ "Some info" ++ (show xs)
  let acc = foldr (+) 0 xs
  logMessage $ WithSeverity Error $ "Critical info" ++ (show acc)

и потом

>>> runLoggingT test (print . renderWithSeverity id)

Ну вот я говорю: гавно кодинг шо так шо этак :)

Для лога/ассерта внутри цепочки преобразований в lodash есть функции tap и thru — из использовать потребует 0 изменений кода, только давления `.tap(console.log)` в цепочку. Конечно, если цепочка построена с использованием lodash. С брейкпоинтами проблемы не понял — где именно их проблемно ставить?

Покажи скриншот бряка и значение/стек которое ты видишь? Шо нет данных? Так что ж это за деабагинг тогда? :)

P.S. Ржу c молодежи... Лодаш с Адерскором и Рамду в проект тащить чтобы «код дебажить». А ничего что сами либы с глюками?

Неполенился, заскринил — ibb.co/mRC2rRB — бряка с правильным текущим значением одной из итераций. Неполенитесь и вы заскринить что-же у вас неработает. Или хотя-бы просто код сюда выложить. А-то может это не глюки а руки? ;)

Ну тоесть ты в упор не видишь что значение в .map() попасть вообще не должно (хотя может тебе и нечетные надо, но где тогда 1?), но тебе норм ибо бряк типа поставился ;)

Кстати, а arrow функции с одинаковым сигнатурами и названием параметров это тоже функциональное программирование? Они всегда pure? Может это все-таки структурные костыли? ;)

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

В общем Аксиома Эскобара © как я и написал в начале...

1. Первая функция пропускает только числа с не нулевым остатком от деления на 2. 3 % 2 == 1, так что 3 — правильное значение
2. На скриншоте вторая остановка на breakpoint — первая была со значением v: 1

Либо признайте что были неправы когда писали о проблемах с дебагингом / логированием, либо покажите проблемный код, потом уже будем менять тему, если обоим будет это интересно

Хіба якщо ти нуб. Багато писав на скалі, набагато краще цієї вашої імперативщини. Зараз пишу на пітоні. Норм мова, але жаль, що під ФП вона заточена практично ніяк.

Писать мало, попробуй через полгода прочить ото все что понаписывал ;)

Чтения кода это отдельный навык. И это уже не в подходе проблема, если ты код прочесть спустя полгода не можешь, тут другие факторы играют ;)

Ну... я часто читаю код, который писали другие. И в чём проблема читать? Лично для меня самое неприятное это стиль ООП равиоли

Значит ты альтернативно одаренный :)

Не знаю, не знаю. Как по мне, то читается ФК часто легче (если не перегибать во время написания, хотя от этого не застрахована ни одна парадигма). А что касается классической отладки, то конечно нельзя, подследовательности действий-то нету. Вся отладка сводится к чтению значений параметров и результатов функций. Лично мне больше ничего не понадобилось пока и очень даже удобно.

Ну чисте фп на мові яка до цього не пристосована це дорого а от фп стиль краше
Я от відео про це записував після 16 років з мовою
youtu.be/fqJ5u46lEyY

круте відео, і загалом канал, лайк, підписка

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