Чому програмісти мають вивчити Haskell, навіть якщо нічого не будуть на ньому писати

Привіт. Я Лущик Павло, працюю програмістом в невеликій компанії, що в основному займається приладобудуванням. Тут не буде монад. В цій статті я продемонструю цікавий метод програмування, який я відкрив для себе під час вивчення Haskell.

Оскільки я в основному пишу на С++, то майже весь код буде на С++ і Haskell. Але спробую написати код ідіоматично і він буде без деструкторів, конструкторів копіювання/переміщення та інших специфічних для С++ речей.

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

struct tree {  
  bool is_leaf;  
  union {    
    int leaf_value; 
    struct {
      tree *node_left;
      tree *node_right;
    };
  };
  tree(int value) : is_leaf(true), leaf_value(value) {}
  tree(tree *l, tree *r) : is_leaf(false), node_left(l), node_right(r) {}
};

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

Найпростіший розв’язок виглядає так:

int tsum(tree *x) {  
  if (x->is_leaf) return x->leaf_value;  
  return tsum(x->node_left) + tsum(x->node_right);
}

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

1. Факторіал з акумулятором

Почнемо з магічного Haskell. Є факторіал:

fact :: Int -> Int 
fact 0 = 1
fact x = x * fact (x-1)

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

fact_acc :: Int -> Int -> Int
fact_acc a x = a * fact x      (визначення)

Перше, що ми можемо побачити, це те, що:

fact_acc 1 x = 1 * fact x

Тобто:

fact x = fact_acc 1 x

Зазначаємо, що fact можна визначити й через fact_acc.

Друге — спробуймо позбутися fact у визначенні fact_acc. Це ніби як схоже на розв’язок звичайних алгебраїчних рівнянь.

Спочатку розглянемо частинний випадок x = 1. за визначенням:

fact_acc a 1 = a * fact 1 = a

А тепер загальний випадок:

fact_acc a x =  a * fact x =  (підставляємо визначення fact)
  a * (x * fact (x-1)) =      (використовуємо асоціативність множення)
  (a*x) * fact (x-1) =        (використовуємо визначення fact_acc)
  fact_acc (a*x) (x-1)

Складемо все до купи й отримаємо повне визначення fact_acc:

fact_acc :: Int -> Int -> Int
fact_acc a 1 = a                      -- (*1)
fact_acc a x = fact_acc (x*a) (x-1)   -- (*2)

А тепер напишемо аналогічний код на C++.

int fact_acc(int a, int x) {
  if (x == 0) return a;         // (випадок 1)
  return fact_acc(a*x, x-1);    // (випадок 2)
}

Такий код можна майже механічно перетворити на ітеративний.

int fact_iter(int a, int x) {
  while (true) {
    if (x == 0) return a;       // (для випадку 1)
    a *= x;                     // (для випадку 2)
    x -= 1;
  }
}

2. Фібоначчі на акумуляторах

Наступний приклад складніший — числа Фібоначчі (неочікувано).

fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib x = fib (x-1) + fib (x-2)

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

fib_acc a b x = a * fib x + b * fib (x-1)

Можна побачити, що:

fib_acc 1 0 x = 1 * fib x + 0 * fib (x-1)

З чого слідує визначення fib через fib_acc.

fib x = fib_acc 1 0 x

Визначимо fib_acc через саму себе. Частинний випадок, коли x = 0 не має розв’язку, бо fib (-1) не визначено, але оскільки ми знаємо, що fib 0 = 0, то можемо прийняти:

fib_acc a b 0 = 0

Для одиниці:

fib_acc a b 1 = a * fib 1 + b * fib 0 = a

Тепер загальний випадок:

fib_acc a b x =  a * fib x + b * fib (x-1) =       (визначення fib для першого додатку)
  a * (fib (x-1) + fib (x-2)) + b * fib (x-1) =    (використовуємо дистрибутивність множення)
  (a + b) * fib (x-1) + a * fib (x-2) =            (за визначенням fib_acc)
  fib_acc (a+b) a (x-1)

Отже:

fib_acc :: Int -> Int -> Int -> Int
fib_acc a b 0 = 0                         -- (*1)
fib_acc a b 1 = a                         -- (*2)
fib_acc a b x = fib_acc (a+b) a (x-1)     -- (*3)

Так це виглядає на C++:

int fib_acc(int a, int b, int x) {
  if (x == 0) return 0;                // (для випадку 1)
  if (x == 1) return a;                // (для випадку 2)
  return fib_acc(a+b, a, x-1);         // (для випадку 3)
}

Майже механічно перетворюємо цей код на ітеративний:

int fibi(int a, int b, int x) {
  if (x == 0) return 0;             // (для випадку 1)
  while (true) {
    if (x == 1) return a;           // (для випадку 2)
    a = a + b;                      // (для випадку 3)
    b = a - b;
    x -= 1;
  }
}

Сподіваюся, тут все зрозуміло. А тепер повернемося до нашої першої задачі з деревами.

3. Задача про дерева на акумуляторах

Почнемо з магічного Haskell, визначивши спочатку тип дерева і функцію tsum.

data Tree =
    Leaf Int
  | Node Tree Tree

tsum :: Tree -> Int
tsum (Leaf x) = x
tsum (Node l r) = tsum l + tsum r

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

reduce :: [Tree] -> Int
reduce [] = 0
reduce (x:ys) = tsum x + reduce ys

А тепер визначимо tsum_acc як функцію з двома акумуляторами:

tsum_acc :: Int -> [Tree] -> Tree -> Int
tsum_acc a xs x = a + reduce xs + tsum x

Як і у попередніх випадках, одразу можна побачити, що:

tsum_acc 0 [] x = 0 + reduce [] + tsum x = tsum x

Тобто:

tsum x = tsum_acc 0 [] x

Проаналізуємо частинні випадки:

tsum_acc a [] (Leaf x) = a + reduce [] + tsum (Leaf x) = a + x

Далі випадок для непустого списку:

tsum_acc a (y:ys) (Leaf x) =
  a + reduce (y:ys) + tsum (Leaf x) =       (визначення reduce і tsum)
  a + tsum y + reduce ys + x =              (комутативніть додавання)
  (a + x) + reduce ys + tsum y =            (визначення tsum_acc)
  tsum_acc (a+x) ys y

Випадок для Node:

tsum_acc a ys (Node l r) =
  a + reduce ys + tsum (Node l r) =         (визначення tsum)
  a + reduce ys + tsum l + tsum r =         (комутативніть додавання)
  a + tsum l + redure ys + tsum r =         (визначення reduce)
  a + reduce (y:ys) + tsum r =              (визначення tsum_acc)
  tsum_acc a (y:ys) r

Отже, маємо:

tsum_acc a [] (Leaf x) = a + x                     -- (*1)
tsum_acc a (y:ys) (Leaf x) = tsum_acc (a+x) ys y   -- (*2)
tsum_acc a ys (Node l r) = tsum_acc a (l:ys) r     -- (*3)

Телепортуємося у С++. Тут як список я використаю перевернутий std::vector.

int tsum_acc(int a, std::vector<tree *> xs, tree *x) {
  if (x->is_leaf) {
    if (xs.empty()) return a + x->leaf_value;             // (випадок 1)
    a += x->leaf_value;                                   // (випадок 2)
    x = xs.back(); xs.pop_back();
    return tsum_acc(a, xs, x);
  }
  xs.push_back(x->node_left);                             // (випадок 3)
  x = x->node_right;
  return tsum_acc(a, xs, x);
}

Що знову ж таки майже механічно можна перетворити на ітеративний код:

int tsum_iter(int a, std::vector<tree *> xs, tree *x) {
  while (true) {
    if (x->is_leaf) {
      if (xs.empty()) return a + x->leaf_value;     // (випадок 1)
      a += x->leaf_value;                           // (випадок 2)
      x = xs.back(); xs.pop_back();
      continue;
    }
    xs.push_back(x->node_left);                     // (випадок 3)
    x = x->node_right;
  }
}

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

Історія успіху та висновки

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

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

Дізнався я про цей метод з книжки Programming in Haskell від Graham Hutton. Також рекомендую Calculating Correct Compilers того ж автора. Надзвичайно цікава стаття, яка використовує цей метод для складніших задач.

science bitch

Дякую за увагу.

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

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

Лучше всего изучить Go, и больше не иметь дел с вышеописанной лабудой.

Продолжаю удивляться, почему гопники такие агрессивные. Ах да, подождите...

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

Лишнее абстрагирование зачастую негативно отражается на перфомансе, и ещё бонусом идёт плохая читаемость кода. К тому же «серебряную пулю» на все случаи жизни всё равно сделать не выйдет. Наиболее практичен подход, следующий принципу KISS, и бритве Оккамы.

Для ООП мов програмування це зазвичай так, бо додаткове абстрагування це зазвичай додатковий прошарок класів. Haskell як раз цікавий тим, що дозволяє створювати абстракції, які одночасно є простими (читай KISS) та потужними, а код зрозумілий та лаконічний.

В мене не так багато вільного часу, і, особисто я, його краще витрачу на вивчення нових фіч С++20/23.

А все ж таки:

Чому програмісти мають вивчити Haskell, навіть якщо нічого не будуть на ньому писати

?

З тієї ж причини, що матан колись зубрили

Я не зубрив)

А, ну то воно може зробити когось кращим програмiстом, але це не точно)

Наука + математика!=Вміння донести та аргументувати свою точку зору до інших

тобто ви погоджуєтеся, що вивчити хаскель корисно, навіть якщо ви не будете на ньому писати?

Ні Павле, бо ж ви так і не розкрили посил

Чому програмісти мають вивчити Haskell, навіть якщо нічого не будуть на ньому писати

. У вас просто клікбейтний заголовок.Без образ.

Ви це хотіли сказати?

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

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

2)Глибше розуміння типових систем: Сувора типізація та складні типові конструкції Haskell допомагають краще зрозуміти значення типів у інших мовах, а також попереджати помилки за допомогою компілятора.

3)Формування «функціонального менталітету»: Навіть у мовах, які підтримують функціональний стиль лише частково (наприклад, JavaScript, Python чи Java з лямбда-функціями), знання функціональних принципів дозволяє писати більш елегантний, зрозумілий і надійний код.

4)Культурний та інтелектуальний розвиток: Haskell — це свого роду «інтелектуальне випробування» для програміста. Це розширює технічну ерудицію та загальну естетичну чутливість до краси й логічної витонченості коду.

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

Це едина МП, в сенс якої я не «вїхав»...

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

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

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

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

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

А по факту все важливе і важке завжди переписується на плюси (зараз інколи раст)

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

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

екосистема Haskell також розвивається, його можна знайти у fintech

За якими метриками розвивається? Prolog, Perl розвиваються?

Які найбільші фінтех де воно активно використовується? Я знаю лише рандомні крипто скеми. Зайшов на список топ проектів на гітхабі, і там декілька в топі то заархівовані проекти. В списку компаній наведених раніше або не використовують, або активно переписують з нього.

За якими метриками розвивається?

Вихід нових версій компілятора, в який активно додаються нові можливості. Покращення в cabal (менеджер пакетів), пакети постійно оновлюються та з’являються нові.

Досі такої не почув, а лише чую про те ще не розумію монади.

Ще раз, більше надійність коду за рахунок ADT. Відчуття, що якщо воно компілюється, то воно працює. ADT використовується в Rust, наприклад, також. Велика кількість помилок, які я припускаю, це коли сукупність змінних приймає значення, про яке я не подумав що воно може виникнути. Якщо брати Haskell, то немає змінних, немає проблеми :-)

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

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

Ок, візьмемо наприклад C#, чи Python, чи Rust До якої реальної проблеми?

пакети постійно оновлюються та з’являються нові

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

Далі, за рахунок монад краще контрольованість за ефектами

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

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

візьмемо наприклад C#, чи Python, чи Rust До якої реальної проблеми?

C# та python — можна знайти купу девелоперів хто згодні писати, майже під все є нормальний сдк чи ліба. Легко заскейлити супорт 24/7 по світу. Тулінг в який вливають багато бабла що робить девелоперів більш продуктивними.

Rust — memory safe c++, трохи плюшок зверху, і дає офіційно дозвіл на пофарбування волосся в зелений і всім казати що ти пишеш на Rust, коли ніхто не питає.

Для референсу в хаскелі 20к пакетів. Це в 5 раз менше ніж раст і значно менше ніж всякі го та жс.

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

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

Ну.. не зовсім. Проблема не в інтерфейсі, а в тому, чи можна це зробити поза цим інтерфейсом. У Haskell єдиний спосіб зробити side effect це монади. А в інших мовах тобі ніхто не завадить зробити щось поза цим інтерфейсом, та ніяких помилок компіляції не буде. Тобі треба відправити HTTP запит? Ну пишеш ти request.get('https://my.cite'), і це буде працювати.

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

Виглядає як сюр, бо нормально реалізувати монади можна лише на чистій функціональній мові. Це довід типу «Ось усі кажуть: „Карузо! Карузо!“ А я послухав — так нічого особливого» — «Ви чули Карузо?!» — «Ні. Мені Рабінович наспівав» А потім з’ясується, що то були і не монади.

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

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

Ну.. не зовсім. Проблема не в інтерфейсі, а в тому, чи можна це зробити поза цим інтерфейсом. У Haskell єдиний спосіб зробити side effect це монади. А в інших мовах тобі ніхто не завадить зробити щось поза цим інтерфейсом, та ніяких помилок компіляції не буде. Тобі треба відправити HTTP запит? Ну пишеш ти request.get(’https://my.cite’), і це буде працювати.

Ну здається ця проблема вирішена не один раз, без монад. В основному комбінація з build rules, лінтів, коміт хукс на імпорти/залежності. В проектах на тисячі людей працювало добре без монад.

Виглядає як сюр, бо нормально реалізувати монади можна лише на чистій функціональній мові. Це довід типу «Ось усі кажуть: „Карузо! Карузо!“ А я послухав — так нічого особливого» — «Ви чули Карузо?!» — «Ні. Мені Рабінович наспівав» А потім з’ясується, що то були і не монади.

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

Якщо є якісь інші метрики, то цікаво почути.

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

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

Ну здається ця проблема вирішена не один раз, без монад. В основному комбінація з build rules, лінтів, коміт хукс на імпорти/залежності. В проектах на тисячі людей працювало добре без монад.

Я не знаю розв’язку та не бачу як це можливо. Це просто загальні слова. Припустомо, що у нас є певна функція increment_counter (збільшення глобального лічильника). Які саме механізми зможуть гарантувати мені, що функція some_user_func ніколи не викличе функцію increment_counter?

Наприклад, є функція another_user_func, яка викликає колись increment_counter. Проект проходить перевірку. Я змінюю функцію some_user_func таким чином, що вона викликає функцію another_user_func. Я хочу отримати помилку перевірки.

global some_counter
def increment_counter():
    global some_counter
    some_counter += 1

def another_user_func():
    print('another_user_func')
    increment_counter()

def some_user_func():
    print('some_user_func')

це має проходити перевірку. А от коли ми замінюємо some_user_func на

def some_user_func():
    print('some_user_func')
    another_user_func()

у нас має бути помилка.

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

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

import Control.Monad.State

type Counter = Int

newtype StatMonad a = StatMonad { runStatMonad :: State Counter a }
  deriving (Functor, Applicative, Monad, MonadState Counter)

incrementCounter :: StatMonad ()
incrementCounter = modify (+1)

doubleWithStat :: Int -> StatMonad Int
doubleWithStat x = do
  incrementCounter
  return (x * 2)

double :: Int -> Int
double x = x * 2
-- double x = doubleWithStat x 
-- Couldn't match expected type `Int' with actual type `StatMonad Int'

main :: IO ()
main = do
  let initialState = 0
      (result, finalState) = runState (runStatMonad $ do
        let y1 = double 5            
        y2 <- doubleWithStat 7
        y3 <- doubleWithStat 11
        let y4 = double 13            
        return (y1, y2, y3, y4)) initialState
  putStrLn $ "Results: " ++ show result
  putStrLn $ "Final counter: " ++ show finalState

Stdout:

Results: (10,14,22,26)
Final counter: 2
Або як з комунізмом, просто кожен приклад це не правильний комунізм :) Наступний раз зроблять правильні номади тоді точно запрацює.

Я таке багато чув про ООП, та самі жахливі архутектури, які я бачив, були саме ООП.

Не завжди такі метріки існують

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

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

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

Я таке багато чув про ООП, та самі жахливі архутектури, які я бачив, були саме ООП.

Ну бо в більшості складні проблеми вирішують методами де є ООП. Поки хаскелі пишуть 48 імплементацій фібоначі, до яких дійсно не приколупаєшся :)

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

Ну... 30k пакетів це не 100 рядків коду. Якщо джун написав 1 000 000 рядків коду за рік, то тут також є велика ймовірність, що щось не так.

Ну бо в більшості складні проблеми вирішують методами де є ООП.

Ну... тут я не погоджуюся, якщо брати бізнес-логіку, то вона зазвичай досить проста. Знову ж таки, на Haskell багато проектів різної складності, наприклад мова програмування Agda, як варіант. Згадувався Cardano. Тобто є і приклади досить великих складних робочих проектів.

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

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

Ок, візьмемо наприклад Python. Самий простий приклад side-effect-у це sys.path.insert. Тому що, нам треба повністю заборонити імпорт sys, та написати повноцінний власний враппер? А скільки таких модулів та пакетів, де простий виклик робить side-effect? Це не кажучи про те, що в будь якому параметрі типа dict можна передати sys.path.

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

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

30k пакетів

20k. + Мабуть половина то різні фібоначі імплементаціі :)

Якщо джун написав 1 000 000 рядків коду за рік, то тут також є велика ймовірність, що щось не так.

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

Agda, як варіант. Згадувався Cardano

Зефейлена мова та зафейлений крипто проект. Цікавим проект стає коли він стає успішним і вспливають багато бізнес проблем.

Самий простий приклад side-effect-у це sys.path.insert

Працював на 2х різних проектах де сервався 3rd party code. На різних мовах. Все це відносно просто робиться без монад. Якщо це про заборону в 1st party коді інкремент робити, то звучить як значно більша організаційна проблема яку мовою не вирішиш. Не бачу нічого поганого в тому щоб вводити великі обмеження на sys, велика вірогідність що на чомусь більш менш серйозному sys.path.insert і не зможеш використати.

Цікавим проект стає коли він стає успішним і вспливають багато бізнес проблем.

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

Все це відносно просто робиться без монад.

Ще раз, приклад в студію, як контролювати side effect-и. Мій приклад це 20 рядків коду. Можна псевдокод.

Ще раз, функція це аналог contsexpr в С++, має завжди для одних й тих самих аргументів повертати однакове значення. Якщо ні, то ми маємо явно вказати, який эфект використовується (читання з файлу, запит до БД, ...)

Ще раз, приклад в студію, як контролювати side effect-и.

вся індустрія розробки про це — про роботу з side effectами.
то функціональщики граються в чисту математику.

Якщо мова дозволяє змінні, то будуть й сайд-ефекти.

Тому вони і є, у мейнстрім мовах — змінні.
Бо це самий простий і очевидний спосіб роботи з сайд-ефектами.

І тому на мовах де немає змінних — маргінально мало написано, і пише хоть щось на таких мовах мізерний відсоток програмістів.
Бо задовбатись можна, писати «монадами».

вся індустрія розробки про це — про роботу з side effectами.

Ну... не погоджуюсь. Більшість кода детерміністична, в якому намішані side effects. Як це зазвичай виглядає? Отримуємо запит, отримуємо все необхідне для його обробки. Потім йде детерміністичний код обробки, який зазвичай доволі чистий. А потім знову розсилаємо результати.

Навіть імперативний код, де дані та логіка розділені, виглядає краще.

І тому на мовах де немає змінних — маргінально мало написано, і пише хоть щось на таких мовах мізерний відсоток програмістів.
Бо задовбатись можна, писати «монадами».

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

Бо задовбатись можна, писати «монадами».

Або більшість розробників просто занадто тупі для того, щоб оволодіти дійсно ефективними засобами розробки. Як більшість людей занадто тупі, щоб стати розробниками.

Або більшість розробників просто занадто тупі для того

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

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

У кого такє враження :)
Якби було б такє враження — індустрія би перейшла на ці методи розробки.
Результат — от що важливо.
Сноби функціональщики не демонструють і особистого результату.

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

Хочеться бути не з бидлом, а вважати себе богемою :)

Ну... я себе скоріше до бидла відношу в такому разі. Але це не образа, це припущення. Я так і написав «або» щоб показати, що це можливість. Ну ок, можна перефразувати якось на кшталт: "припустимо, що якщо випадкового джуна посадити за сучасний промисловий Haskell код, то за рік він не зможе принести велью. Тоді це відповідь на питання:

Якби було б такє враження — індустрія би перейшла на ці методи розробки.

Немає спеціалістів, щоб переходити.

Знову ж таки, індустрії більше, як на мене, більше притаманні особливості еволюції: виграє перший, а не кращий. Бо так можна вважати JavaScript найкращою мовою програмування у світі, до сама популярна.

Немає спеціалістів, щоб переходити.

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

виграє перший, а не кращий

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

Бо так можна вважати JavaScript найкращою мовою програмування у світі

І що такє «кращий» тому — невідомо.
На ньому пишуть купу корисних аплікацій.
На Haskell — не пишуть практично нічого.

Тому що він кращий, чи тому що він фактично мертвий?

Якщо складний проект працює відповідно до ТЗ, це вже успіх з точки зору розробників.

Щось по аутсорсовські написано. Одне діло написати інстаграм на 3 користувача, інше на мільярди. Кардано — це віза на три користувача.

Ще раз, приклад в студію, як контролювати side effect-и. Мій приклад це 20 рядків коду. Можна псевдокод.

Я ж написав, що залежно від яка проблема вирішується. Bazel/buck правило на імпорти наприклад. Або токени якщо контроль доступа. Більш того вирішувалось це на скейлі сотні-тисячі девелоперів які шарять один кодбейз.

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

На будь якій мові яка підтримує duck typing interfaces. Бо монади це таких хітрий спосіб казати «мої типи підтримують Х (наприклад, concatenate та flatten якщо це list монада), і мені пофігу як саме». Якщо язик функціональний, буде більш сахару, але воно працюватиме і без нього.
— C# + dynamic — OK
— go + interfaces — OK
— Python + protocols — OK

Без фп буде виглядати якось так:

type = list[int]
monad = to_list_monad(type)
data = [[1, 2], [3]]
monad.flatten(data) => [1, 2, 3]

А нащо? Бо замість list[int] може бути якась екзотична приблуда, накшталт columnar_model, np.ndarray, torch.Tensor чи хто зна що ще, а ви хочете щоб оце усе можна було пхати у логіку яка скажемо розібʼє дані на чанки, трансформує кожен чанк із допомогою якогось АПІ, а потім збере усе до кучі і поверне назад дані у їх оригінальному вигляді будь то тензор чи ще щось. І ви дуже не хочете писати логіку під кожен тип даних окремо.

На будь якій мові яка підтримує duck typing interfaces. Бо монади це таких хітрий спосіб казати "мої типи підтримують Х (наприклад, concatenate та flatten якщо це list монада)

А ти точно розумієш, що таке монади?

type = list[int]
monad = to_list_monad(type)
data = [[1, 2], [3]]
monad.flatten(data) => [1, 2, 3]

У термінології Haskell це класи, наприклад,

class  Eq a  where
  (==), (/=)            :: a -> a -> Bool
  x == y                =  not (x /= y)
x /= y = not (x == y)

Але монади це не інтерфейси, ніякого flatten в монадах немає. Ну я розумію, що монаду List можна вважати бектрекінгом (backtracking), але до чого там flatten?

А ти точно розумієш, що таке монади?

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

От як Чатгпт пояснив, на моє прохання простою мовою:
Монада — це спосіб упакувати дані та послідовність операцій над ними у функціональному програмуванні, де немає звичних циклів, змін стану та інших елементів імперативного стилю.
(кінець цитати)
Функціональщики пробують пояснювати ескімосам, яка корисна штука кондиционер, а ескімоси ніфіга не розуміють що корисного, дивного у холоді.
І функціональщики такі — ну, йолопи! ми їм про цінну штуку яка генерує холод, а вони ніфіга і про холод не розуміють!

Ну отож, у імперативщені монади корисні лише якщо ви хочете щось розраховувати «дуже специфічним способом», наприклад гарантуючи використання векторних інструкцій коли на вході матриці, звичайних list операцій коли на вході rowar моделі, а також комбінувати усе це щастя коли на вході columnar моделі (ака model(ids=["id1«, «id2», «id3»], vectors=[[1, 2, ..., 100500], [4, 5, ... 100500], [7, 8, ..., 100500]]) — трансформації vectors повинні бути hardware accelerated, трансормації ids — ну як є, нічого з ними не зробити). В таких випадках монади «знають» як виконати розрахунки «оптимально», бо їх реалізація залежить від типу даніх на вході

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

Це зовсім не має ніякого відношення до монад. Або це інший термін.

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

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

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

Монади тому й імперативщикам незрозумілі, бо в них немає потреби в імперативних мовах.

Особисто у мене було 30 років досвіду імперативного програмування, лише потім я почав дивитися на Haskell. Навіть попри те, що мій рівень його розуміння слабкий, дуже шкодую, що 15 років тому замість Haskell я розбирав .NET, але монад мені не вистачає.

От як Чатгпт пояснив, на моє прохання простою мовою

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

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

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

data = [1, 8, 3, 9, 2, 4]
sum = 0
for x in data:
    if x % 2 == 0:
        sum += x

то щоб це обчислити, там монади не потрібни, результат буде прямим аналогом

from functools import reduce
from operator import add
reduce(add, map(lambda x: 2 * x, filter(lambda x: x % 2 != 0, data)))

Змін стану так, має певне відношення до монад. Але це не означає, що змінювати стан у Haskell не можна. Можна, але кожна функція, яка змінює стан, буде повертати відповідний тип. Я наводив приклад

doubleWithStat :: Int -> StatMonad Int
doubleWithStat x = do
  incrementCounter
  return (x * 2)

Його можна прочитати так: функція doubleWithStat приймає аргумент типу Int, повертає аргумент типу Int, при цьому змінює стан статистики (StatMonad).

Особисто у мене було 30 років досвіду імперативного програмування, лише потім я почав дивитися на Haskell.

Дивакі завжди існували. у всіх сферах.

Більшості програмістів не зрозуміли ваши уподобання.
У пік ґайпу я теж поліз дивитися.
І на Haskell і на Scala.
Виявилось непотрібом для практичної розробки.
Щось на зразок R&D у computer science, лабораторій.
Цінне і корисне з яких з’являється потім і у мейнстрім мовах.

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

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

А досвід індустрії розробки ПЗ вказує на зворотнє.
Що виділення чистих функцій навіть для функціональщиків важка робота.
Важка — бо робиться повільно.

«мідл на пхп» наляпає CRM з адмінкою за X годин
а «експерт на Haskell» і третини роботи не зробить за цей час.

Тому й на пхп повно працюючих програм, а на Haskell — треба вишукувати.

Та й с непростим і небезпечним С++ така ж історія.
На ньому простіше софт писати, з пристойною надійністю ніж на Haskell.

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

У пік ґайпу я теж поліз дивитися.
І на Haskell і на Scala.
Виявилось непотрібом для практичної розробки.

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

Звісно, якщо емулювати імперативні конструкції, наприклад, робити параметр та рекурсивний виклик там, де потрібна змінна, то... це дійсно буде пеклом.

а вони ніфіга і про холод не розуміють!

Дик холоду не існує, тільки тепло ;)

Ну так,

# тут генерується новий тип який відповідає (концептуально) монаді list
monad: type = to_list_monad(list[int])

# тут використовується аналог bind
monad.flatten([[1, 2], [3]]) # [1, 2, 3]

Насправді там набагато більш методів, як от monad.empty(), monad.from_iterable(), protocol.as_sequence(self), protocol.__get_item__(self, slice), protocol.scatter(self, indexes) і таке інше (protocol це instance of monad)

На хаскелі воно більш лаконічне (лише bind та return) але й повільніше

# ось flatten на хаскел
[[1, 2], [3]] >>= id

Чи можна замість monad.flatten зробити monad.bind(func) на python? Ну можна авжеж, але навіщо? Воно й так концептуально те саме :) Конкретне апі не так важливе, важливо щоб 1) воно було математично ідентичне монадам (мало таку ж саму потужність) 2) щоб коли ви робите умовний monad.flatten(...) воно працювало оптимально (тобто не робило скажимо tensor.tolist() якщо монада репрезентує torch.Tensor, а використовувало натівне c-backed torch api яке вміє у flatten без допоміжних конверсій у повільні пітоновські типи; тому api таке як є — щоб для усіх типів його можно було реалізувати strait away)

Біль детально © ЖПТ (вибачайте якщо доу партер не вміє в латех)

Монада списків через flatten і from_iterable
1. Функтор T:
Функтор для монади списків відображає тип A у список T(A) = [A], тобто кожен елемент A перетворюється в список з цим елементом.
2. Операція введення через from_iterable:
Функція from_iterable замінює стандартну операцію введення (\eta) і дозволяє створити список із будь-якого ітерабельного об’єкта, зокрема, одноелементного списку. Для елемента a вона дає:

\text{from\_iterable}(a) = [a].

Вона також підтримує складніші ітерабельні структури, наприклад:

\text{from\_iterable}([a_1, a_2]) = [a_1, a_2].

3. Операція розгортання через flatten:
Операція flatten (або \mu) розгортає вкладені списки в один плоский список:

\mu([[a_1, a_2], [a_3], [a_4, a_5]]) = [a_1, a_2, a_3, a_4, a_5].

4. Закони монади:
• Ідентичність: Перетворення елемента в список і негайне його «розгортання» не змінює результат:

\mu(\text{from\_iterable}(a)) = [a].

• Асоціативність: Порядок застосування розгортання до вкладених структур не має значення:

\mu \circ T(\mu) = \mu \circ \mu_T.

Висновок

Монаду списків можна визначити через flatten (для розгортання вкладених списків) та from_iterable (для введення елементів у список). Ці функції забезпечують необхідні операції для монади: введення та розгортання, при цьому дотримуються всіх необхідних законів монади.

***

Давайте доведемо, що використання flatten для розгортання списків та from_iterable для введення елементів утворює монаду списків, що задовольняє всі закони монади.

Монада: визначення та необхідні операції

Монада визначається трійкою:
1. Функтор T:
Функтор, який відображає об’єкти з категорії в монадичний контекст.
• Для списків T(A) = [A], тобто A відображається в список із елементів A.
2. Операція введення \eta:
Операція, яка «вводить» елемент у монадичний контекст (у випадку списків — створює одноелементний список):

\eta(a) = [a].

Ми використовуємо from_iterable, яка надає нам те саме — перетворює елемент у список:

\text{from\_iterable}(a) = [a].

3. Операція розгортання \mu:
Операція, що згортає вкладені структури (списки списків) у простіший список (з’єднує всі елементи всіх вкладених списків). Для списків це flatten:

\mu(\text{flatten})([[a_1, a_2], [a_3], [a_4, a_5]]) = [a_1, a_2, a_3, a_4, a_5].

Закони монади

Монада повинна задовольняти два основні закони:

1. Закон ідентичності (унітарність)

Цей закон стверджує, що введення елемента в монаду, а потім розгортання не повинно змінювати елемент:

\mu \circ T(\eta) = \mu \circ \eta_T = \text{id}_T

Доказ:
• T(\eta) перетворює елемент a на список \eta(a) = [a].
• Тепер застосуємо \mu до цього списку:

\mu(\eta(a)) = \mu([a]) = [a].

• Таким чином, застосування \mu після введення елемента в список через \eta дає нам саме той самий елемент, що й перед введенням. Тобто, закон ідентичності виконується.

2. Закон асоціативності

Закон асоціативності стверджує, що порядок застосування розгортання до вкладених списків не має значення:

\mu \circ T(\mu) = \mu \circ \mu_T

Доказ:

Нехай у нас є вкладені списки списків. Розглянемо застосування \mu до цього списку двома способами.
• Ліва сторона:
Спочатку застосуємо \mu до списку списків:

\mu(\mu([[a_1, a_2], [a_3], [a_4, a_5]])) = \mu([a_1, a_2, a_3, a_4, a_5]) = [a_1, a_2, a_3, a_4, a_5].

• Права сторона:
Спочатку застосуємо \mu до кожного внутрішнього списку:

\mu([[a_1, a_2], [a_3], [a_4, a_5]]) = [a_1, a_2, a_3, a_4, a_5].

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

Висновок

Ми показали, що використання flatten для розгортання списків і from_iterable для введення елементів задовольняє всі закони монади:
1. Закон ідентичності: застосування введення та розгортання не змінює значення.
2. Закон асоціативності: порядок застосування розгортання не впливає на результат.

Таким чином, це монада списків.

То можна просто дати посилання, але flatten це лише одна функція, яку можна написати з використанням монади. Але монади це обмежуються flatten. Як в анекдоті:
— Для чого потрібен Access?
— Для того, щоб працювати з базою даних Борей.

# ось flatten на хаскел
[[1, 2], [3]] >>= id

Чи можна замість monad.flatten зробити monad.bind(func) на python?

Ну... простіше використати concat [[1, 2], [3]]
Використання монади тут явний overkill.

Ну можна авжеж, але навіщо? Воно й так концептуально те саме

Для того, щоб менше писати, та повторно використовувати код. Я вже наводив приклад

import Control.Monad

queens n = foldM (\y _ -> [ x : y | x <- [1..n], safe x y 1]) [] [1..n] where
    safe x [] _ = True
    safe x (c:y) n = and [ x /= c , x /= c + n , x /= c - n , safe x y (n+1)]
    
main = do
  print $ length $ queens 13

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

А який буде аналог на Python?

Ну так і буде:

def safe(x, queens, n):
«„„Перевіряє, чи безпечно поставити ферзя на позицію x.““»
for i, q in enumerate(queens):
if x == q or x == q + n + i + 1 or x == q — n — i — 1:
return False
return True

def queens(n):
«„„Генерує всі можливі розташування n ферзів на дошці.““»
def solve(queens, depth):
if depth == n:
yield queens
else:
for x in range(1, n + 1):
if safe(x, queens, 0):
yield from solve([x] + queens, depth + 1)

yield from solve([], 0)

if __name__ == «__main__»:
print(sum(1 for _ in queens(13)))

Але монади тут зайві (ну добре, ‘yield from’ це насправді монада, але ж вбудована а мову).

Кастомні монади потрібні коли ви хочете розвʼязувати ту саму задачу із будь якими типами. Не ферзі, а коні. Не шахи, а нарди. Не ходять, а квакають. Якщо воно монадично зроблене, то монади 1) перш за все скажуть вам чи можна рахувати комбінації у квакаюяих конях при грі в нарди взагалі (якщо ні то тайп еррор) 2) якщо можна — ваш алгоритм буде працювати без змін коду.

О! Я знайшов ще один застосунок для монад: пишеш три рядки коду, кидаєш ChatGPT та отримуєш простирадло :-)

Але знову порівняємо:

Haskell:

import Control.Monad

queens n = foldM (\y _ -> [ x : y | x <- [1..n], safe x y 1]) [] [1..n] where
    safe x [] _ = True
    safe x (c:y) n = and [ x /= c , x /= c + n , x /= c - n , safe x y (n+1)]
    
main = do
  print $ length $ queens 13

Python:

def safe(x, queens, n):
    print(queens)
    for i, q in enumerate(queens):
        if x == q or x == q + n + i or x == q - n - i:
            return False
    return True

def queens(n):
    def solve(queens, depth):
        if depth == n:
            yield queens
        else:
            for x in range(n):
                if safe(x, queens, 0):
                    yield from solve([x] + queens, depth + 1)

    yield from solve([], 0)

if __name__ == '__main__':
    print(sum(1 for _ in queens(4)))

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

Монади так, це реюзабельність коду. Наприклад, ми хочемо генерувати пари Лендхофа. І на Haskell знову ж таки нам треба лише написати перевірку, чи можна вставити в даному розташуванні наступний номер в задану позицію:

import Control.Monad

langford n = foldM (\y _ -> [x : y | x <- [1..maxx], safe x y]) [] [1..n] 
    where maxx = 2 * n - 2
          safe x y = let lenY = length y in and [ x <= maxx - lenY, safe' x y, safe' (x+2+lenY) y ]
          safe' x [] = True
          safe' x (c:y) = and [ x /= c, x /= c + 2 + length y, safe' x y]

main = do
  let results = langford 11
  print $ length results 

Підсумок: в даному випадку монада (1) покращую читабельність коду; (2) надає можливіть його повторного використання.

queens n = foldM (\y _ -> [ x : y | x <- [1..n], safe x y 1]) [] [1..n] where
safe x [] _ = True
safe x (c:y) n = and [ x /= c , x /= c + n , x /= c — n , safe x y (n+1)]

схоже на brainfuck )

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

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

if err != nil {
    return err
}

:)

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

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

Ну... в такому разі можна відмінити defer, бо ми будемо бачити конкретно, це метод викликається.

Тому це питання балансу та культури (а плані інформації, отриманої від інших) . Якщо ти знаєш сішну метафору

uinsigned int n = 8;
while (n --> 0) {
    printf("%d", n);
}

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

uinsigned int n = 8;
while (n > 0) {
    --n;
    printf("%d", n);
}

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

Добре, спробуємо порівняти варіанти на Haskell та Go.

Haskell:

{-# LANGUAGE UnicodeSyntax #-}

import Control.Monad

queens n = foldM (\y _ → [ x : y | x ← [1..n], safe x y 1]) [] [1..n] where
    safe x [] _ = True
    safe x (c:y) n = and [ x /= c , x /= c + n , x /= c - n , safe x y (n+1)]
    
main = do
  print $ length $ queens 13

Go:

package main

import (
  "fmt"
)

func queens(n int) [][]int {
  var solve func(y []int, level int) [][]int
  solve = func(y []int, level int) [][]int {
    if level == n {
      return [][]int{y}
    }
    var solutions [][]int
    for x := 1; x <= n; x++ {
      if safe(x, y) {
        newY := append([]int{x}, y...)
	solutions = append(solutions, solve(newY, level+1)...)
      }
    }
    return solutions
  }
  return solve([]int{}, 0)
}

func safe(x int, y []int) bool {
  for i, c := range y {
    if x == c || x == c+(i+1) || x == c-(i+1) {
      return false
    }
  }
  return true
}

func main() {
  n := 13
  fmt.Println(len(queens(n)))
}

Ну... тут функція safe, тільки у разі Haskell це рекурсія з додатковим параметром — індексом першого стовбчика. Варіант Golang більше зрозумілий, за виятком того, що додаткова машинерія додаванням одиничкі в індексом, бо нумерація починається з нуля.

Інше це деталі, /= це аналог !=, рекурсія по списку, c:y означає що c це перший елемент списку, y це решта. Плюс додатковий параметр — зміщення хвоста відповідно базового списка.

Haskell

safe x [] _ = True
 safe x (c:y) n = and [ x /= c , x /= c + n , x /= c - n , safe x y (n+1)]

Go

func safe(x int, y []int) bool {
  for i, c := range y {
    if x == c || x == c+(i+1) || x == c-(i+1) {
      return false
    }
  }
  return true
}

Фукнція safe це те, що треба реалізовати Haskell розробнику. Після цього... треба знати, що монада «список» це є перебір з відкатами backtrace, та просто налаштувати параметри цього перебору.

queens n = foldM (\y _ → [ x : y | x ← [1..n], safe x y 1]) [] [1..n] 

fold це аналог reduce у світі Haskell. Взагалі, термінологія Haskell трохи посунута, що цього треба звикати. Функція foldM приймає три аргумента: функція, яка акамулує значення, вона приймає те, що вже акумульомано, та новий елемент. Далі, початкове значення акумелятора, та список, на яким нам треба виконати дії.

Грубо кажучи, це

result = start
for x in iterable:
    result = function(result, x)

для Haskell розробників це один з базових паттернів. Добре, у нашому ми акумулюємо список, start це порожній список, iterable це список з n елементів. function це в нас лямбда, яку на Python можна переписати як

lambda acc, elem: [ [new_y] + acc for new_y in range(1, n+1) if safe(elem, acc) ]

Так, трохи магії Haskell, але... ми приймає список вже розташованих ферзів, та повертаємо усі варіанти продовження перебору. Тобто, якщо на шахівниці ферзі розшташовані по полях a2 та b7 (тобто acc = [2, 7]), то ми маємо повернути поля c3 та c5 для продовження перебору, це відповідає спискам [2, 7, 3] та [2, 7, 5].

Ми бачимо, що ми не використовуємо весь потеціал, у нас параметр elem буде послідовно від 1 до n, номер ітерації, але він нам не потрібен.

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

Ну а в Golang...

func queens(n int) [][]int {
  var solve func(y []int, level int) [][]int
  solve = func(y []int, level int) [][]int {
    if level == n {
      return [][]int{y}
    }
    var solutions [][]int
    for x := 1; x <= n; x++ {
      if safe(x, y) {
        newY := append([]int{x}, y...)
	solutions = append(solutions, solve(newY, level+1)...)
      }
    }
    return solutions
  }
  return solve([]int{}, 0)
}

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

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

int tsum_acc(int a, std::vector<tree *> xs, tree *x) {
  if (x->is_leaf) {
    if (xs.empty()) return a + x->leaf_value;             // (випадок 1)
    a += x->leaf_value;                                   // (випадок 2)
    x = xs.back(); xs.pop_back();
    return tsum_acc(a, xs, x);
  }
  xs.push_back(x->node_left);                             // (випадок 3)
  x = x->node_right;
  return tsum_acc(a, xs, x);
}
це просто жах як з точки зору сигнатури функції так і з точки зору реалізації і маніпуляції вектором.
Використовував на практиці С++ і Haskell можу сказати що для мене Haskell це brainfuck. В реальності на С++ можна писати кращий код, більш зрозумілий, швидший і який взагалі використовує менше ресурсів.
Нажаль тут немає порівняння Хаскеля з С++ а тільки з Сі алє слабкі места Хаскеля демонструються benchmarksgame-team.pages.debian.net/...​me/fastest/clang-ghc.html

ще рекомендую автору замість штучних прикладів спробувати реалізувати одній і ті задачі з Leetcode на Haskell і на С++ і порівняти швидкодію і час написання

Цікаві експерименти, тільки чомусь приклади на базі ігнорування алгоритмів, тобто усі приклади із наївними алгоритмами.
Задача перша це прохід графа в глиб та в ширь, і як відомо є два методи позбавлення рекурсії — перший це структура данних — стек це зберігає пам’ять але повільніше відповідного до золотого правила (виграш в пам’яті дорівнює програшу в швидкості і навпаки), другий ви побачите в більшості реалізацій дерев і ітераторів в STL — третій вказівник в структурі який вказує на root вузол (відповідно після позбавлення від bool і вирівнюванням структур може вийде і те на та насправді) — тут використовується більше пам’яті, але це працює швидше.
Факторіал — один із найвідоміших прикладів для демонстрації рекурсії, і так само відомо — що це дуже не оптимальний алгоритм, найвідоміші методи — Решето Ератосфена та задіяння SIMD і дерев з паралелізацією обчислень, зокрема і на відеокартах.
Числа фібоначі — так само алгоритми на швидких перемноженнях матриць та динамічне програмування.

Я не справжній зварювальник і комп’ютер сайнсів не кінчав, але мені здається, для малих аргументів факторіалу вистачить і наївної рекурсивної реалізації в С++, рахувати наближено можна і формулою Стірлінга, а для цілочисельного факторіалу від мільйонів чи мільярдів буде якась складна оптимізована хрінь, де буде якась divide-and-conquer розбивка і/або заміна множення багаторозрядного числа згорткою зі скалярним добутком окремих розрядів, і це знову ж таки буде на плюсах.

А ще, не знаючи Haskell, але немало писавши на Wolfram Mathematica, функціональне програмування запам’яталось дуже складним компромісом між читабельністю коду і оптимальністю його виконання. Підходячи чисто функціонально, легко і зручно наприклад шукати максимальный елемент списку шляхом взяття першого елементу від сортованого списку, або для двовимірного набору даних в .csv для задання початку координат графіку зробити транспонування, відняти зміщення від першого і другого списку, і транспонувати назад в пари координат. Ось тільки для оптимізацій компілятору бажано передавати всю композицію з функцій відразу (або юзати ліниві обчислення) і відступати від наглядності типів даних. Може з правильною CS базою можна писати читабельно і продуктивно, але мені це підозріло нагадує вайлайнери на баші з численними пайпами — навіть якщо воно ефективно, якщо щось зламається, полагодити навіть автор колу не факт що зможе.

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

Таблиці, або навіть тупого масиву 😉
Гугл каже, що 20! макс що влізає у 64 біт

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

Якщо дерево незбалансоване, то рішення із std::vector буде використовувати у 2-3 рази (залежить від stdlib) більше пам’яті ніж рекурсія, бо вектора виділяють більше памʼяті ніж потрібно, щоб не робити realloc кожен раз коли додають новий елемент. Тому переписувати рекурсію теж не варто. Замість цього балансуйте дерево.

Ну а якщо «дуже треба» і балансувати дерево чомусь не варіант, краще використовувати Morris Tree Traversal — зовсім інший алгоритм — medium.com/...​ce-algorithm-5d2d2d47814a

Дякую, але 1. мені здається балансування дерева теж рекурсивне; 2. не обов’язково брати саме std::vector, та і взагалі тут задача про використати heap замість стеку, а як той heap використаний то вже інша справа.
Нагадую, що це задача демонстративна. Є інші подібні задачі не обов’язково з деревами

Єєєє, приклад «оптимізації» із неочевидною математикою де «оптимізація» робе код повільнішим, та ще й жере памʼять, це ж найс!!!

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

Та ні.

— Збалансоване бінарне дерево має 2^N листів, де N — висота дерева. Це чотири мільярди елементів при глині рекурсії у 32 фрейма. Це багато. Це дуже багато.
— Як часто у ядрі лінукс треба обходити дерева? А як часто їх можна обходити дозволивши собі алокацію памʼяті на кожен обхід? Нагадую: алокація памʼяті це дуже повільно.
— Ще раз: така задача вирішується іншим чином: або балансування дерева рекурсія, або додаткові дані у дереві щоб його можна було обходити без використання додаткової памʼяті.

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

Якщо дерево незбалансоване, то рішення із std::vector буде використовувати у 2-3 рази (залежить від stdlib) більше пам’яті ніж рекурсія, бо вектора виділяють більше памʼяті ніж потрібно, щоб не робити realloc кожен раз коли додають новий елемент. Тому переписувати рекурсію теж не варто. Замість цього балансуйте дерево.

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

Цей код дуже простий, але використовує рекурсію, що не дуже добре, бо стек має межі.

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

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

Якось складнувато, як на мене. Я би написав щось на кшталт

data Tree =
    Leaf Int
  | Node Tree Tree

foldTree :: (Int -> b) -> (b -> b -> b) -> Tree -> b
foldTree fLeaf fNode (Leaf n) = fLeaf n
foldTree fLeaf fNode (Node l r) = fNode (foldTree fLeaf fNode l) (foldTree fLeaf fNode r)

sumTree :: Tree -> Int
sumTree = foldTree id (+)

Або з використанням стандартної ліби:

import Data.Foldable (Foldable, foldMap)

data Tree a =
    Leaf a
  | Node (Tree a) (Tree a)

instance Foldable Tree where
    foldMap f (Leaf x) = f x
    foldMap f (Node left right) = foldMap f left `mappend` foldMap f right

sumTree :: (Num a) => Tree a -> a
sumTree = sum

>instance Foldable
хід конем звичайно, але тоді б і статі б не було. схоже ви не зрозуміли головну ідею статі

Щоб вивчити C++, нам не треба для тренування писати власну STL. Аналогічно, можна вивчити Haskell шляхом вивчання, при цьому використовуючи стандартні засоби для таких задач. Ідея зрозуміла, але трохи під питанням, як на мене.

Щось не побачив саме аргументів до чого тут Хаскел. Я так підозрюю, вам треба було розгорнути рекурсію в ітеративний підхід, це вивчають в універі (власне на лекції про рекурсію). Але я занадто старий та ледачий, щоб це робити. Тому:
1) Act as a C++ developer
2) Lets say you have the following structure:
3) Refactor the following code to replace recursion with iterative approach:
Результат

#include <stack>

int tsum(tree *root) {
    if (!root) return 0; // Handle the case where the tree is empty

    int sum = 0;
    std::stack<tree*> stack;
    stack.push(root);

    while (!stack.empty()) {
        tree *current = stack.top();
        stack.pop();

        if (current->is_leaf) {
            // If it's a leaf, add its value to the sum
            sum += current->leaf_value;
        } else {
            // If it's an internal node, push its children onto the stack
            if (current->node_right) stack.push(current->node_right);
            if (current->node_left) stack.push(current->node_left);
        }
    }

    return sum;
}

Це схоже на те що ми робили в універі.
4) Use vector instead of stack — щось схоже на ваш код

В чому була ідея статті?

UPD.
Якщо вже адміни не здатні зробити візівіг чи маркдаун, то хоч додайте доку по тегам.
Як працює тег code?

якщо вважати, що node_left або node_right ніколи не буде рівними нулю, і root кладеться в стек перед викликом функції, то

...
stack.push(root);
stum_iter2(0,stack);
...

int tsum_iter2(int sum, std::stack<tree *> stack) {
  while (!stack.empty()) {
    tree *current = stack.top();
    stack.pop();

    if (current->is_leaf) {
      sum += current->leaf_value;
      continue;
    }

    stack.push(current->node_right);
    stack.push(current->node_left);
  }
  return sum;
}
далі переходимо tail call optimized версію.
int tsum_acc2(int sum, std::stack<tree *> stack) {
  if (stack.empty()) return sum;

  tree *current = stack.top(); stack.pop();
  if (current->is_leaf) {
    sum += current->leaf_value;
    return tsum_acc2(sum, stack);
  }
  stack.push(current->node_right);
  stack.push(current->node_left);
  return tsum_acc2(sum, stack);
}
що на хаскелі очевидно виглядає якось так
tsum_acc2 acc [] = acc
tsum_acc2 acc ((Leaf x):xs) = tsum_acc2 (acc+x) xs
tsum_acc2 acc ((Node a b):xs) = tsum_acc2 acc (a:b:xs)
схоже, що ваша tsum_acc2 визначається так
tsum_acc2 a xs = a + reduce xs
перевіримо
tsum_acc2 a [] = a + reduce [] = a

tsum_acc2 a ((Leaf x):xs) =
  a + tsum (Leaf x) + reduce xs =
  a + x + reduce xs =
  tsum_acc2 (a+x) xs

tsum_acc2 a ((Node x y):xs) =
  a + tsum (Node x y) + reduce xs =
  a + tsum x + tsum y + redure xs =
  a + tsum x + reduce (y:xs) =
  a + reduce (x:y:xs) =
  tsum_acc2 acc (x:y:xs)
вірно.
далі визначемо її через tsum
tsum_acc2 a [] = a + reduce []
tsum_acc2 a [x] = a + tsum x
tsum_acc2 a (x:xs) = a + tsum x + reduce xs
з чого слідує
tsum_acc2 0 [x] = tsum x
tsum x = tsum_acc2 0 [x]
або
tsum_acc2 0 [x] = tsum_acc 0 [] x
що є _математичним_ доказом, що ви написали те саме, що і я. але маю зазначити, що ваш код виконує більше stack.push(). можете це перевірити.
короче ідея статі саме в тому, що код можна розглядати як математику. нажаль більшість читачів схоже цього не вловила. фактично, частина запунилася на факторіалах, частина на нормалізації дерев, але ідеї на жаль мало хто побачив.
короче ідея статі саме в тому, що код можна розглядати як математику. нажаль більшість читачів схоже цього не вловила

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

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

Наразі не маю доступу до вольфрамівської математики, а веб-версія досить урізана.
Мова іде про ось цей алгоритм en.wikipedia.org/...​ion#Computational_expense і оцю формулу en.wikipedia.org/...​ramer’s_rule#General_case

Склалося враження, що писати на ній виходить або дуже нечитабельно, або доводиться покладатися на магію компілятора, яка як завжди, гарантій не дає:
mathematica.stackexchange.com/...​terminants-of-polynomials

ніколи не працював з wolfram matemathica.
свого часу, коли я займався чисельними розрахунками, це була теж лінійна алгебра, множення матриць, але це все було на с++. Єдина проблема яку я мав: це було ще під дос і там RAND_MAX був 65536, і отже випадкові числа 1.0*rand()/RAND_MAX були гранульовані по 1/65536. це породжувало цікаві ефекти, які зникли коли код скомпілювали під лінукс в якому RAND_MAX 32 бітне число. більше нічого про лінійну алгебру я вам сказати не можу:)

що є _математичним_ доказом, що ви написали те саме, що і я. але маю зазначити, що ваш код виконує більше stack.push(). можете це перевірити.

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

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

Тупі людишки! Ви ж пояснили все чітко і виділили ключові моменти (наприклад, що математична доказовість важлива і як її треба досягати в загальному випадку), якісно структурували статтю, пройшли кілька рев’ю у своїх колег (з різним досвідом), а не просто розкрутили один приклад рівня 1-2 курсу в кілька різних шматків коду.

>Тупі людишки!
це написали ви, а не я. не бачу конструктивної критики.

не бачу конструктивної критики.
нажаль більшість читачів схоже цього не вловила.

Якщо серйозно, то дочитайте абзац до кінця, там не критика, а конкретні поради

І як завжди всі приклади функціональщини базуються на титанах факторіал, фібоначчі і так далі.
Реальних прикладів з реальних задач не завезли?)

Реальних прикладів з реальних задач не завезли?)

fp кейсів використання closure, scala, haskel, ocaml/f# можна накопати серед продакшину у різних apple/facebook/netflix/github/whatsup також цього добра з легкістью вистачить і в консалтингу(особисто стикався на проді з успішним досвідом використання fp)/телекомі(привіт erlang)/аудиті/великих банках і т.д. Одразу швидке утонення чи вас не забанили в гуглі/chatgpt/wkipedia випадково щоб зняти очевидні проблеми когнітивних здібностей персоналей?

В гуглі сорс код є реальних проектів з проду?

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

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

По вашому статтю писати також не потрібно було, так як в гуглі можна знайти подібне?)

В гуглі сорс код є реальних проектів з проду?

Ну, наприклад, cardano-wallet (HTTP-сервер і командний рядок для керування UTxO та HD-гаманцями в Cardano.) та IHaskell (Ядро Haskell для Jupyter).

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

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

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

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

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

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

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

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

всякі різні процеси (і бізнес логіку також) часто малюють квадратиками і стрілочками. їх дуже зручно моделювати стрілками (ті що Arrows у хаскелі). хоча код Arrows дійсно ніхто не любить.

Але наскількі це потрібно в розробці яка в більшості своїй складається з бізнес логіки?

Ок, я бачу два пункта. По-перше, ADT (Algebraic Data Types). В принципі, можна перевірити, що усі можливі варіанти обробляються, тобто функція поверне якийсь результат без різних виключень, падінь, і т. п., за виключенням вичерпаності пам’яті. Також якщо функція обмежена лише структурною рекурсією, то ми автоматично отримуємо доказ, що вона виконається за кінцевий час, є тотальною. Звісно, що це може бути мільярд років, але тупе зациклення ми піймаємо. В принципі розробники Rust частково цим користуються.

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

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

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

і наскільки корисний такій «доказ»? чи допоможе вам це в системах реального часу? чи інших критичних системах? особливо коли мова про «може бути мільярд років»? так чи інше детальне тестування(unit testing, system integration and etc) таких систем потрібне

і наскільки корисний такій «доказ»? чи допоможе вам це в системах реального часу? чи інших критичних системах?

Наскільки часто ти припускався до банальної помилки, яка призводила до нескінченої рекурсії або зациклювання? У мене бувало. В принципі, я колись бачив стандарти NASA, де були в С++ заборонені нескінчені цикли for(;;)

особливо коли мова про «може бути мільярд років»?

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

def solve(position):
    result = position.result
    if result is not None:
        return result

    for move in position.moves():
         position.do(move)
         new_result = solve(position)
         position.undo(move)

         if new_result.better(result):
             result = new_result

    return result

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

def solve(position, max_depth):
    if max_depth == 0:
        return remi
    max_depth -= 1

...

Тепер так, компілятор буде щасливий, бо ми обмежили максимальну глибину рекурсії. Але якщо параметр max_depth буде 1000, то це мало чим відрізниться від нескінченості. Але, як мінімум, компілятор звернув увагу на проблему, ми його надурили

так чи інше детальне тестування(unit testing, system integration and etc) таких систем потрібне

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

20 років в айті ніколи не мав проблему з нескінченним циклом, те все що ви кажете треба покривати тестами, тести може бути в рази простіше написати ніж морочитись з хаскелем, не кажучі про можливості контролю використання ресурсів які надає С++

20 років в айті ніколи не мав проблему з нескінченним циклом

Вони рідко, але бувають, наприклад, відома історія як LLVM спростував велику теорему Ферма:
blog.regehr.org/archives/140

Нескінчена рекурсія або зациключання у мене траплялося, але не можу сказати, що це дуже критично.

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

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

cardano-wallet — не такий великий проєкт. Але раджу подивитись на cardano-node — повністю на Haskell, з першого дня. Навіть смарт контракти на Haskell-like мові програмування — Plutus.

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

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

Кожен день ферзей на шахівницю виставляю, іноді навіть 8!

Мені більше подобається приклад, який розв’язую задачу розташування 8 ферзей на шахівниці, це теж не вважається реальною задачею?

Скільки разів ви її розв’язували за цей рік для комерційного проекту? Який відсоток роботи цей код складає від загального об’єму роботи?

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

Хто буде читати статті, що описують реальні прикладні проекти? Ну напевне люди, що хочуть навчитись чогось у розрізі прикладних проектів.

Хто буде читати статті, що описують реальні прикладні проекти? Ну напевне люди, що хочуть навчитись чогось у розрізі прикладних проектів.

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

Скільки разів ви її розв’язували за цей рік для комерційного проекту?

Парсери писав неодноразово, а це одна й та сама монада.

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

От чому з бігтек контор звільнення йдуть, звільняють профанаторів. Для ілюстрації проблеми на великому проекті, треба прибрати нерелевантне з її опису і лишити лише те має пряме відношення до проблеми. Наприклад, статті про ЛевелДБ чи дремель, або статті Фаулера.

Парсери писав неодноразово, а це одна й та сама монада.

Непогана спроба сховатись за словом «монада». Але
1) чому б тоді не описати досвід розробки парсерів?
2) якщо ви не займаєтесь розробкою лише парсерів, то 90% часу ви будете описувати бізнес логіку і передачу розпаршених елементів
3) поясніть, що таке монада (бо моє уявлення, що це банально заміна класу в ФП) і яким чином розв’язання задачі, що активно оперує масивами має спростити розробку парсерів (де основна модель дерева, якщо не зводити все до ліспу)

Непогана спроба сховатись за словом «монада». Але

Проблема у тому, що досить важко у двох словах пояснити, що таке монада. Але це точно не заміна класів. Монада скоріше це спосіб винести різні ефекти з послідовності дій. Самий простий ефект це обробка помилок — ми не виконуємо наступну дію, якщо попередня повернула помилку. Для монади list таким ефектом може бути перебір варіантів (backtraking), коли ми повертаємо список можливостей, а у самій монаді вже схований перебір варіантів. Таким чином це працює як для переборних задач. Наприклад, для задачі восьми ферзів нам треба лише написати функцію, яке поверне список безпечних клітинок в наступному рядку, а решта зробить монада.

import Control.Monad

queens n = foldM (\y _ -> [ x : y | x <- [1..n], safe x y 1]) [] [1..n]
  where
    safe x [] _ = True
    safe x (c:y) n = and [ x /= c , x /= c + n , x /= c - n , safe x y (n+1)]

main = do
  print $ length $ queens 13

Тут foldM просто послідовно виконує дії, а safe повертає вільні ще небиті клітинки в наступному рядку.

Аналогічна монада використовується у парсері, коли ми просто задаємо що за чим йде, а парсинг з відкатами йде під капотом. Ось класичний приклад парсеру CSV з обробкою помилок:

import Text.ParserCombinators.Parsec

csvFile = endBy line eol
line = sepBy cell (char ',')
cell = quotedCell <|> many (noneOf ",\n\r")

quotedCell = 
    do char '"'
       content <- many quotedChar
       char '"' <?> "quote at end of cell"
       return content

quotedChar =
        noneOf "\""
    <|> try (string "\"\"" >> return '"')

eol =   try (string "\n\r")
    <|> try (string "\r\n")
    <|> string "\n"
    <|> string "\r"
    <?> "end of line"

parseCSV :: String -> Either ParseError [[String]]
parseCSV input = parse csvFile "(unknown)" input

main =
    do c <- getContents
       case parse csvFile "(stdin)" c of
            Left e -> do putStrLn "Error parsing input:"
                         print e
            Right r -> mapM_ print r

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

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

func safeDiv(x, y int) (int, error) {
	if y == 0 {
		return 0, errors.New("division by zero")
	}
	return x / y, nil
}

func pipeline(x, y, z int) (int, error) {
	a, err := safeDiv(x, y)
	if err != nil {
		return 0, err
	}

	b, err := safeDiv(a, z)
	if err != nil {
		return 0, err
	}

	return b, nil
}

брудновато....

safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv x y = Just (x `div` y)

pipeline :: Int -> Int -> Int -> Maybe Int
pipeline x y z = do
  a <- safeDiv x y
  b <- safeDiv a z
  return b

це чистіше, бо код обробки помилок не видно.

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

>що таке монада
у рівнянні F = ma сила і прискорення векторні величини. можна і без векторів, але тоді буде система рівнянь. А що таке вектор? це деяка абстрактна штука, якою моделюють дещо, що має не тільки величину , але і напрямок.
так от програма на хаскелі це і не програма, а рівняння, яке моделює поведінку комп’ютера під час виконання програми. і для моделювання ефектів, які виникають під час виконання програми використовують абстрактне математичне поняття, монаду, про властивості якої написано вже де тільки можна. причому не обов’язково використовувати монади. наприклад RR парсер json може бути аплікативним, йому не обов’язково бути монадою. а може бути і не аплікативний, але буде така шляпа, що важко буде в чому розібратися.
>банально заміна класу
приходьте до нас, хаскелістів. у нас є і монади і класи:)

Гарнe пояснeння. Трохи додало розумiння, чому коли бачу код на функцiональних мовах, мозок завжди кажe: «воно тобi нe потрiбно, закрий».

Бо нeзважаючи на 30 рiчний (включно i як хобi) досвiд програмування на дeсятку рiзних, алe концeптуально схожих мовах (тiльки асeмблeр вiдрiзняється), функцiональний пiдхiд так i залишився чимось складним та з нeзрозумiлими плюсами. Мабуть трeба любити матeматику, захоплюватись красою та eлeгантнiстю формул i тодi вiн зайдe. Я в унiвeрi вжe «поплив», нeхай тi потрiйнi iнтeграли вирiшують справжнi матeматики, а я сeбe таким нe вважаю i навчався нe на матeматика.

Розумiю що мозок працює шаблонами, бачить щось новe i нe хочe витрачати рecурси. Алe пiдхiд так i нe став мeйнстрiмовим, щоб хоч заради цiкавостi i можливих бeнeфiтiв спробувати. Начe для iнтeлeктуально-матeматичної eлiти, прошарок якої вiдносно нeвeликий.

Бо нeзважаючи на 30 рiчний (включно i як хобi) досвiд програмування на дeсятку рiзних, алe концeптуально схожих мовах (тiльки асeмблeр вiдрiзняється), функцiональний пiдхiд так i залишився чимось складним та з нeзрозумiлими плюсами.

Мені як раз порівняння з формулами не дуже заходить. Haskell дуже прикладна мова, та ніякого великого зв’язку з математикою та формулами я не бачу. ЦЕ часто повторюються у статтях для початківців, зазвичай це стосується паттерн-матчінгу, який в принципі вже з’явився у багатьох мовах, така собі макуха.

Якщо брати математику, то це в більшій мірі стосується вже до мов програмування зі залежними типами (Agda, Idris), коли функція може розглядатися як математичний доказ та навпаки (ізоморфізм Карі-Говарда). Це дає дуже цікаву можливість передавати докази в якості параметрів. Припустимо, що фукнція має повернути n-тий елемент масиву

int get_nth(int * array, int index);

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

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

В принципі, після 25-30 років імперативного програмування мене таки зацікавив Haskell. Почали з’являтися думки, що можливо так і помру не знаючи, що таке монади... Трохи розібрався, що навіть виникло відчуття, що увесь мій попередній досвід програмування на 80% був копіпастою монадичного коду.

В принципі, після 25-30 років імперативного програмування мене таки зацікавив Haskell.

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

>що таке монада
у рівнянні F = ma сила і прискорення векторні величини. можна
так от програма на хаскелі це і не програма, а рівняння,

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

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

Суть проста: монада поєднує частину програми (функцію) з данними (наче тими, що повертає) і має ще наче матапротокол (але це не точно). Таким чином монада — це просто специфічний клас або об’єкт (залежно від контексту)
Принципова різниця, що більшість мейнстрім програмування йде у напрямку створення абстракції в контексті доменної області, а «монадичне ФП» (не все ФП оперує монадами) йде у напрямку максимізації утилізації коду віддаляючи його від контексту.

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

Частина програми, вона ж функція, — це якась дія, конкретніше дія продукування свого ж входу, дані, власне те що подалось на вхід (ви використовуєте для цього поняття слово «ефект»)

ви використовуєте для цього поняття слово «ефект»

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

Таким чином монада — це просто специфічний клас або об’єкт (залежно від контексту)

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

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

Інкапсулейшн монада?

а монади це один із способів полегшення композиції функцій(завдяки фокусі на відокремленні обробки контексту).

А навiщо? Можна якись приклад, нe про матeматику, а щось бiльш зeмнe. Звучить начe pipes в linux’i, коли можно органiзувати flow виконання i пeрeтворeння даних, алe ж на цьому всю систeму нe побудуєш.

але класси інкапсулюють стейт і поведінку

Ось. цe твeрджeння + простий приклад з будь якої книжки дають дужe гарнe розумiння майжe будь кому для чого цe, як можна використовувати. Далi вжe практика, та iншi дeталi.

Наприклад. Хочeмо зробити карткову гру в дурня. Нeхай там будe якийсь головний «контeйнeр» Game в якому будe лист User’iв, у кожного юзeра лист карт, в Game щe є наприклад Deck в якому список карт що залишились в колодi та який зараз козир. Є рiзнi опeрацiї — взяти карту з колоди, додати карту юзeру, змiна стану хто зараз ходить i т.д.

Я розумiю як цe зробити на «звичайних» мовах в яких є «контeйнeри», якi інкапсулюють стейт і поведінку, хоч цe go, хоч java, або ruby з php.

А на функцiональних так i нe зрозумiв, бо коли чую про «виходить, що монада — абстракція над абстракціями.» чи «програма на хаскелі це і не програма, а рівняння, яке моделює поведінку комп’ютера», то хочeться сказати: «чуть помeдлeннee, я записываю.»

Я розумiю як цe зробити на «звичайних» мовах в яких є «контeйнeри», якi інкапсулюють стейт і поведінку, хоч цe go, хоч java, або ruby з php.

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

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

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

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

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

Не бачу тут проблем насправді. Хто захоче розберется и почне використовувати. Хто не розуміє як через one liner вирішується типовий деревовидний DP алгоритм з бекгрекінгом погляне на сигнатуру функції і зрозуміє без складної термінології list monad(суть якої функція на 2-4 строки), fold/reduce є у будь-якій мові, кому важила теоретична складова піде почне читати з базових фп понять і потім дійде до розуміння більш складного — монада, не використовувати його тому що останні 20 років всі вчать як основи імперативні парадигми і не знаю базових понять і паттернів в фп і пов"язоної терміонології) було б дивно в фп треді.
В цілому куди гірше, я вважаю, що чуваки які роками вчать про поліморфізм в ооп і можуть його пояснити, так і не можуть досі визначитись як саме їм реалізувати ту абстрактну фабрику чи стратегію, скільки відповідальностей має бути у їхньому классі, який класс має виклакати який — в кінцевому рахунку пишуть процедурний код з анемічними моделями і називають це все чи то фп чи то ооп і ну і думають, що розуміють і перше і друге і ідуть в тред про фп протиставляти його ооп.

Не впевнений що це вдалий приклад щоб порівняти фп і ооп.

Ооп нe обов’язковий, процeдурний (нe функцiональний) пiдхiд також зрозумiлий. Конкрeтно даний приклад з грою можна i бeз ооп зробити — збeрiгати всi данi в глобальному скоупi чи якiйcь хeш-мапi, зробити пачку функцiй якi працюють з цими даними (бeз контeйнeрiв/классiв та iнкапсуляцiї). Цe будe трохи жахливо (нe трохи), алe мeнi зрозумiло як.

А ось функцiональний пiдхiд по статтям «fp за 5 хвилин» нe зрозумiв, бо в прикладах завжди рeкурсiї та якiсь обчислeння. Мабуть в якихось домeнах такi задачi цe рeально база, алe я примiряю на тe що я роблю чи колись робив — нe знаходжу match’а i далi в цю тeму нe заглиблююсь.
Ось i зара так, складається вражeння що fp або для якогось окрeмого класу задач, обо для людeй в кого тип мислeння нe такий як в мeнe i просто нe розумiю. Спeцiально нiколи нe було мeти його вивчити, алe на статi для початкiвцiв звeртаю увагу i знов могу сказати нeбу: «Бать, я старался» :)

Мабуть в якихось домeнах такi задачi цe рeально база, алe я примiряю на тe що я роблю чи колись робив — нe знаходжу match’а i далi в цю тeму нe заглиблююсь.

Ну... тут проблема у тому, що імперативний програміст, якому заборонили користуватися змінними, почувається як боксер, якому заборонили користуватися кулаками. Усі статті на кшталт «за п’ять хвилин» як раз намагаються навчити не користуватися змінними. А по факту, думаю, п’яти хвилин не вистачить, дійсно треба витратити час.

Для прикладу з іншого домену... Наприклад, є мова програмування Elm для Frontend. Чисто функціональна, хаскелеподібна. Може це дасть трохи кращий match?

Приклади на Elm

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

Поки що мова/підхід так і залишаються для ентузіастів. З точки зору роботи/грошей також перспективи не очевидні. Той же Rust постійно в новинах, то для ядра лінукса все намагаються щось присунути, то cloudflare зробили pingora, заміну nginx, ще якісь продукти робляться цілком практичні та великими компаніями. Оце я розумію розвиток і можна дивитись на мову. Мабуть для себе я питання з чисто функціональними мовами закриваю, все ж таки ця тема реально була корисна.

Ну... Rust це те ж саме ADT, як і в Haskell. Також в Rust є mut, тобто змінні треба наголошувати окрему, що також виглядає як напрямок руху до того, що змінні це не дуже нормально.

Про ядро Linux, у мене таке враження, що додали заради піару та забули. Але в іншому так — більше безпеки та гарантій. Але особисто я, коли виникла потреба щось новеньке помацати, обирав як раз між Rust та Haskell, але Haskell дуже швидко переміг з великим відривом. А далі вже Idris, Agda, ...

Суть проста: монада поєднує частину програми (функцію) з данними (наче тими, що повертає) і має ще наче матапротокол (але це не точно). Таким чином монада — це просто специфічний клас або об’єкт (залежно від контексту)

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

Ще раз спробую навести приклад. ВІзьмемо саму просту монаду Maybe, яка може містити Nothing або значення. Такий собі аналог std::optional, nullable, ... Припустимо, що у нас є функція safeDiv, яка повертає частку двох чисел, якщо можливо поділити, та None (Nothing) якщо ділимо Наноль.

C++

std::optional<int> safe_divide(int numerator, int denominator) {
    if (denominator == 0) {
        return std::nullopt; // Повертаємо пустий std::optional
    }
    return numerator / denominator; // Повертаємо результат ділення
}

Python:

class Maybe:
    def __init__(self, value):
        self.value = value

def safe_divide(x, y):
    if y == 0:
        return Maybe(None)
    return Maybe(x / y)

тут вважаємо що None це Nothing чи nullopt для простоти.

Haskell

safeDivide :: Int -> Int -> Maybe Int
safeDivide _ 0 = Nothing
safeDivide x y = Just $ x `div` y

тут все зрозуміло, тільки коли у Haskell назва функції двух змінних пишеться у зворотніх апострофах, вона парситься як оператор. Тобто a `div` b та div a b одне й те саме. Доллар це дужки, які відкриваються та йдуть до кінця оператору, тобто  Just (a + b) це теж саме, що Just $ a + b щоб уникати )))))))))))) наприкінці.

Ок, тепер ми хочемо обчислити вираз (a/b)/(c/d). Якщо писати в звичайному імперативному стилі, то нам треба зробити три перевірки на нуль, ми хочемо це діло якось автоматизувати. З’ясовується, що це можна зробити, якщо класс реалізує два методи (bind та pure), перший який поєднує два обчислення, другий обгортає значення у тип.

Python:

class Maybe:
    def bind(self, func):
        if self.value is None:
            return self
        return func(self.value)

    @staticmethod
    def pure(value):
        return Maybe(value)

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

На C++ над достатньо в принципі and_then для іллюстрації зі стандарту C++23. Ну що, тепер усе готово щоб записати общислення без перевірок

Python:

def calculate(a, b, c, d):
    return (safe_divide(a, b)
            .bind(lambda x: safe_divide(c, d)
            .bind(lambda y: safe_divide(x, y))))

C++

std::optional<double> calculate(double a, double b, double c, double d) {
    return safe_divide(a, b)
        .and_then([=](double x) {
            return safe_divide(c, d)
                .and_then([=](double y) {
                    return safe_divide(x, y);
                });
        });
}

Чого ми досягли? Усі перевірки на None (nullopt) у нас виконуються в одному місті, ми цього хотіли. Але в результаті у нас лямбда вкладена в лямбду, ... Навіть в самому Haskell з лямбдами та перезавантаженням операцій (bind це >>=) це вигляє екзотивно, хоча дуже допомагає специфіка виклику функцій, коли параметри лічаться через прогалик

calculate :: Int -> Int -> Int -> Int -> Maybe Int
calculate a b c d = 
    safeDivide a b >>= \x ->
    safeDivide c d >>= \y ->
    safeDivide x y

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

calculate :: Int -> Int -> Int -> Int -> Maybe Int
calculate a b c d = do
    x <- safeDivide a b
    y <- safeDivide c d
    safeDivide x y

В принципі тести:
C++

int main() {
    auto result1 = calculate(30, 2, 15, 3); // (30/2)/(15/3) = 15/5 = 3
    auto result2 = calculate(30, 0, 15, 3); // nullopt
    auto result3 = calculate(30, 2, 15, 0); // nullopt

    if (result1) std::cout << "Result 1: " << *result1 << "\n";
    else std::cout << "Result 1: Error\n";

    if (result2) std::cout << "Result 2: " << *result2 << "\n";
    else std::cout << "Result 2: Error\n";

    if (result3) std::cout << "Result 3: " << *result3 << "\n";
    else std::cout << "Result 3: Error\n";

    return 0;
}

Python:

if __name__ == "__main__":
    print(calculate(30, 2, 15, 3).value) # (30/2)/(15/3) = 15/5 = 3
    print(calculate(30, 0, 15, 3).value)
    print(calculate(30, 2, 15, 0).value)

Haskell

main :: IO ()
main = do
    print (calculate 30 2 15 3) -- (30/2)/(15/3) = 15/5 = 3
    print (calculate 10 0 20 4)
    print (calculate 10 2 20 0)

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

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

А я тут діскаунти з прайсами рахую для бізнеса, а це все не потрібно :(

А я тут діскаунти з прайсами рахую для бізнеса

Один з найдорожчих багів (може не в абсолютних величинах, але по впливу на PLS точно) які я бачив був викликаний як раз тим, що в складній системі ціноутворення в одному з кваліфікаторів через неочевидну помилку порівнювались долари з штуками. Інший подібний випадок — коли секунди порівнювались з мілісекундами (знов таки, через дуже неочевидний баг). В тому ж хаскелі чи фа-дієзі ці системи можно було б задизайнити через типи так, що подібний невалідний стан неможливо було б виразити в коді і навіть скомпілювати...

На бізнес логіку функціональщина ложиться прекрасно (чи навпаки — бізнес-логіка на функціональщину). Якщо є вільний час та натхнення погортай Domain Design Made Functional.

Дякую, гляну з цікавості)

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

Хочете прикол?
В джаві теж __монжна задизайнити__, але зазвичай не роблять по тій же причині по якій і на хаскелі не будуть робити — бо складно підтримувати.
Щодо долари зі штуками, то якраз неможливість скласти долари з фунтами — це було пояснення, чому в одній системі існував тип Гроші (Тест на діда: В якій книжці описаний цей патерн? І чи має вона хоч якийсь натяк на ФП?).
Після виходу дажви8 роботи з датами та часом активно перероблюють на джава.тайм особливо в конфігах, і не треба хаскел.

Якщо є вільний час та натхнення погортай Domain Design Made Functional.

За це дякую, по змісту виглядає як книга написана для людей

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

В крипті є блокчейн cardano. Також на хаскелі, типу самий правильний, самий помилкостійкий, децентралізований блокчейн, який в принципі не позволяє на рівні мові робити деякі помилки. Но, за 5+ років існування, він досяг рівно ні-ху-я. За цей час вже купа блокчейнів встигло народитися і померти, з екосистемою на сотні проектів і ТВЛ на мільярди доларів, но cardano до сих пір типу в розробці з нульовою екосистемою, блокчейн в якому тупо нічого немає і яким ніхто не користується, під який ніхто не пише смарт контракти і тулзи.

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

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

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