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

UPD 2024-12-15: переніс приклади з tryocaml на gist, так як посилання на tryocaml нормально не працювали.

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

Я заходився писати коментар, але він виріс до таких розмірів, що можна його оформити окремим постом. Поїхали. Нащо мене слухати? Я займався комерційною розробкою на Haskell та останні 13 років комерційно пишу на OCaml.

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

Що раніше ви знаходите помилки, то дешевше їх виправити. Динамічні мови як Python, або статичні як C++ і Java, значною мірою покладаються на перевірки під час виконання або на багатослівні анотації типів для запобігання помилкам. Однак система типізації Гіндлі-Мілнера (Hindley-Milner, далі HM) та її варіації, що широко використовуються в функціональних мовах, таких як OCaml, Haskell, Eml, F#, Scala, Rust, пропонує радикально інший та потужний підхід: використання типів для усунення цілих класів помилок на етапі компіляції.

Основою HM є поєднання виведення типів з алгебраїчними типами даних (ADT та GADT). Виведення типів означає, що компілятор автоматично визначає типи будь-якого виразу та усієї програми загалом, роблячи код менш багатослівним, але при цьому строго типізованим. Це дає змогу розробникам зосереджуватися на логіці програми без компромісів у безпеці типів. Проте справжня сила HM полягає в її здатності усувати потребу в розлогому коді, наприклад, перевірках на null або захисному програмуванні. HM дає змогу кодувати інваріанти та усувати недійсні стани безпосередньо в типовій системі.

Але чому варто вивчати HM, якщо ви не використовуєте OCaml чи Haskell? Тому що ці принципи є універсальними. Розуміння HM допомагає по-іншому мислити про типи та їхнє використання для безпечного моделювання доменної логіки. Навіть у таких мовах, як C++ і Java, ви можете застосовувати подібні патерни для більш строгого кодування обмежень.

Але досить пропаганди, вона нікому не цікава без коду. Приклади будуть на OCaml, бо у коментах усі люблять правити код, а так більше ймовірність, що я не зроблю якусь тупу помилку :)

Мотивуючий приклад 1: система обліку чогось

Якщо ви вже знайомі з ADT та pattern matching, можна одразу гортати до прикладу 2.

Беремо умовну систему обліку чогось (що ми будемо називати assets). Спочатку в усіх assets був серійний номер і ми фіксували його в нашій системі. Згодом асортимент побільшав і з’явились assets без серійого номера. На них ліплять наліпки з «inventory ID», і це стало новим стандартом — на все вішають inventory ID. Інколи і на старі assets, які нам відомі під серійним номером. Теперь у нас є зоопарк: частина assets лише з серійним номером, частина — з inventory ID, частина — з тим і тим.

Можна це оформити так:

module Asset_v1 = struct
  type t = {serial:string option; inventory_id:string option; other_data:string}

  let set_inventory_id asset inv_id = {asset with inventory_id = Some inv_id}
                                      
  let needs_inventory_id asset =
    match asset.inventory_id with
    | None   -> true
    | Some _ -> false 
end

Дуже просто, дуже компактно, не дуже гарно. Так як обидва види ID є optional, то у нашому коді технічно можливі assets, у яких вони обидва відсутні, що є проблемою.

Замість цього зробимо ID алгебраїчним типом, якій кодує лише три допустимих комбінації:

module Asset_v2 = struct
  type id = 
    | Serial of string
    | Inventory_id of string
    | Serial_and_inventory_id of {serial:string; inventory_id: string}
                          
  type t = { id : id; other_data : string }

  let set_inventory_id asset inv_id =
    let new_id =
      match asset.id with
      | Inventory_id _ -> Inventory_id inv_id
      | Serial serial -> Serial_and_inventory_id {serial; inventory_id=inv_id}
      | Serial_and_inventory_id info -> Serial_and_inventory_id {info with inventory_id = inv_id} 
    in
    {asset with id = new_id}
    
  let needs_inventory_id asset =
    match asset.id with
    | Inventory_id _ | Serial_and_inventory_id _ -> false
    | Serial _ -> true
end

Код став більше за обсягом, але ми позбулися будь-якої можливості мати assets без serial та без inventory id у нашому коді. Крім того, обидві наші функції за рахунок pattern matching (або ще кажуть «деструкції») по типу ID тепер будуь перевірятися компілятором на тотальність покриття всіх варіантів («конструкторів») типу ID.

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

module Asset_v3 = struct
  type id = 
    | Serial of string
    | Inventory_id of string
    | Serial_and_inventory_id of {serial:string; inventory_id: string}
    | RFID of string
                          
  type t = { id : id; other_data : string }
end

Як тільки ми це зробимо, отримаємо помилки компіляції в обох функціях «set_inventory_id» та «needs_inventory_id». Які вкажуть на те, що ці функції не обробляють assets, у яких ID — це RFID. І нам потрібно буде додати цю підтримку, наприклад, так:

  let set_inventory_id asset inv_id =
    let new_id =
      match asset.id with 
      | Inventory_id _ -> Inventory_id inv_id
      | Serial serial -> Serial_and_inventory_id {serial; inventory_id=inv_id}
      | Serial_and_inventory_id info -> Serial_and_inventory_id {info with inventory_id = inv_id}
      | RFID _ -> Inventory_id inv_id
    in
    {asset with id = new_id}

  let needs_inventory_id asset =
    match asset.id with
    | Inventory_id _ | Serial_and_inventory_id _ -> false
    | Serial _ | RFID _ -> true 

Код можна взяти із gist і погратися з ним на tryocaml.

Мотивуючий приклад 2: API для баз даних

Уявимо собі просте API для бази даних:

module Jdbc : sig
  type connection

  val connect : config:string -> connection

  val execute : connection -> string -> unit
  val query : connection -> string -> string list

  (* wraps [f] in BEGIN ... END, does error handling/cleanup *)
  val in_transaction : connection -> f:(connection -> 'a) -> 'a
end

З його допомогою інші люди пишуть усілякі бібліотеки для Великого Бізнесу™:

module Account_management : sig
  (* IMPORTANT: always do these in transaction! *)
  val process1 : Jdbc.connection -> unit
  val process2 : Jdbc.connection -> unit

  (* IMPORTANT: This will fail to run in transaction! *)
  val update_report1 : Jdbc.connection -> unit

  (* Ok to run either in transaction, or outside of one *)
  val report2 : Jdbc.connection -> string list
end

Припустимо, що process1 та process2 виконують купу пов’язаних операцій в базі, і повинні виконуватись в рамках транзакції. У той же час update_report1 робить щось на кшталт «REFRESH MATERIALIZED VIEW CONURRENTY ...» в Postgres, і тому не буде працювати в рамках транзакції. Але також буде і велика кількість операцій, які, наприклад, цілком read only, і їх можна використовувати як в рамках транзакції, так і без.

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

let () =
  let open Jdbc in
  let open Account_management in
  let conn = connect ~config:"jdbc://dbhost/dbname" in
  (* BAD: User forgot to run the [process1] in transaction *) 
  process1 conn;
  (* This is actually fine *)
  update_report1 conn;
  in_transaction conn ~f:(fun conn ->
      process2 conn;
      List.iter print_endline (report2 conn));
  (* BAD: user does nested transactions, but our database does not support them. *)
  in_transaction conn ~f:(fun conn ->
      process1 conn;
      in_transaction conn ~f:(fun conn ->
          process2 conn));
  (* BAD: Report1 cannot be ran in transaction! *)
  in_transaction conn ~f:(fun conn ->
      update_report1 conn)
;;

(Повна версія коду с іграшковою реалізацією цих API на gist , можете погратися з нею на tryocaml)

Як ми можемо покращити цю ситуацію? Наприклад, можна додати до Jdbc.connection якійсь boolean, який вказує, чи ми зараз в транзакціі, чи ні. І потім в рамках Account_management можемо на цей boolean дивитсь, і кидати exception при некоректному користуванні. Але це будуть перевірки часу виконання.

Чи можемо ми виключити можливість некоректного використання функцій з Account_management на етапі компіляції? Так, можемо. Ми можемо зробити тип Jdbc.connection параметричним, і у якості параметра використовувати тип з двома конструкторами regular та in_transaction, які будуть кодувати поточний стан з’єднання:

module Jdbc : sig
  type 'a connection

  val connect : config:string -> [`regular] connection

  val execute : _ connection -> string -> unit
  val query : _ connection -> string -> string list

  val in_transaction : [`regular] connection -> f:([`in_transaction] connection -> 'a) -> 'a
end

Ці зміни — лише в сигнатурі фунцій, нам не потрібно додавати жодного коду в імплементацію! Тепер система типів «знає», що для in_transaction потрібне з’єдння, яки ще не є в транзакції, а для execute та query можно використовувати connections як в транзакції, так і ні.

Теперь функції з Account_management можуть вказувати, якого виду connection ім потрібен:

module Account_management : sig
  (* IMPORTANT: always do these in transaction! *)
  val process1 : [`in_transaction] Jdbc.connection -> unit
  val process2 : [`in_transaction] Jdbc.connection -> unit

  (* IMPORTANT: This will fail to run in transaction! *)
  val update_report1 : [`regular] Jdbc.connection -> unit

  (* Ok to run either in transaction, or outside of one *)
  val report2 : _ Jdbc.connection -> string list
end

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

Тепер усі «погані» приклади просто перестають компілюватись:

Line 61, characters 11-15: "process1 conn"
Error: This expression has type [ `regular ] Jdbc.connection
but an expression was expected of type [ `in_transaction ] Jdbc.connection
These two variant types have no intersection

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

(Тут можна погратись з повним кодом після додавання type parameter і переконатись, що некоректні варіанти дійсно не компілюються)

Таким самим чином ми можемо розрізняти файли, відкриті на запис та на читання, і не давали писати в r/o файли на етапі компіляції:

module type File = sig
  type 'a file
  
  val open_ro : string -> [`ro] file
  val open_rw : string -> [`rw] file
  
  val read : _ file -> len:int -> bytes
  val write : [`rw] file -> bytes -> unit  
end

Або розрізняти empty, sparse та compact буфери в реалізації якогось формату сериалізації, і не давати (на етапі компіляції) писати на диск sparse та empty буфери:

module type Buffer = sig
  type 'a buffer
  
  val create : unit -> [`empty] buffer

  (* [delete] removes [len] bytes at [offset] and marks buffer as sparse *)
  val delete : _ buffer -> offset:int -> len:int -> [`sparse] buffer

  (* [compact] removes gaps introduced by [delete] *)
  val compact : [`sparse] buffer -> [`compact] buffer    
      
  (* [append] does not change the sparseness of the buffer, but
     it becomes non-empty if it was empty *)    
  val append : [`empty|`compact|`sparse] buffer -> bytes -> [`compact |`sparse] buffer

  (* you can only save [`compact] buffers *)
  val write_to_disk : [`compact] buffer -> unit      
end

Використання потужних систем типізації унеможливлює некоретні стани в вашій програмі, економить вам час і нервові клітини. Ось чому корисно вчити Haskell/OCaml/Scala/...

Сподіваюсь, що це було достатньо близько до реального життя :)

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

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

Треба колись зібратися і написати шось отаке про LabView, мову G, та екосистему National Instrument. Набагато цікавіше і корисніше буде навіть для тих, хто ніколи не буде на ньому писати ))

не треба, я вже в універі з тим насидівся

LabView

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

Здогадуюсь шо за хню вам давали у виші )) класика вітчізняної псевдо-вищої освіти в різних політехаха, які ні з бугра поставали «університетами». На цьому робляться самі різні системи. Від маленьких для якихось наукових експериментів, до огромних індастріал з купою обладнання. З приблизно 20-30 річного досвіду використання, можу сказати, шо з дуже виликою вирогідністю, ті шо вам за нього розповідали, не робили на ньому нічого, крім примитівних прикладів, та і то в стилі програмування на Паскалю. Там зовсім інший принцип і підхід до снаряду — data driven flow, на відміну від звичайного control flow в алгорітмічних мовах. Розуміння цього принципу і вміння на ньому мислити і програмувати, є набагато корисніше для загального розвитку, чим фігня з типами цього топіку. І до речі, там є усе, і версіоність, та інше.

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

«Є лише дві думки — одна невірна, а друга — моя»

:)

Це працює в обидві сторони ;)

Невже я десь написав «, а усе інше — це якась фігня»? Піду перечитаю.

Ага, перечитайте. Починаючи із заголовка «Чому програмісти мають вивчити мову....», тобто, усі(!) мають займатися якоюсь не потрібною фігнею, бо

«Є лише дві думки — одна невірна, а друга — моя»

Перечитав. Бачу, що поки що про «фігню» вдруге пишете сами ви, а не я, і ви продовжуєте вичитувати поміж слів в моєму тексті те, чого там немає.

Якщо брати до уваги тільки букви — так. А якщо суть — ніт. Ліпше вчить LabView, користі в тому для розвитку набагато більше ))

LabView для Lego Mindstorms було дуже доцільним.

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

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

Там зовсім інший принцип і підхід до снаряду — data driven flow, на відміну від звичайного control flow в алгорітмічних мовах. Розуміння цього принципу і вміння на ньому мислити і програмувати, є набагато корисніше для загального розвитку, чим фігня з типами цього топіку.

Такий самий принцип є у Boomi (фігня для створення інтеграцій) — все граф, по якому бігають «документи».
Загальна користь від такого підходу доволі сумнівна, оскільки ти обмежений «потоком» і тим, що ти можеш із ним зробити. Але тут може ще грати роль обмеженість і бідність самого Boomi — якби там не було б скріптів (JS та Groovy), то можна було б застрелитись.
Сподіваюсь, у LabView також є скріпти.

Версійність — у Boomi воно також є, але зроблено тотально «на от’єбісь».

Дякую за статтю! Було-б цікаво бачити більше таких статтей тут!

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

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

І те саме відноситься до будь-якої сфери діяльності.

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

І я зараз не хочу сказати, в жодному разі, що динамічні мови програмування — це лише для тих, хто не зміг. Це ж не просто так Python використовується в Data Science — бо витрачати когнитивний ресурс одночасно на вибір та ефективне використання «апаратних» типів даних і аналіз даних просто неможливо. Він обмежений у бідь-якої людини і у генія також.

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

Цікаво, а що більше використовується в реальному продакшн — OCaml чи Haskell? І в чому основна різниця між ними (ідеологічно)? І наскільки Rust функціональний, якщо порівнювати з ними? Ну, тобто, скажемо так з теоретичної/математичної точки зору.

Просто тому, що ФП має більшу когнитивну складність. Так само, як і писати на асемблері.

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

І в чому основна різниця між ними (ідеологічно)?

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

І наскільки Rust функціональний, якщо порівнювати з ними?

Ну... певні кроки є, але.. у порівнянні з Haskell зовсім не функціональний. Самий простий тест — монади.

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

Я приблизно це і мав на увазі. Це різні полюси складності.

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

Джаваскріпт або краще тайпскріпт (де наче є ГМ типи)?
ФП — це досить проста штука. Інша історія, що часто про нього розповідають «категоричні ендофунктори».
Але от ТС приклад того, що в тій спільноті є адекватні люди. Ось класний приклад доповіді «для людей» dou.ua/...​rums/topic/51522/#2913615 . Кложур спільнота була колись адекватною

I.

Просто тому, що ФП має більшу когнитивну складність.

Ні, не має. Щонайменше, поки не звикнеш до процедурного стилю. А ось з процедурного вже переходити на функціональний складніше, бо ламає звички.
Тому я і кажу, що якісний програміст має знати хоча б основи одної мови ФП (LISP, Erlang...) і Forth, щоб побачити те ж саме з різних сторін. Тоді для будь-якого засобу розуміння буде вже кращим. (Не кажу про Prolog, це вже для джедаїв-первертнів. Але і він може бути на користь.)

Але для крудошльопства це таки не обовʼязково.

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

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

Порівняйте у цьому аспекті хоча б C, Pascal і Ada. В Ada автовиводу типів, здається, немає, але ви можете легко обмежувати діапазони значень. Алгебраїчні типи на зразок {ok,Result}|{error,Error} теж можна, з ускладненнями. Це вже щось більш цікаве.

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

Не памʼятаю, чи наводив я тут приклад. Функція заповнює масив до 4 значень і повертає кількість заповненого, або −1 якщо помилка, в int8_t. Функція, що її викликає, кладе результат в uint8_t, заповнює масив значень (дивом нічого не ломає) і передає по мережі. При помилці воно втискує 255 (бо −1) значень в масив на 4 комірки. Інший модуль приймає, десеріалізує і нарешті крешиться через переповнення в стеку і псування адреси повершення. Зрозуміло, що мова з AutoMM, як Java, C#, Python, Go, чи десятки інших, не дозволила б такий ефект. Але і щось рівня C, але з якісним контролем типів, просто не спонукала б на таке диверсійне використання одного значення...

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

Функціональний підхід з’явився чи не в той же час, як і процедурний. Але поширення такого не мав. Чому?

Людина, яка чудово розуміє і використовує функціональний підхід, принаймні за моїм досвідом, не має проблеми писати у процедурному стилі. А ось навпаки це чомусь викликає проблеми, бо «ломає звички» ;)

Але для крудошльопства це таки не обовʼязково.

Тобто?

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

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

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

З точки зору мого коментаря про складність — це одне і те ж. Я ж про складність.

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

І я зараз не хочу сказати, в жодному разі, що динамічні мови програмування — це лише для тих, хто не зміг. Це ж не просто так Python використовується в Data Science — бо витрачати когнитивний ресурс одночасно на вибір та ефективне використання «апаратних» типів даних і аналіз даних просто неможливо. Він обмежений у бідь-якої людини і у генія також.

Уявіть, що вам треба обробити деякий масив даних. Мій особистий досвід з тих мов, що я знаю:

Python — Закинув все у список чи словник і вперед. Я думаю на цьому етапі виключно про те, що я роблю з даними.

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

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

Але і з функціональним підхідом те саме. Я пишу модулі на terraform і у нас купа, скажемо так, бізнес логіки і бізнес-правил, коли з output одних ресурсів треба зробити input для інших, з перетворенням цих даних. І все, що в мене є для цих перетворень — функції самого terraform (доволі обмежена множина) — в принципі, той самий функціональний підхід, коли з однієї чи кількох вхідних структур даних отримуємо іншу структуру даних виконуючи послідовно якісь функції. І за моїм досвідом — це складно. Не тільки мені, я то справляюсь, просто написати це на Python чи PowerShell значно простіше, але і іншим, бо більшості співробітників важко розуміти що і як я зробив. І важко підтримувати. Тому я і кажу про когнитивну складність.

Функціональний підхід з’явився чи не в той же час, як і процедурний. Але поширення такого не мав. Чому?

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

Чому поширення такого не мав? Дуже просто. Компʼютер на найнижчому рівні працює процедурно. У нього є послідовність станів і переходи між станами. Коли був тільки асемблер, вчили процедурній роботі. Коли зʼявились перші мови вищого рівня, вони теж були процедурні, бо не вистачало ресурсів на ефективну трансформацію з функціональної побудови. Тоді навіть гарних оптимізуючих компіляторів не було, вони зʼявились ближче до 1990, а раніше найліпші робили код в рази повільніший за людину з асемблером.

І саме в цей період виникли всі основні мови и виникли традиції. Вибачте за міряння сивиною, але я почав в 80-х і все це бачив власними очима. Якась ЕС-1022, що споживала десятки кВт, мала швидкість і памʼять в сотні разів менше найменшої Raspberry Pi. Не могли тоді нормально ганяти програми на якомусь LISP всюди і для всього, всі ФП були нішевими саме через ресурси.

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

А традиції, в яких, звісно, процедурний стиль займав головне місце, вже склались.

А ось навпаки це чомусь викликає проблеми, бо «ломає звички» ;)

Я не мав досвіду в широкій масі з тим, що хтось навчився спочатку на LISP, а потім його пересадили на якийсь Python. Але є окремі знайомі, що скаржаться, що їм так незручно. І є група, яка вчилась майже з нуля програмуванню під Erlang, і їм легко зайшло. Так що не тільки навпаки.

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

Нітъ! Процесор працює саме повністю і безперечно з процедурним кодом. Альтернативи тут немає.
Навіть якась LISP-машина, яких робили навіть серійно, просто має орієнтацію на конкретні структури даних і методи їх покрокової обробки.

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

Обирається той підхід, на який є люди і ресурси. І ось тут спрацьовує, що всі 80 активних років IT перевага була на боці процедурного підходу саме через його близкість до заліза. Його не просто використовують — йому, через те, вчать. А раз йому вчать — то він і далі розвивається більше, ніж альтернативи. Замкнене коло.

Уявіть, що вам треба обробити деякий масив даних.

А поясніть-но мені, до чого тут не мої слова? Ви вставили як цитату щось що я не писав.

Хоч і не мій приклад, але прокоментую:

Тобто значно більше когнитивне навантаження.

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

Якщо ви далі обробляєте дані в цьому ж коді, то вам треба зрозуміти, що робити з цими даними. І ось тут вистрілює те ж саме навантаження.

Я не бачу принципової різниці.

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

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

А традиції, в яких, звісно, процедурний стиль займав головне місце, вже склались.

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

Тобто якісь речі, підходи, шаблони і паттерни стають мейнстримом, а якісь — ні.

Чому?

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

Вже 30 рочків, як це є. За 30 рочків багато чого стало мейнстримом, на що раніше не вистачало ресурсів.

Чому про динамічні мови кажуть «та просто візьмемо потужніше залізо» — а ФП це досі обмежує?

А поясніть-но мені, до чого тут не мої слова? Ви вставили як цитату щось що я не писав.

Я процитував сам себе просто щоб не повторюватись.

Вам треба просто використати універсальний контейнер. Такі є і в Rust.

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

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

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

Я ніколи не писав у функціональному стилі і не писав на terraform до того, як почав працювати на проекті. Зі мною на цей проект приходили люди з бекграундом PowerShell і без знання Python. Приходили без знання terraform, приходили з бекграундом Python і без знання PowerShell — зараз всі, за потреби, пишуть скрипти та функції (я маю на увазі хмарні — AWS Lambda & Azure Function) на обох мовах. Більше того, якось перейшли до декларативного підходу при написанні Ansible playbook (хоча тут вже було складніше) — тільки 3 речі викликають проблеми:

— чисто функціональний підхід до перетворення даних в teraform
— регулярні вирази
— однострічники в скриптах (bash/grep/sed/awk/...) — по суті той самий функціональний підхід.

Це не викликає проблеми у мене. Ну, так, спочатку складніше, але принципово проблем немає. І я так само можу сказати:

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

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

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

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

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

Тобто якісь речі, підходи, шаблони і паттерни стають мейнстримом, а якісь — ні.

Чому?

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

Я навіть більше прикладів покажу. Он є така штука kdb+. Пошукайте програми для неї. І краще на k, ніж на q (це її вбудовані мови такі). Це гірше ніж ФП. Але аналітика великих банків сидить на ній, і платять шалені гроші за ліцензії — коли воно ще було не почасово, було 50k$ за процесор. Я на ній дещо писав, більш не хочу:) але вражень набрався. Чи треба їм розповсюджувати на всіх? Думаю, ні. (Але можуть — знаю людей, які створили схожі униіверсальні аналоги.)

Вже 30 рочків, як це є. За 30 рочків багато чого стало мейнстримом, на що раніше не вистачало ресурсів.

Чому про динамічні мови кажуть «та просто візьмемо потужніше залізо» — а ФП це досі обмежує?

Ось тут саме та інерція, про яку кажу. Нема невідʼємного обмеження динамікою. Зараз у Haskell, Ocaml, інших дуже гарні компілятори. Є обмеження традиціями і налагодженим конвеєром освіти і праці.

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

Є готові. Посилань не накопичував, але бачив.

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

Я не знаю terraform. Може, це його проблема. Я 5 років писав на Erlang. Це легко, якщо вийти за шаблони процедурного мислення. У нас була група асушників-адмінів, яких вчили програмувати саме на ерлангу. Це було легко. Не було нічого на погляд тяжкішого, ніж звичайне навчання того, хто вже в IT, але не програмував.

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

Ви могли б отримати достатній досвід для порівняння, якщо б писали хоча б в обʼємі півсотні задач якогось codingame, наприклад, на LISP (будь-якому діалекті, можна і Scheme, і CommonLisp, і Clojure) і Erlang. Або Erlang і Ocaml. Або LISP і Haskell. Тоді б ви могли сказати, проблема в Terraform одному чи в цілому в ФП.

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

Ви не бачили, я бачив. Особливо коли використовуються, наприклад, віконні функції. Ось це як раз близько до чистого ФП. Або конструкції з многорівневим вкладеним select і not in, там треба серйозно розгорнути в голові, щоб зрозуміти, що воно робить.
Але SQL дуже обмежена декларативна мова (за що його лають).

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

Згоден. У мене теж, але саме з ФП у мене явно більше досвіду, як і з його порівнянням з ПП. Приклади, як люди успішно входять через ФП, показують, що не можна думати, що тільки ПП «інтуітивне» чи якось таке.
(Або ІП? Я весь час звав його процедурним, хоча багато хто зве імперативним. Може, останнє точніше.)

Он є така штука kdb+

Ну дивіться. Є дещо принципово нішеве. ФП ж не позиціонується як щось нішеве?

Є готові. Посилань не накопичував, але бачив.

Та є, але на суто моїх задачах, все одно їх треба адаптувати. А Python — запхнув все у єдиний список і не звертаєш уваги на тип даних.

Ви могли б отримати достатній досвід для порівняння, якщо б писали хоча б в обʼємі півсотні задач якогось codingame, наприклад, на LISP (будь-якому діалекті, можна і Scheme, і CommonLisp, і Clojure) і Erlang. Або Erlang і Ocaml. Або LISP і Haskell. Тоді б ви могли сказати, проблема в Terraform одному чи в цілому в ФП.

По-перше, в terraform таки є проблеми, з обмеженістю вбудованих функцій і розширенням, скажемо так, мови. Але. Справа ж не в конкретній мові. Справа в підході.

Доречі. Яка з функціональних мови найбільш широковживана? тобто використовується у реальних проектах у найбільшому числі різних областей? Чи, може, навпаки — яка з широковживаних мов найбільш «функціональна», скажемо так. Це я питаю, бо я б подивився, але корисніше було-б, якщо можна було б відразу мати реальні застосування при вирішення практичних завдань. У мене це автоматизація чи обробка даних на Linux/Mac. Що порекомендуєту з вашого досвіду?

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

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

що не можна думати, що тільки ПП «інтуітивне» чи якось таке.

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

ФП ж не позиціонується як щось нішеве?

Ні. А треба? ;)
Фактично як раз воно близько до нішевого, і це не зовсім адекватно його корисності.

Доречі. Яка з функціональних мови найбільш широковживана? тобто використовується у реальних проектах у найбільшому числі різних областей?

Моя власна думка: LISP в усіх варіантах. Другим Erlang (включаючи Elixir). Третім OCaml.
А тепер дивимось на поточний TIOBE (так собі критерій, але щось відображає): 21 — LISP. 30 — Haskell. 31 — Scala. 48 — Elixir. 49 — ML (все сімейство). 50 — Clojure (можна вважати дуже специфічним варіантом LISP). Erlang, F# після них в 51-100. Ну, близько до того що я думав.

У мене це автоматизація чи обробка даних на Linux/Mac. Що порекомендуєту з вашого досвіду?

Не знаючи характер даних і методи обробки... нічого.

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

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

Навпаки для математиків більш інтуїтивним повинно бути якраз ФП.

Саме так. (Згадуючи деякі спілкування з «чистими» математиками...)

Ні. А треба? ;)

Звісно ні, це ріторичне питання було ;)

LISP в усіх варіантах.

мені його синтаксис не подобається ;)

А якщо серьозно, дякую, подивлюся.

Після цього перемкнутись складніше і, типово, ліньки.

Може й так, але чи багато з тих, хто почав з функціональних мов мав проблеми зворотнього переходу? ну, тобто, lisp чудово розумію, а python не розумію? ;) Причому ні незручно, а саме не розумію, не можу щось зробити.

Я, наприклад, про таких взагалі не чув.

ну, тобто, lisp чудово розумію, а python не розумію? ;) Причому ні незручно, а саме не розумію, не можу щось зробити.

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

Може й так, але чи багато з тих, хто почав з функціональних мов мав проблеми зворотнього переходу?

Джоель Спольски писав, проблема зрозуміти вказівники :-)

Прикольно ;)

Ну насправді до вивчення С я ж тех не стискався з вказівниками а ні в Basic, а ні в Pascal — але вказівники як такі це не частина функціонального чи імперативного підходу.

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

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

Доречі, щодо абстракції від реалізації — читаю зараз про ліниві обчислення у Haskell — і про те, що функції можна явно вказати — «тут обчислюй явно» (для уникнення space leak) — все ж таки від фізики ніде дітися і конкретні моменти треба враховувати, як би ти не хотів мислити в суто математичній парадигмі. ;)

Ну насправді до вивчення С я ж тех не стискався з вказівниками а ні в Basic, а ні в Pascal — але вказівники як такі це не частина функціонального чи імперативного підходу.

Спольский писав про LISP, та, ІМХО, це більше відноситься більше до мов з динамічними типами. І скоріше за усе це архітектурні аспекти, зазвичай ти будеш з таким плавати.

ФП намагається абстрагуватись від фізичної/аппаратної реалізації,

Зовсім паралельно, якщо чесно. На Haskell є проект операційної системи, також у тестах швидкодії правильно написаний Haskell часто не поступається по швидкості Сі. Тут більше питання, чи є доступ до машинних типів, у Haskell вони є (Int, Word, Float, Double).

імхо — не плутати операції над адресами від операцій над значеннями та тримати в голові розміри конкретних структур даних

Це другорядне, головне це уявляти як це працює в адресному просторі.

Доречі, щодо абстракції від реалізації — читаю зараз про ліниві обчислення у Haskell — і про те, що функції можна явно вказати — «тут обчислюй явно» (для уникнення space leak) — все ж таки від фізики

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

головне це уявляти як це працює в адресному просторі.

так а що там специфічного і важкого для уяви?

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

Ну, для мене виключно теоретичний інструмент для математиків це щось з Mathematica/Maple/Axiom/etc

так а що там специфічного і важкого для уяви?

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

Ну, для мене виключно теоретичний інструмент для математиків це щось з Mathematica/Maple/Axiom/etc

А навіщо він математикам? Вони і програмувати часто не вміють. Символічні обчислення Haskell не застосований, до Maple далеко. Чисельні методи знову MATLAB або Python.

Знову таки, наприклад, подивимося на cabal: Browse and search packages, відсортуємо по скачуванням (DLs) за останні 30 днів. Що на першому місті? Дивимося на топ-20

 1. pandoc           2090    Conversion between markup formats
 2. http2            1702    HTTP/2
 3. tls              1539    TLS protocol native implementation
 4. hspec            1396    A Testing Framework for Haskell
 5. network          1378    Low-level networking interface
 6. ghc-lib-parser   1377    The GHC API, decoupled from GHC versions
 7. texmath          1374    Conversion between math formats.
 8. aeson            1322    Fast JSON parsing and encoding
 9. Cabal            1190    A framework for packaging Haskell software 
10. hashable         1161    A class for types that can be converted to a hash value
11. conduit          1137    Streaming data processing library.
12. lens             1110    Lenses, Folds and Traversals
13. http-client      1108    An HTTP client engine
14. persistent       1099    Type-safe, multi-backend data serialization.
15. skylighting      1093    syntax highlighting library
16. yaml             1015    Support for parsing and rendering YAML documents
17. text             1014    An efficient packed Unicode text type.
18. ansi-terminal     964    Simple ANSI terminal support
19. typst             956    Parsing and evaluating typst syntax.
20. extra             905    Extra functions I use.

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

Я маю дещо інший досвід щодо:

Вони і програмувати часто не вміють.

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

Причому перші, часто, страшні формалісти.

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

А ось перші, так.

Тож, я не став би узагальнювати.

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

Ну, я не починав з Python, просто він був (та і є) моїм інструментом-мультітулом довги роки.

Щодо мов програмування, поки я вчився у школі, доступних варіантів, крім basic тупо не було. В університеті мене вчили pascal але я спробував С і він здавався (і здається) значно зручнішим ніж pascal/basic. З боку високорівневих інструментів я захоплювався Maple/Mathematica — але більше для символьних обчислень, бо це виглядало дуже круто, що можна маніпулювати дуже складними рівняннями. Але це не сприймалось як програмування — скоріше як експеримент та фізика (бо саме ці задачі я і вирішував).

І, повертаючись до фізиків/математиків — перші («формалісти») — «боялися», не розуміли і не використовували Mathematica, працюючи олівцем на папері. В той час як другі — чудово використовували як олівець так і Maple/Mathematica і навіть Java коли щось треба було порахувати ;)

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

Я скоріше мав на увазі прикладне програмування: розгорнути HTTP сервері, ... Приблизно те, що у списку. Так математики розуміють програмування, можуть вирішувати свої задачі, leetcode, розробляти алгоритми. Воно можуть освоїть backend при потребі, але... потреби часто немає.

В університеті мене вчили pascal але я спробував С і він здавався (і здається) значно зручнішим ніж pascal/basic.

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

навіть Java коли щось треба було порахувати

Порахувати, а не встановити TCP з’єднання через openssl.

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

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

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

;D

Колись я почав писати всіляку автоматизацію і perl був майже всюди по дефолту, і на той час, для скриптів Perl/Python/Ruby в принципі були рівноцінні — я обрав Python суто тому, що мені більше подобався синтаксис. (ну, ще тому, що кілька ліб значно простіше було встановити для Python ніж для Perl, але скоріше це був більше привід).

Те саме зараз, коли на моїх слабеньких віртуалках (та Raspberry Pi Zero) стало замало оперативки для кількох «демонів» на Python — можна було переписати їх на C/C++ але якось Rust приглянувся більше. З точки зору можливостей обрати можна було будь який.

З іншого боку, читати Python завдяки синтаксису, значно легше читати.

Так само Rust має переваги, як на мене, над C.

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

Ну... можна порівнювати Rust та Cі, бо це різні мови. Хтось буде акцентувати на безпеці, хтось на реалізації алгоритму dancing link без заміни вказівників на індекси.

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

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

Головна проблема це неконсистентність змінних, коли змінні отримують значення, про які ти й не подумав, що воно може виникнути. І тут не допоможе класичний unit-тест, бо ти не можеш протестувати кейз, про який ти взагалі не подумав. Так, частково може допомогти великий текст, який штучно нагенерує уся можливі варіанти та перевірить. Якщо брати на різні мови, то ADT (як в Rust) вже структурує та обмежує такі неконсистні стани, бо щоб зайти у середину треба робити паттерн-матчінг. Ну а немає змінних — ще більше контролю та складніше припустити помилку. Що також гарний стиль у Rust (mut треба вказувати окремо).

Взагалі, Rust має дуже великий вплив від чистого ФП: перший компілятор був написаний на OCalm, ADT, ... Чимось він проміжний.

Ну насправді до вивчення С я ж тех не стискався з вказівниками а ні в Basic, а ні в Pascal

как можно в паскале не сталкиваться с указателями?
разве что в 3-4 версии? (я беру турбо паскаль, как наиболее распространённый)
90% «алгоритмов и структур данных» используют динамическое распределение памяти и указатели
а ООП без него вообще никак

Ми вчили якусь версію Pascal без ООП, ще на PDP-11 ;) А basic в мене був на ZX80.

Turbo Pascal я побачив після С і тоді я не побачив жодної переваги Turbo Pascal.

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

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

Ми вчили якусь версію Pascal без ООП, ще на PDP-11 ;) А basic в мене був на ZX80.

А, ну так то да, согласен

Turbo Pascal я побачив після С і тоді я не побачив жодної переваги Turbo Pascal.

Однопроходный компилятор — наше всё. скорость разработки значительно выше.

Однопроходный компилятор — наше всё. скорость разработки значительно выше.

мені здається, написати скрипт щоб збирав все «за один раз» (однією командою) можна було навіть тоді. Або я не зрозумів, про що ви.

мені здається, написати скрипт щоб збирав все «за один раз» (однією командою) можна було навіть тоді. Або я не зрозумів, про що ви.

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

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

Pascal це мова, яка підтримувала модулі, що давала можливість просто компілювати модулі у порядку, а потім використовувати вже скомпільовані результати.

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

Хедеры (а после — компайлед хедеры), если правильно помню, как раз появились из-за желания не парскить/компилить сорцы несколько раз
В паскале аналогичной цели служили декларации юнитов

Ну... не зовсім так.

Якщо брати pascal, то кожен unit має interface та implemetation. Це дозволяло визначити порядок, в якому треба компілювати модулі, за це відповідав uses в секції interface.

unit Unit1;
interface
uses Unit2;
implementtion
end.

та

unit Unit2;
interface
implementtion
uses Unit1;
end.

То першим компілювався Unit2, потім Unit1. Якщо в обох зробити uses в секції interface ти отримуєш circular module dependency чи щось подібне.

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

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

#include <header1.h>
#include <header2.h>

а в іншому

#include <header2.h>
#include <header1.h>

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

void magic_func(void);

а в другому

#define magic_func debug_magic_func

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

#include <math.h>

нам треба взяти файл math.h, перечитати, розпарсити, побудувати таблицю символів, та додати до поточної одиниці трансляції. На відміну від

uses Math;

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

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

Якщо брати pascal, то кожен unit має interface та implemetation.

Да, я знаю (ну, если быть честным, то «знал». 100 лет не совался туда уже)
Но тем не менее — интересный и полезный экскурс, спасибо
Людям будет полезно

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

Эээ или вы о какой-то специфической тонкости, или неправы. Именно у C такой проблемы нет. Все декларации должны быть встречены до использования соответствующих переменных (и функций — кроме неявного int foo(...), которое рудимент в C и некоторыми компиляторами уже не принимается, ну и опциями часто можно запретить). Если нужно сделать рекурсию, например, то там требуется форвард-объявление. В этом смысле он не отличается от Паскаля.

А вот чем отличается — это что встретив идентификатор, не заглянув в таблицу типов, часто нельзя понять, что значит конструкция с ним. Может, это замедляло парсинг кода на C во времена TurboC. А, может, штуки типа x++. Или линковка...

А вот в C++ уже есть требование, по сути, не парсить до конца пока не уяснены смыслы всех идентификаторов — то же most vexing parse: `a b©;` это определение переменной или объявление функции? Пока не понятно, что такое `c`, решать однозначно нельзя, а оно может оказаться в конце определения класса, в котором эта конструкция где-то в теле метода. Есть и похуже случаи.

Добре, як тоді пояснити це?

$ cat test.c 
int f1(int n)
{
    if (n == 0) return 1;
    return n * f2(n-1);
}

int f2(int n)
{
    if (n == 0) return 1;
    return n * f1(n-1);
}

int main()
{
    printf("Hello world: %d!\n", f2(5));
}
$ gcc test.c
test.c: In function ‘f1’:
test.c:4:16: warning: implicit declaration of function ‘f2’; did you mean ‘f1’? [-Wimplicit-function-declaration]
    4 |     return n * f2(n-1);
      |                ^~
      |                f1
test.c: In function ‘main’:
test.c:16:5: warning: implicit declaration of function ‘printf’ [-Wimplicit-function-declaration]
   16 |     printf("Hello world: %d!\n", f2(5));
      |     ^~~~~~
test.c:16:5: warning: incompatible implicit declaration of built-in function ‘printf’
test.c:1:1: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
  +++ |+#include <stdio.h>
    1 | int f1(int n)
$ ./a.out 
Hello world: 120!

Так, мова розвивається, зараз ми отримуємо warning, а потім вже помилку лінковки, так, його можна заборонити, але мова йшла про 90-ті роки.

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

Добре, як тоді пояснити це?

Саме так, що
1) Мова вимагає предоголошення функцій: тобто, нормально мав бути прототип: `int f2(int);` перед тілом f1().
2) Якщо такого прототипу нема, то спрацьовує фолбек, що декларує `int f2();` що те ж саме що `int f2(...);`, але компілятор видає попередження.
3) Завдяки вимозі, щоб працювало і в такому випадку, правила ABI для випадку еліпсіса зроблені так, що все працює (наприклад, з x86-64/SysV перший аргумент завжди в rdi).

але мова йшла про 90-ті роки.

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

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

Проблема не тільки в стеку. Вся calling convention має бути підлаштована під це.

Ось наприклад соберіть щось найпростіше типу

#include <stdio.h>
#include <stdarg.h>
int my_printf(const char *format, ...) {
  va_list ap;
  va_start(ap, format);
  int rc = fprintf(stdout, format, ap);
  va_end(ap);
  return rc;
}

Скомпілюйте і подивіться на асемблерний результат — це ж буде жах. Під Linux/x86-64:

my_printf:
.LFB23:
        .cfi_startproc
        endbr64
        subq    $216, %rsp
        .cfi_def_cfa_offset 224
        movq    %rsi, 40(%rsp)
        movq    %rdx, 48(%rsp)
        movq    %rcx, 56(%rsp)
        movq    %r8, 64(%rsp)
        movq    %r9, 72(%rsp)
        testb   %al, %al
        je      .L2
        movaps  %xmm0, 80(%rsp)
        movaps  %xmm1, 96(%rsp)
        movaps  %xmm2, 112(%rsp)
        movaps  %xmm3, 128(%rsp)
        movaps  %xmm4, 144(%rsp)
        movaps  %xmm5, 160(%rsp)
        movaps  %xmm6, 176(%rsp)
        movaps  %xmm7, 192(%rsp)
.L2:
        movq    %fs:40, %rax
        movq    %rax, 24(%rsp)
        xorl    %eax, %eax
        movl    $8, (%rsp)
        movl    $48, 4(%rsp)
        leaq    224(%rsp), %rax
        movq    %rax, 8(%rsp)
        leaq    32(%rsp), %rax
        movq    %rax, 16(%rsp)
        movq    %rsp, %rcx
        movq    %rdi, %rdx
        movl    $1, %esi
        movq    stdout(%rip), %rdi
        movl    $0, %eax
        call    __fprintf_chk@PLT
...

Навіщо весь цей адъ і сектор Газа? Чому variadic частина не кладеться на стек? Те ж саме для Linux/x86-32 просте і красиве:

my_printf:
        subl    $12, %esp
        leal    20(%esp), %eax
        subl    $4, %esp
        pushl   %eax
        pushl   24(%esp)
        pushl   stdout
        call    fprintf
        addl    $28, %esp
        ret

А проблема в тому, що C вимагає, щоб при відсутности декларації виклик все одно оброблявся коректно, тому весь хвіст variadic параметрів повинен вкладатись точно так же якби був точно визначений в прототипі — у даному випадку (x86-64/SysV) перший в rdi, другий в rsi, і так далі.

А якщо б взагалі було заборонено викликати функції без прототипу і той прототип мали б декларувати — як зробили в C++ і як Apple робить в своєму діалекті C — то ваш приклад би не зкомпілювався, а мій не мав би для va_start весь цей непотріб.

(Цікаво з MS ABI. У них, наскільки я бачу, variadic частина завжди на стеку, але вони не забороняють компіляцію без декларації. Хм...)

Я це знаю, у мене більше питання до формулювання. Для мене «вимагає» одначає більше, що інакше робити не можна. Тому

Мова вимагає предоголошення функцій: тобто, нормально мав бути прототип: `int f2(int);` перед тілом f1().

Ми можемо сказати «рекомендує», можна сказати «вважається застарілим», але... якщо код компілюється, то він як-би відповідає стандарту.

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

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

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

Так, можливо я поплутав... Просто у 90-ті пан був студентом, мав час та натхнення, по фану писав компілятор асемблеру, і там чомусь виникала проблема в додатковому проході... Але деталі вже не пам’ятаю.

Але, все ж таки ІМХО, архітектурне рішення з хідерами (та прототипом за замовчуванням) було не дуже вдалим, бо збільшило час компіляції та збільшила кількість помилок.

Ми можемо сказати «рекомендує»

Хай так, згоден.

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

У асемблера є декілька місць на таке. Найпростіший умовний чи безумовний перехід на мітку в коді далі: `je moo` ...якийсь код... `moo:` - і все, не знаючи де це moo буде, не можна скомпілювати зміщення — ось вам вже два проходи. Більш складне: je/jb/etc. компілювати в коротку чи довгу (>=i386, ±32kB/2GB) форму. (Більш того, довга форма в одному місці може викликати подовження коду і вимогу довгої форми в іншому місці! тому треба компілювати так, щоб воно зійшлось дуже швидко, без осцілляцій.) Посилання на змінну, що визначена нижче — що писати в обʼєктний файл, коли треба зміщення. І все таке.
Але це не рівень C, це пізніше. У Паскаля є та ж сама проблема при генерації вихідного машинного коду, це тільки на рівні його вхідного коду можна казати про один прохід.

Але, все ж таки ІМХО, архітектурне рішення з хідерами (та прототипом за замовчуванням) було не дуже вдалим, бо збільшило час компіляції та збільшила кількість помилок.

TurboPascal робився під одну платформу. Ті компільовані модулі не придатні вже для 32-бітного x86, там int ширший. А в початковому Pascal модулів не було, це віддано реалізаціям.

Для C є в багатьох реалізаціях предкомпільовані хедери. Вони справді прискорюють компіляцію.

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

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

Ну тому що ми скоріше мислимо також у процедурному стилі.

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

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

Більшості (в т.ч. і мені) на ньому мислити важче.

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

А далі починаються ефекти, коли, наприклад, вже хтось лякається того, що в foo(bar(), baz()) невідомо в C/C++, в якому порядку bar() і baz() виконаються, і вони вимагають якусь Java, де цей порядок встановлено правилами мови. Їм некомфортно мати недовизначеність навіть в таких малих питаннях.

Бачив колись проект де частина коду написана в процедурному стилі, а частина — в функціональному. Результат був, м’яко кажучи, так собі — ця мішанина тільки принесла додаткових проблем.

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

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

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

"

артефактом конкретної реалізації

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

Мені здається, що Валентин каже виключно про те, що у виразі накшталт let x = f(g(h()), m(), n())  порядок обчислення g() , m() , n() залежитиме від конкретної реалізації компілятора (чи опцій компіляції тощо), але безумовно, що h() буде обчислено до g()

Саме так. g напряму залежить від результату h, там порядок дій буде однозначний. Між g, m, n порядок не визначений. Функціональний підхід понукає робити так, щоб не просто він був не визначений зарані — а щоб він не впливав взагалі на результат. (Для цього і рекомендують чисті функції, так компілятору легше визначитись.)

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

elixir
«взяти щітку»
|> «видусити пасту»
|> «почистити зуби»

haskell
«почистити зуби» $ «видусити пасту» $ «взяти щітку»

Ви вже показуєте результат часткового розкладання в послідовність дій.
Це вже потім (і показує часткове недорозуміння).
Насправді на верхньому рівні це реалізувати в стилі задач і результатів:

(синтаксис умовний)

забезпечити_чисті_зуби(з) = {
забезпечити_чисті_зуби_з_щіткою(з)
або забезпечити_чисті_зуби_з_гумкою(з);
}

// А тут вже я сказав це як послідовність. Це не зовсім коректно, але припустимо для простоти. Але мені облом розкладати.
забезпечити_чисті_зуби_з_щіткою(з) = { почистити_зуби_з_щіткою(з) і_потім прополоскати_рота(з); }

почистити_зуби_з_щіткою(з) = {
нехай щ := щітка_з_пастою();
застосувати_чистку(з, щ);
}

щітка_з_пастою() :== {
нехай щ := взяти_свою_придатну_щітку();
нехай п := взяти_відповідну_пасту();
// NB щ і п можуть бути забезпечені в будь-якому порядку чи зовсім паралельно!
нанести_пасту(щ, п);
результат = щ;
}

Ну і так далі.

При чому зверніть увагу. Ми частково як би задаємо порядок дій через звʼязок даних (виглядає як змінні, але в ФП сам термін «змінні» має інший смисл або не використовується — часто просто кажуть «іменовані значення»).
Далі, незалежність значень, як щ (щітка) і п (паста), дає можливість паралельно їх забезпечувати. Компілятор сам вирішить, як це краще зробити (в межах його «розуміння»).
Далі, кожне «забезпечити» може включати в себе навіть «сходити в магазин і купити» чи «виготовити самому».) (Згадуємо вже кулінарні рецепти. «Візьміть дві ложки тушкованої цибулі» — скільки за цим криється, може, її ще виростити треба?)

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

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

Саме так — я просто намагався показати, що навіть у «недетермінованих» мовах можна вистроїти залежності між функціями, які будуть виконуватись лінійно.
Хоча, само собою, Elixir не є недетермінованою мовою, але я навів його через наявніть оператора |>

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

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

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

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

можна ж жувати жуйку не засовуючи її до рота і не дістаючи із шафки...

Здається, ви вже глузуєте. Ну це добре, що зрозуміли головну ідею :)

Є гарний MOOC на якому вчать SML www.coursera.org/...​arn/programming-languages
Мені дуже було корисно коли тільки починав

Стаття класна!
Є відео від Scott Wlaschin який розказує про майже теж саме, для тих хто не читає
youtu.be/...​GZFuo?si=P0Q4nF2jfbH2jhXa

У нього ще книжка є непогана з тією ж назвою

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

Мова зазвичай визначає технічний стек. Візьмем скажімо С/С++, крім безпосередньо мов програмування — треба знати принаймні три компілятори і їх особливості, ститчні бібіліотки , динамічні і т.д., IDE та їх налаштування, системи збірки як то GnuMake та Cmake і т.д. Тобто як це встановлювати, налаштовувати і писати відповідний софт під різні бізнес домени. Бо скажімо між Embedded і Unreal Engine чи Goodot — величезна різниця, де на вивчення стеку, на рівень мідла піде від року до півтора. Усі технології, а також бізнес домени — що більш важливо, мають свій суттєвий поріг входу.
Те саме : Java/Spring, PHP/Zend/Wordpress/Drupal і т.д., C#/.NET Core, Python/Django, Python Tensorflow/PyTorch, Scala/Apache Spark, TypeScript/Node.JS/Next.JS, React/Redux, Angular, Vue і т.д. і т.п.
В більшості випадків бізнес не може взяти от просто так спеціаліста з вулиці чи з бенчу, щоб він одразу почав видавати продукцію — бо існує Закон Брукса. Навіть при занні техстеку, ще необзідно «знати проект», тоюбто спіцифіку бізнесу і розуміння бізнес вимог разом із знанням кодової бази, архітектури системи, робочих процессів та команди. Власне тому і дуже популярні в ІТ бізнесу тех стеки, де те цей поріг входу є щонаменшим, так елементарно менше собівартість. Крім того через відносно низький поріг входу, стрімко росте пропозиція — відповідно і за Адамом Смітом і Давидом Рикардо падає ціна. Власне через це JavaScript та діалекти в топі в Україні, у техстеку відносно дуже не великий поріг входу. По усьому світі на першому місці Python.

Вибираю між найголовнішими проблемами: вчити Хаскель або кричати щоб Oracle відмовилась від trademark на Javascript)

або кричати щоб Oracle відмовилась від trademark на Javascript)

Краще перейменувати Javascript, щоб HRи не плутали більш з Java. Така назва з самого початку була некоректним трюком на збігу. Так що тут я за Oracle :)

Oracle відмовилась від trademark на Javascript

Ага счаз, вони ще за Java з Google хочуть бабулес, власне процент від продажів Android. А з JavaScipt ще буде бій, доки не роздуплять як роширити WebASM на доступ до DOM. Тоді TypeScript можна буде компілювати безпосередньо у WebASM минуючи JavaScript. Тоді ринок дуже швидко зміниться дуже сильно.

Ого, непогано друже, непогано) Я наче прочитав сам себе)

Я думаю ми можемо нарешті кодити в REPL почати, тобто в IDE + REPL runtime javascript)

Але ми хочемо постійно донизу, у статіку та компіляцію)

млін, прочитав PERL, подумав та ну нафік ))

Та да заррраз...
усі фронтенднри побігли вебАСМ вчити, заміть js/css/ та іншої хрені

WebASM це ByteCode/IL Сode, який вже і просто зараз вміє виконувати віртуальна машина JavaScript, як V8 так і Spyder Monkey так і Chakra.
Усі браузери станом на зараз його вже підтримують (а також Node).
Та проблеми з ним такі самі як і наприклад із Java Applet та Macromedia Flesh, нема доступу до DOM на пряму, треба викликати зовнішні функції JavaScript.
Якщо з рештою позбавитись такої вади, впровадженням чіткого API доступу до DOM то гегемонії JavaScript кінець.
Скріпити до браузера в повному об’ємі можна буде писати будь якою мовою програмування яку можна компілювати у WebASM. Вже є компілятори із : C, C++, Rust, Go, Java та TypeScript.
У JavaScript як у трушної скріптової мови програмування, далі вже нема куди є як низка беззаперечних переваг, так і низка суттєвих недоліків, наприклад динамічна типізація. Власне тому вже і роблять надбудови типу TypeScript або Dart

Рекомендую перед сном перечитати увесь цикл про індусівархітектурних астронавтів від Джо Спольскі.

en.wikipedia.org/...​ki/Architecture_astronaut Та зжато це усе описав Джон Кармарк «Класс программістів які тільки хочуть говорити про усі речі лише узагальнено, без деталей реалізації». www.joelonsoftware.com/...​ure-astronauts-scare-you
У самого Сполькі — це триндуни які нічого не роблять, крім якихось безкорисних малюнків, а на виході жодного корисного для бізнесу продукту.
Тобто це не зовсім про те, про що йдеться в темі тут швидше — якраз про деталі реалізації з low-level. А з low level — Ocaml зерелізився в 1996-му, це ровесник : Java, JavaScript, PHP та Python і т.д. — епоху торнадо технологій 90-х років минулого сторічча.
В цілому він не повторив навіть судьбу Object Pascal та Delphi які стрімко набрали величезну популярність, та стухло в нульві. І навіть при цьому в тому же TIOBE Index www.tiobe.com/tiobe-index Delphi тримається на 11 позиції, випереджаючи навіть PHP. Visual Basic в свою чергу на 9-й позиції. Тобто це по суті супорт легасі, тезхнології давно мертві. Nim, Oссaml, Logo, Vala, Racu, Ring, Scheme, Zig і т.п. там з низу в переліку дуже цікавих маргіналів, нижче за 0.1%. При цьому той же Zig дісно дуже цікавий.
А откі зазиви, це завичай теж проста хрінь. У якоїсь компанії є купа коду написанного на чомусь новомодному в 1996-му і тепер ніхто не знає, що робити із цим легасі. Де взяти людей на спорт ? От малюють челенджі і т.п.

Оу дідько) Ви теж знаєте чього мужчіну www.joelonsoftware.com/...​ure-astronauts-scare-you
який продав свій проджект glitch.com ?)

Excel та Stack Overflow — це теж він. Його знають мало не усі, та не усім — що він пише є сенс погоджуватись. Як і будь який субьєкт капіталізму — він каже те, що вигідно саме йому і його бізнес стратегіям в данний конкретний проміжок часу. Коли у нього була мала компанія він писав одне в «Верблюди та пісочниця» де поливав Oracle лайном. Коли створив коппорації — став писати зовсім інше, де фактично сам став тим самим Oracle. Сам він з Microsoft, а знаючи про корпоративну війну — рівня Pepsi та Coca Cola тих часів коли він це писав, то там триндеж.

Я і досі на нього підписаний ))

Общая идея понятна, но у меня к этому пара пессимистичных комментариев.

Первое — что для того, чтобы понять в принципе полезность системы типов, совершенно не обязательно как-то учить Хаскель или что-то ещё более выдающееся... массу неплохих примеров можно получить даже на C++ или Python, за счёт разных ограничений на выполняемые операции с объектами (переменными, значениями, называй как хочешь). Вопрос в мотивации — и уже потом в методах. Повторюсь, что я не говорю, что не надо учить. А то, что реально есть возможность это обойти — и большинство таки будет обходить именно потому, что так легче.

И часто даже готовые примеры есть — см. например злополучный chrono в C++, где есть раздельные типы со своим ratio и между которыми надо вызывать явные конверсии, а в месте их упаковки из какого-нибудь struct timespec или распаковки обратно — там уже твёрдо имеешь дело с конкретным типом.

У меня был совсем недавно случай, когда требовалось что-то в стиле подобного — но на Python и при ограничениях на сложность. Задача была перейти в таймстампах с секунд на миллисекунды. В итоге... я переименовывал переменные, методы (некоторые — сначала во временные длинные названия, а потом обратно!), поля транспортных сообщений, напускал на это всё функтексты и линтеры, и на каждом шагу добивался работоспособности. Потом спаковал это в два коммита — первый в промежуточное состояние с временными именами и сигнатурами, второй — обратно к обычным рабочим. Вполне рабочий метод. И безо всяких... к счастью или к сожалению, уже зависит от точки зрения.

И упрощение до возможностей реальных языков за пределами семейств ML, Haskell, Agda, Idris и прочих — будет происходить постоянно потому, что это в принципе возможно.

Второе — пример с параметризованным JdbcConnection имеет мгновенно видимые недостатки: тут то, что параметризация создаёт _разные_ объекты. А не один в меняющемся состоянии. Если нужно поддерживать доступ и с транзакциями, и без, то получается, что надо держать не одно соединение, а два. А при описанной параметризации — и четыре. В четыре раза больше соединений — а выдержит ли?

И вот тут мне пришли воспоминания о том, что не получилось на практике. А ситуация была такая: есть объект типа «звонок», на который неизбежно навалено чуть более чем всё (это при том, что 100500 особенностей, типа состояния сессии, истории аккаунтинга и пр. были вынесены уже в дружественные объекты). И есть несколько источников такого «звонка» типа «приёмник трансфера», у которых вначале есть своя весьма иная логика. А потом надо в объект устоявшегося звонка (который уже универсальный на всех) передать огромный склад параметров — и поменять ссылки у всех, у кого ссылка на данный объект. И вот тут хочется не параметризовать объект чем-то — а подменять его тип на ходу. А никакая из объектных систем в доступных языках не позволяет это делать законными методами. Есть хаки (как в JS подменять prototype), но они ограничены в реализации.

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

На самом деле я, конечно, неправ, что такую высокую тему «заземляю» на чисто практические применения, да ещё и практически отвергая (поддерживая на словах) использование тех самых средств (языков), о которых тема статьи, а про Хиндли-Милнера так вообще ни слова не сказал. Но куда деваться...

И вот тут хочется не параметризовать объект чем-то — а подменять его тип на ходу. А никакая из объектных систем в доступных языках не позволяет это делать законными методами.

Smalltalk...

Под «доступными» я имел в виду те, на которых мы могли писать это соответственно корпоративным условиям. Smalltalk туда не входил.

По любому, спасибо, запишу в копилку, что он такое позволяет.

В Erlang ещё тоже примерно так же можно.

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

А в статических... Чтобы проверить подмену нужно выполнить код.
Или — «отказаться» от переменных как в ФЯ. Нет ссылок, нет проблем :)

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

TS интересен, у него вроде тьюринг полные возможности по работе с типами.
Код этих типов правда будет, ух, забанят на код ревью, потому что не прочтёт почти никто :)
Но у него и типизация структурная, что упрощает жизнь.
В одном из подкастов по Пайтон услышал что жалеют уже, что в Пайтон вставили номинативную. «Структурную надо было...»

В Erlang ещё тоже примерно так же можно.

Там будет слишком много заката солнца вручную. Если вы не перезапускаете на новом процессе с другими данными, то делать все эти выборы в коде... на gen_fsm нормально не укладывается, а иначе писать самому.

Или — «отказаться» от переменных как в ФЯ. Нет ссылок, нет проблем :)

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

В одном из подкастов по Пайтон услышал что жалеют уже, что в Пайтон вставили номинативную. «Структурную надо было...»

Как по мне, ещё замороченнее было бы. А там уже жутковато.

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

Ну, так и есть — это можно видеть в комментариях прямо тут.

упрощение до возможностей реальных языков за пределами семейств ML, Haskell, Agda, Idris и прочих — будет происходить постоянно потому, что это в принципе возможно.

Угу. С другой стороны — rust, swift, reasonml, typescript постепенно поднимают планку того, что считается «реальным языком» :)

пример с параметризованным JdbcConnection имеет мгновенно видимые недостатки: тут то, что параметризация создаёт _разные_ объекты. А не один в меняющемся состоянии. Если нужно поддерживать доступ и с транзакциями, и без, то получается, что надо держать не одно соединение, а два. А при описанной параметризации — и четыре. В четыре раза больше соединений — а выдержит ли?

Моя мгновенная реакция на это: «не надо два соедниения, и уж точно не четыре». Что наводит меня на мысль, что либо я не понимаю твоего примера, либо ты моего. В моем коде в с phantom type (только что починил ссылки на примеры, btw — никто не не указал, что оно было слегка побито :) буквально в конце есть пример, когда на одном соединении делаются три транзакции и три операции без явных транзакций между ними. Где там нужно два или четыре соединения?

никто не не указал, что оно было слегка побито

А, может, потому и не понял.

Спасибо, с таким намёком видно — как тип уточняется «по месту» и неявно.

Это, конечно, очень простой пример. Я не уверен, что оно переносимо в ту обстановку, которую я описывал — там будет сильно сложнее вычислить такие вещи, компилятор может не потянуть. И не представляю себе, возможно ли вообще перепроектировать, чтобы он потянул;\

Но есть о чём думать.

без написання додаткового коду і без будь-яких runtime перевірок.

Цікаво, що «шановні коментарі», повністю проігнорували ваш key-point, і зосередились на стпндартному «Haskell/OCaml/Scala/... не потрібні» )

Навіть у таких мовах, як C++ і Java, ви можете застосовувати подібні патерни для більш строгого кодування обмежень.

У Java буде треба робити ієрархію (RegularConnectionIn / TransactionConnection), це збільшить обсяг коду у рази, і не вирішить усіх проблем з коректністю.

Пост цікавий, але, нажаль, у черговий раз демонтрує (дуже) потужний Blub Paradox в головах, і epic-fail освіти, бо люди просто не розуміють нащо писати type-safe код, і щось робити за O(0) коли і так платять за O(n^2)

і epic-fail освіти, бо люди просто не розуміють нащо писати type-safe код, і щось робити за O(0) коли і так платять за O(n^2)

1) Власне чому епік фейл, а не нові реалії? За рахунок росту обчислювальних спроможностей зараз можливо економити на мінімально необхідній якості розробників. Тому (бізнесу) нераціонально піднімати нижню планку і отримати додаткові ризики.
2) О(1), а не від 0. І чому ви вирішили, що виправлення проблеми, яка знайшлась на етапі компіляції має вже такий розрив з виправленням помилок, що знайшлись на етапі юніт тестів?

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

отримаємо знову помилку в рантаймі.

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

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

Ось приклад коду зі статі:
  type t = {serial:string option; inventory_id:string option; other_data:string}
Розкажить, будь ласка як cюди «попаде null із бази чи запиту з мережі» ?

get_from_db() -> t

в методі робиться, наприклад, sql запит, який повертає по суті dict[str, Any].
окреме питання як ви його скастите до типу t, але зазвичай це робиться або примусовим кастом, або валідацією (останнє рідко бо це окремий вагон логіки в типізованих мовах).

get_from_db() -> t

get_from_db() -> (t, 'error) result
бо, замість t може бути помилка, і її буде зручно покласти до Result

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

Це ж до вас було питання, як ви у t null запхаєте.
Але беріть до уваги, що кастів у Ocaml нема, null також нема.

І повертаючись до початку:

і отримаємо знову помилку в рантаймі.

як саме, де, і яку помилку ви отримаєте у рантаймі ?

Розвинена система типів дозволяє вам «кодувати» весь стан системи саме типами, і повністью уникнути «несподіванок» у рантаймі.

добре, розкажіть як окамл дістає (перевірено) типізовані дані з БД?

Розвинена система типів дозволяє вам «кодувати» весь стан системи саме типами

Хм.. а навіщо тоді запускати саму програму? Що тоді робить код, ми все ж описали типами. Навіщо — рантайм взагалі?

А тому що в нас є — потоік подій, які призводять до зміни стану.

І от у нас у коді
Додати n елементів у масив.
як типами перевірити що вже не влізе? Ну щоб на етапі статичної перевірки, а не у рантаймі?

От заради «додавання у масив» і пишеться код. Хоч на пхп, хоч на хаскель.

Іншими словами, проблема глобально математична:
Як без запуску рантайма встановити що оця конфігурація клітин в грі Життя залишить на полі бодай одну клітину, принаймні через 1000 ходів

як типами перевірити що вже не влізе? Ну щоб на етапі статичної перевірки, а не у рантаймі?

Ключові слова dependent types та value indexed types. Якась, наприклад, Agda чи Idris так зможе. Але тут вже починаються tradeoffs, які призводять до того, що dependent types значно менше поширені.

Я в курсі що агда чи ідріс може про масиви, чи майже може :)

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

Приклад був про необхідність «if» у рантаймі.

Неможливо сформулювати тип конфігурації клітин на дошці Життя які житимуть. Або помруть за 1000 ходів.

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

А в чому проблема? Це звичайний input, над яким ми проводимо обчислення. Це як прочитати вміст файлу.

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

Хм.. а навіщо тоді запускати саму програму? Що тоді робить код, ми все ж описали типами.
От заради «додавання у масив» і пишеться код. Хоч на пхп, хоч на хаскель.

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

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

Ніяк, бо це halting problem, як за визначенням undecidable.

Але, завдяки коректному опису проблеми типами, ми зможемо гарантувати, програма приніймні виконає ці 1000 ходів, а не вилетить по RuntimeException.

програма приніймні виконає ці 1000 ходів,

Якщо пам’яті «в масиві» не вистачить на 500 ходу, то як ви типами можете надати гарантії що вона виконає 1000 ходів?

Ніяк, бо це halting problem, як за визначенням undecidable.
Якщо пам’яті «в масиві» не вистачить на 500 ходу, то як ви типами можете надати гарантії що вона виконає 1000 ходів?

Ну... за допомогою dependency type ми можемо лише надати гарантії, що на виконання 1000 ходів буде створено не більше ніж N екземплярів A, не більше ніж M екземплярів B, ... А з цього порахувати мінімум пам’яті.

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

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

ми можемо лише надати гарантії, що на виконання 1000 ходів буде створено не більше ніж N екземплярів A

Звідки це відомо для гри Життя наприклад?
Яким чином ви отримали оте «буде створено не більш як N»?

От тільки з практичної точки зору зазвичай якщо у нас немає пам’яті

То ми просто перевіряємо це в рантаймі.

Яким чином ви отримали оте «буде створено не більш як N»?

Мови зі залежними типами.

Наприклад,

filter : ∀{ℓ}{A : Set ℓ} → (A → 𝔹) → 𝕃 A → 𝕃 A
filter p [] = []
filter p (x :: xs) = let r = filter p xs in
                     if p x then x :: r else r

length-filter : ∀{ℓ}{A : Set ℓ}(p : A → 𝔹)(l : 𝕃 A) →
                length (filter p l) ≤ length l ≡ tt
length-filter p [] = refl
length-filter p (x :: xs) with p x
length-filter p (x :: xs) | tt = length-filter p xs
length-filter p (x :: xs) | ff =
  ≤-trans{length (filter p xs)} (length-filter p xs) (≤-suc (length xs))

це тривіальний приклад, в якому доводиться, що довжина списку після фільтрації не може збільшуватися. Сігнатура функції length-filter це твердження. Тіло функції це доказ. Доброго ранку, ізоморфізм Карі-Говарда. Таким чином вже зараз можна уявити функцію, яка повертає стати, та доводити відповідні теореми.

Ще інший приклад

module my-braun-tree {ℓ} (A : Set ℓ) (_<A_ : A → A → 𝔹) where

data braun-tree : (n : ℕ) → Set ℓ where
  bt-empty : braun-tree 0
  bt-node : ∀ {n m : ℕ} →
            A → braun-tree n → braun-tree m →
            n ≡ m ∨ n ≡ suc m →
            braun-tree (suc (n + m))

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

Мови зі залежними типами.

Ви не зрозуміли проблеми, а вже понаписали коду :)

Ви не зрозуміли проблеми

Бо ніякої проблеми не існує. Якщо брати мову програмування зі залежними типами, то можна написати програму, яка генерує N поколінь, починаючи зі заданого становища у грі «життя». Це можуть усі мови програмування. Бонусом буде доказ того, що програма працює коректно та не містить багів. Також можна довести оцінку зверху кількості операцій (читай час виконання). Також можна довести оцінку зверху максимальної кількісті створених об’єктів (читай вимоги по пам’яті). Це максимум про що можна мріяти, тому які проблеми?

От тільки з практичної точки зору зазвичай якщо у нас немає пам’яті, то це із серії, що зникло живлення

А ось це вже диверсія — настільки закладатись на памʼять.

Зазвичай. Якщо є обмеження, то зазвичай це невеличка кількість сутностей.

Шаг 1: читаем, что проблема останова неразрешима в общем случае.
Шаг 2: пишем алгоритмы, которым конкретно (в смысле the particular algorithm) можно доказать все нужные свойства, включая конечность выполнения. Это несмотря на.
Шаг 3: убеждаемся, что компьютеры всё ещё работают, а не уходят в вечный цикл на каждое действие пользователя.

Если это и аналогия, то очень точная. Всё описать типами, может, и вредно для скорости написания. А вот защититься от наиболее типовых граблей, чтобы тебе не писало, что через NaN минут к тебе приедет [object Object] — это как раз то, для чего вообще придумали типизацию.

читаем, что проблема останова неразрешима в общем случае.

Так я не о проблеме останова :)

Так я не о проблеме останова :)

Так я и написал, что это аналогия.

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

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

Ось і думай собі що з цим робити...

Fail Fast — усе давно відомо. Валідація на вході методу, відкат транзакції на звершенні з помилкою відображення помилки валідації на інтерфейсі. Це усе давно відомо і проробляється стандартними методами фреймворків. В тому-же Spring Framework наприклад методи дуже просунуті як по валідаціях, так і по транзакціях, записах в базу даних і т.д.

Fail Fast — усе давно відомо.

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

Ось тепер пропоную замислитись, як справлятись з такими проблемами.

Ще один варіант в variant type накшталт Unparsed of bytes, і роби з ним, що хочеш :)

Ну це вже занадто;\

Чому? Особливо якщо є задача нічого ніде не відкидати, не загубити, і мати можливість може спробувати обробити ще раз у майбутньому (скажімо новою версією коду)

Вам приходить рядком (чи нехай вже мапою) json об’єкт юзера на 10 полів, де запросто могли прислати age рядком або birth_date числом, але у форматі YYYYMMDD, а не timestamp. Тут або довіряти інпуту, або як ви зазначили використовувати «вбудовані» способи валідації. Тільки от нюанс: те що в спрінгу (чи скажімо pydantic в пітоні) виглядає як парочка аннотацій, насправді виконує купу коду, який як і будь-яка бібліотека теж може містити помилки в той чи інший час.

Звісно купа коду і т.д. Насправді там декілька методів і усе дуже гнучко (с пів року тому створювали навчальну програму для студентів корпоративних курсів).
Це так зване контрактне програмування , так певним чином оперативність знижується як по CPU так і по пам’яті, це плата за абстракції. При цьому ефективність праці програміста росте на порядки. Якщо профайлингом виявивлено, що ці ділянки коду коштуєють за дорого її завжди можна оптимізувати в ручну. Як відомо, критичними ділянками будь якої програми зазвичай є усього лише 10% коду, так звані вузькі ділянки bottle neck. А передчасна оптимізація відома як корень усіх зол.

Fail fast це класно коли можливо, але тепер уявіть процесор якогось потоку даних, де треба просто ігнорувати невалідне, але при цьому зберігати репорти про причини невалідності. А потім прийдуть продакти і на окремі причини невалідності вкажуть вам як їх треба фолбекати. Але що найгірше — 90% валідацій будуть sanity check-ами, які ніколи не спрацюють бо ніхто ніколи не пришле вік юзера рядком. Але вам заради ваших типів прийдеться потратити тиждень+ часу, бо ж у теорії таке можливо.

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

Додаете ще один варіант до ADT, що буде зберігати невалідне as-is (строкою, чи байтами, чи json чи чим воно ще там прийшло):

type message = 
  | Birthday of Date.t
  | Age of int
  | Volume of float
  | Unknown of string

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

Volume і Birthday прийшли рядками, як ваш тип на це відреагує?

Якось так, наприклад:

type message_type = Birthday | Age | Volume | Unknown

type message = 
  | Birthday of Date.t
  | Age of int
  | Volume of float
  | Unknown of (message_type * string)

let decode_message protocol_buffer =
  let message_type, message_body = get_next_message protocol_buffer in
  match message_type with
  | Birthday -> 
    (match Date.of_string_opt message_body with
    | Some date -> Birthday date
    | None -> Unknown (Birthday, message_body))
  | Age -> 
    (match Int.of_string_opt message_body with
    | Some age -> Age age
    | None -> Unknown (Age, message_body))
  | Volume ->
    (match Float.of_string_opt message_body with
    | Some volume -> Volume volume
    | None -> Unknown (Volume, message_body))
  | Unknown -> Unknown (Unknown, message_body)

Що було валідною датою чи флоатом — їде далі як Britday чи Volume, усе інше перетворюється на Unknown. Усі варіанті покриті. Усі незрозумілі дані помічені типом message, в якому вони приїхали.

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

Власне це я і мав на увазі. Масштабуйте до більшої кількості полів і більшої кількості дата об’єктів і ваш проект на 30% складатиметься із цього замість бізнес-логіки.

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

Якщо дійсно

ваш проект на 30% складатиметься із цього замість бізнес-логіки.

, то у цей момент одноразово пишеться ppx (compile-time code generation extension), який пише цей бойлерплейт за вас, и код буде десь такий:

type message_type = Birthday | Age | Volume | Unknown

type message = 
  | Birthday of Date.t
  | Age of int
  | Volume of float
  | Unknown of (message_type * string)
  [@@derive safe_parser ~of:message_type]

let decode_message protocol_buffer =
  let message_type, message_body = get_next_message protocol_buffer in
  safe_parser message_type message_body

Але 30% — це мабуть поєтичне перебільшення :)

Попросив чатгпт переписати перший приклад, на TS.
Цікаво стало, протестувати чатгпт :)

Ось код, отримаємо помилки компіляції в обох функціях «setInventoryId» та «needsInventoryId»

// Asset_v2: Використання алгебраїчного типу для ID
namespace Asset_v2 {
  // Тип, що описує три допустимі стани ID
  type Id =
    | { type: "Serial"; serial: string }
    | { type: "InventoryId"; inventoryId: string }
    | { type: "SerialAndInventoryId"; serial: string; inventoryId: string }
    | { type: "RFID"; inventoryId: string };

  type Asset = {
    id: Id;
    otherData: string;
  };

  function setInventoryId(asset: Asset, inventoryId: string): Asset {
    let newId: Id;

    switch (asset.id.type) {
      case "Serial":
        newId = { type: "SerialAndInventoryId", serial: asset.id.serial, inventoryId };
        break;
      case "InventoryId":
        newId = { type: "InventoryId", inventoryId };
        break;
      case "SerialAndInventoryId":
        newId = { ...asset.id, inventoryId };
        break;
      default:
        // Використовуємо перевірку на невичерпність
        const _exhaustiveCheck: never = asset.id;
        throw new Error(`Unhandled type: ${_exhaustiveCheck}`);
    }

    return { ...asset, id: newId };
  }

  function needsInventoryId(asset: Asset): boolean {
    switch (asset.id.type) {
      case "InventoryId":
      case "SerialAndInventoryId":
        return false;
      case "Serial":
        return true;
      default:
        // Використовуємо перевірку на невичерпність
        const _exhaustiveCheck: never = asset.id;
        throw new Error(`Unhandled type: ${_exhaustiveCheck}`);
    }
  }
    // Приклад використання
  const asset1: Asset = { id: { type: "Serial", serial: "123" }, otherData: "Example" };
  const updatedAsset = setInventoryId(asset1, "INV-001");
  console.log(needsInventoryId(updatedAsset)); // false
}

Погратись на typescriptlang.org/play

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

Давайте так, переважна частина роботи це звичайнісінький CRUD Backend/Fronted/Mobile усе. Не треба там ані Ocaml ані Clojure, Lisp, Haskel і т.д. Розвинені і широко використовні станом на зараз тех стеки.

Fronted JavaScript та діалекти TypeScript, Darth і т.п. Фреймворки : React/Redux, Angular, Vue а також СSS Bootstrap та Foundation.

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

Загальний internet домен:

  • PHP з фреймворками Zend, Wordpreess, Joomla!, Drupal, Typo 3 і т.д. — найпоширеніші рішення в інтернет, найпоширеніша технологія для більшості не великих інтернет проектів.
  • JavaScipt та діалекти Node.JS з Next.JS — друга за популярністю технологія бекенду яка відібрали велику долю ринку в усіх інших, з багатьох причин. Особливо подобається в Україні, бо бізнес може повернутись у Full Stack коли усі можуть робити як backend так і frontend на Next та React.
Корпоративний сектор : електронна комерція, документ обіг, ERP, СRM, банківський соф і т.д. і т.п.
  • Java (та діалекти Kotlin, Goroovy, Scala) зі Spring Framework та Hibernate (JEE, Jakarta EE, Play, Vaadin, Struts і т.д. це усе вимерло або використовується в межах статистичної помилки).
  • C#/.NET — ASP і т.д.
Інфраструктура, SAS сервіси включно з AI/ML, Dev Ops та claud computing
  • Python/Django
  • Go lang

Mobile — тут усе просто. iOS — Swift, Java/Kotlin — Android. А також універсальні React Native на TypeScipt та Flutter на Darth — діалекти JavaScript.

Автоматичне тестування — Java/Rest Asshured, Selenium, Cucumber і т.п. і т.п. та Python

Звісно є ще скажімо сфери : GameDev, High Perormance, Desktop та Embedded (в Big Tech ще і system programming). Тут беззаперечним лідером є тех стек С/С++ (хоча тут дуже багато чого може ще бути в додаток і майже усе що вище перелічено і ще наприклад LuaScript і т.п.)

Розраховуючи на от такий попит на ринку : Clojure, Haskel, Ocaml, Flutter, R і т.д. це усе мови на погратись бо скучно стало, при чому підозрюю, що і для самих розробників тих мов.

От по цій вкладці найбільш запотребуваною мовою на ринку зараз є JavaScript/TypeScript jobs.dou.ua Fronted 221 та Node.js 193 Потім Python 170 загальний і AI/ML106. Потім PHP, .NET та Java.

Розумний AI каже що найбільш запотребувані тех стеки в світі станом на зараз.
PL: Python, JavaScript, Java, C# та Go lang.
Фреймворки: React, Node.js, Spring Boot, Angular, .NET Core

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

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

Скоріше за все, то ця стаття — це саморефлексія автора на власні кар’єрні досягнення будучи окамл програмером протягом 14 років. Тепер автор прагне оправдати свій досвід намагаючись переконати читачів, що їм потрібен Хінді-Мільнер, який використовують 2.5 землекопи в 3.5 мовах програмування.

В противному випадку, я не можу пояснити отакі пасажі:

Але чому варто вивчати HM, якщо ви не використовуєте OCaml чи Haskell? Тому що ці принципи є універсальними. Розуміння HM допомагає по-іншому мислити про типи та їхнє використання для безпечного моделювання доменної логіки.

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

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

Чесно, зразу пригадав тут іншу писанину про TypeScript, де автор розпедалював, що «потрібно глибоко знати теорію сетів», бо в системі типів TS є щось схоже на юніон-типи, а юніон — це ж операція над множинами. І пофіг, що типи в TS не є множинами.

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

Скоріше за все, то ця стаття — це саморефлексія автора на власні кар’єрні досягнення будучи окамл програмером протягом 14 років. Тепер автор прагне оправдати свій досвід ...

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

Так сміявся, аж до сліз на очах. Щиро дякую панові Богдану.

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

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

А ще я можу поділитись своїми оферами від інших фангів

Робіть курси по behavioral interview.

там немає на що дивитись, так?

То скромний анонім і не вважає себе найшвидшим стрільцем заходу, із найбільшою мушкою на револьвері... ;)

подивись де працював я.

Я дивився — гідні компанії, ви молодець. Проте, якщо ще не поцікавились, Jane Street — це компанія із ліги Citadel, Hudson River, Two Sigma і т.д. Складність відбору до цих компаній в рази вища порівняно навіть з Google, а інші фаанги навіть і поруч не були.

Та і доу декілька років тому брали у мене інтерв’ю про математику

От і спробуйте податися на вакансію в одну з цих компаній. Хороші знання математики вони поважають. Якщо що, від оферу завжди зможете відмовитись ;)
Спеціалісту з вашими роками досвіду вони запропонують і компенсацію вище ніж та ж Meta. Хоча починаючи десь з Google L7 — порівняти компенсації вже складніше, але... проб’єтесь на L7 — з задоволенням про це почитаю статтю.

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

Для спеціаліста, який не мав нагоди попрацювати з потужною системою типів, не знайомий з концепціями make illegal states unrepresentable, exhaustive matching і тому подібними, автор статті дає чудову рекомендацію і наводить корисні приклади. Знайомство з такими речами точно дасть поштовх розвитку програміста.

коли HM являється часним випадком обмеженої системи типізованого ламбда калкулусу (System Fω), що уже немає нічого спільного з універсальністю.

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

А під універсальністю мається на увазі, що знання отримані під час вивчення умовного OCaml чи Haskell, стануть в нагоді для написання кращого коду на Python, чи Go, чи Java.

Досить прості та умісні твердження, я би сказав. Чому вас натомість понесло в глибини філософії лямбда числення — тільки вам самому відомо.

Чому тоді не почати зразу з математики, з теорії типів, наприклад?

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

Знайомство з такими речами точно дасть поштовх розвитку програміста.

Яку ? Що такого я зможу напрограмувати, щоби виграти таким чином технологічну конкуренцію або створити новий ринок ? Я припарився писати що в капіталізмі єдина ключова цінність — капітал. Хто в нас відоміші програмісти : Ілон Маск (щось писав на PHP та С), Біл Гейтц Basic (кажуть улюблена мова в нього С++ досі щось там робить). Ще наприклад Джон Карлмарк, Лінус Торвальдс — С/С++. За великим рахунком люди які давно керують і займаються технологічним бізнесом.

Я припарився писати що в капіталізмі єдина ключова цінність — капітал.

ОК, хочете про капітал — давайте. Скільки цифр налічує сума вашої річної компенсації вираженої в USD?
Ризикну припустити, що в співробітників компаній типу Jane Street за 14 років ця кількість біля семи.
Що ще цікавого про капітал скажете?

У програмістів ? З якої стати капіталісту платити найманому працівникові вище ринку. Ocaml в алготрейдінгу це локальний бздик місцевого CTO, якій увірував що яка небудь мова програмування замінює професійне тестування.
Я з таким стикався неоднарозово, тільки там CTO бздикнуло усе переписувати на Goovy. Звісно лише збільшення часу з того отримав поки команда засвоїла новий синтаксис.
Для тестування і зменшення багів довелось брати на роботу QA, та включати тестування естімейт.

У програмістів ?

Так. Ви ж можете легко в цьому переконатися. Зайдіть на levels.fyi та подивіться. Числа там відповідають реальності, це вже не однією людиною підтверджено.

Це не вище ринку, до речі. Це такий ринок в Штатах і в меншій мірі ще UK для найкращих. Знову ж таки, переконатися в моїх словах допоможе levels.fyi

Ocaml в алготрейдінгу це локальний бздик місцевого CTO

Так, може бути, легко.

З іншого боку, можливо, керівництво компанії зробило спостереження, що найкращі програмісти, яких би вони хотіли найняти і платити їм винагороду близьку до семизначної, чомусь дуже полюбляють оте ФП[1]. То і вирішили розробляти на OCaml — чисто щоб привабити потрібний тип розробника. Хоча строго кажучи — так, ту ж саму систему можна було би побудувати і на C++, скажімо. Могло і так бути, як вважаєте?

[1]: Я що від ФП, що від ринку Штатів, що від найкращих — дуже далеко, якщо що. Не подумайте, що я так сам себе нахвалюю.

Так. Ви ж можете легко в цьому переконатися. Зайдіть на levels.fyi та подивіться. Числа там відповідають реальності, це вже не однією людиною підтверджено.

Це постійне посилання у HR-ів з бодішопів. Зазвичай таким зазиваловом на Ocaml чи Сobol — тобто не ринкову херню, з якої тепер не ясно що робити бо нема кому супортити зазвичай так і зазивають. Бо особисто я, коли матиму десь великий дохід буду мовчати до останньої можливості. На ділі сапортяга з узкою нішою, проходили це усе вже. Уся інша індустрія йде за технологіями з Big Tech.

Добре, ви мене зловили.

Дійсно, компенсацій більше за 5К не існує. Та і 5К насправді платять тільки в Києві, і то — одному програмісту, найкращому в Україні. Юра Буряк про це давно говорить.

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

Ок, тож десь в 2009 у «місцевого СТО» стався «бздик», і він (може навіть сілкомиць?) перевів усіх і усе на ocaml, і ... що сталося далі, на вашу думку? :)

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

Як на вашу думку розгортались події далі? :)

В моїх випадках трое таких от CTO (або рівень по нижче та враховуючи рівень корпорації там підлеглих значно більше ніж різні малі компанії можуть собі уявити) — «вирішили рухутись далі». Потім скорочення замовлення, рампдауни і т.д. — що власне для команди обнулення карьєрних зусиль, та часто пошук нової роботи. На реплатформінг на загальні технології вищі керівники вибирили команди які знались саме на цих технологіях (у інших вендорах), щоби мінімізувати власні ризики. Насправді отимували Bus Factor та через час намагались повернути втрачені команди, що часто не вдавалось бо нас привчили маркетингово, що знання якоїсь технології важить дорожче за зання бізнесу і розуміння бізнес вимог, щоби не ходити до аналіста по 5 разів на день і не преробляти невірно реалізовний функціонал. Тому так, різні позаринкові технології та вузька спеціалізація, це часто шлях в нікуди та червоні прапорці як для команди так і для бізнесу. Чим більше у вас в проекті якоїсь екзотики, тим більше в цьому проекті бізнес ризиків. А для програміста — це часто карьєрні ризики і недолік по фінансам відповідно до конкурентів. Чи варто лізти в OCaml і т.п. або писати код, який «не такий» як в туторіалах — ні не варто. Це часто або погані заробітки, або конфілкт на код ревью і потім проблеми дісциплінарного характеру. Лізти варто в модне стільне, молодежне — там де є гроші які вам згодні за роботу заплатити.

Гаразд, зрозумів за ваших СТО, але ж ви розповідали за CTO Jane Street. Ось в мене питання про нього і подальшу долю його рішення :)

Парадокс, але успіх/невдачі компанії може ніяк не залежати від CTO/мови програмування. І те що умовний Goldman Sach десь юзає Java, а не С#, навряд чи якось відбивається на його доходах.))
А екзотика типу OCaml імовірно просто додає трішки складності з наймом та й усе.

А екзотика типу OCaml імовірно просто додає трішки складності з наймом та й усе.

Від кандидатів не вимагають досвіду з OCaml (або функціональним програмуванням взагалі) :)

Як на вашу думку розгортались події далі? :)

Чи є десь статті чи доповіді про те чому Jane Street використовують Окамл і які переваги/недоліки зловили?
Чув певні історії, але це рівень чуток з 10 раундами переповідань. І чомусь думав, що Окамл там був чи не з заснування контори.

Є jane street blog, там є blog.janestreet.com/why-ocaml (воно з цього плейлиста, там є ще: www.youtube.com/watch?v=-J8YyfrSwTk, і взагалі www.youtube.com/@janestreet/videos)

ane Street — це компанія із ліги Citadel, Hudson River, Two Sigma і т.д.

Я добре знаю за ці компанії. Це буквально 3-тя ліга (в хорошому сенсі) після FAANG та FAANG-like компаній серед кращих компаній для пошуку роботи.

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

Це напів-правда. Там вимоги значно більші, авжеж. Хадсон Рівер до цих пір вимагають диплом в СіЕс або математиці. Ну, і досить різний підхід до інтерв’ю. В гуглі дрочка на задачки по ДП, а в Джейн Стріт — математика на першому місці.

Спеціалісту з вашими роками досвіду вони запропонують і компенсацію вище ніж та ж Meta.

Насправді, це не так. В тому ж Хадсон Рівер грейди не такі гранульовані, як в інших компаніях. І якщо ми візьмемо того-ж сеньйора, то в середньому фангі компенсація буде більша (пруф тут: www.levels.fyi/...​e&track=Software Engineer). Але є дуууууже велика різниця між SWE в гуглі чи мета і квантом в HRT чи JS. З квантів в трейдинг фірмах деруть так, що у тебе взагалі не буде ніякого WLB. Це не Мета, де ти пів дня провітрюєшся в гарненькому офісі. Але інтерном краще йти в трейдинг фірму так як там дуже добре платять нюградам. Тут не спорю.

проб’єтесь на L7 — з задоволенням про це почитаю статтю.

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

Я погоджуюсь зі сказаним до наступного моменту:

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

Так, одному із сотні інженерів ця тема може дійсно бути цікавою. Мені самому дуже цікава теорія типів і мов програмування. Чи дає це прям якийсь буст в роботі і покращує розуміння мов програмування. Я б сказав, що в 99% випадків — ні. Для середнього пхп/го/пайтон/сі/сі-шарп і тд програміста — це взагалі ефемерні матерії, які в цих мовах навіть не застосуєш.

Щодо підходу з вибором HM перед усим іншим, то вважаю, що такий підхід фундаментально хибний.

Я 2 роки тому купив собі 2 томи видання Відкритого суспільства і його ворогів Карла Поппера. Один із найвизначніший філософських творів 20-го століття. З філософією був знайомий тільки з сучасними редакціями робіт стоїків, кініків та епікурійців. Не довго думаючи почав читати і на превеликий жаль для себе не зміг прочитати більше сотні сторінок, бо стало ясно, що у мене немає елементарної бази. Бо робота Поппера — це критика робіт Платона, Гегеля та Маркса. Бо без розуміння «ідеальної держави» Платона та діалектики Гегеля моє читання «Відкритого суспільства» буде чисто для галочки.

Так от і тут. Людині, яка не має бази в теорії типів, закопуватися в HM буде настільки ж корисно, як мені 2 роки тому в роботи Карла Поппера.

Це буквально 3-тя ліга (в хорошому сенсі) після FAANG та FAANG-like компаній серед кращих компаній для пошуку роботи.

Це спірне твердження. Деякі люди дотримуються думки, що трейдингові компанії — це якраз перша ліга, потім — FAANG і FAANG-like. Але що таке найкраща компанія для роботи — це питання комплексне і суб’єктивне, звичайно.

Це напів-правда. Там вимоги значно більші, авжеж

Ну а чому ж напівправда, якщо вимоги дійсно значно вищі?

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

Насправді, це не так. В тому ж Хадсон Рівер грейди не такі гранульовані, як в інших компаніях. І якщо ми візьмемо того-ж сеньйора, то в середньому фангі компенсація буде більша

Так, ви праві. Перевірив для US Google, FB і JS на levels. JS платить відчутно більше на нижчих рівнях, для Google L6 і Meta E5 оплата вже приблизно однакова.

Це звичайні SWE, наскільки розумію. Саме по квант SWE не знаходжу на levels статистики.

Це не Мета, де ти пів дня провітрюєшся в гарненькому офісі.

Я особисто би, напевно, ритму трейдинг фірми не витримав. Хоча, читаю, що в JS з WLB відносно непогано. А за моєю інфою з перших рук, в Meta останні пару років не сильно попрохолоджуєшся — 60-годинний робочий тиждень нікого взагалі не дивує, кожні півроку після перформенс рев’ю на PIP йдуть 15(?) відсотків.

Чи дає це прям якийсь буст в роботі і покращує розуміння мов програмування. Я б сказав, що в 99% випадків — ні.

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

Разом з тим, я би не був такий категоричний. Дійсно, безпосереднього бусту в 99% випадків не спостерігається, але, мені здається, закладається певний фундамент, тренується спосіб мислення, які з часом за певного вдалого збігу обставин можуть перейти вже у вимірюваний приріст продуктивності, чи оплати, чи ще чогось конкретного.

Так от і тут. Людині, яка не має бази в теорії типів, закопуватися в HM буде настільки ж корисно, як мені 2 роки тому в роботи Карла Поппера.

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

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

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

то в коментах було тільки єхидство та гостроти.

:)

Так от і тут. Людині, яка не має бази в теорії типів, закопуватися в HM буде настільки ж корисно, як мені 2 роки тому в роботи Карла Поппера.

Мови з HM можно просто починати використовувати, без того щоб «закопуватись» в HM.

Автор всього лиш говорить, що ...

А під універсальністю мається на увазі, що ...

Підтверджую, що автор говорить і має на увазі саме це :)

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

переконати читачів, що їм потрібен Хінді-Мільнер, який використовують 2.5 землекопи в 3.5 мовах програмування.
коли HM являється часним випадком обмеженої системи типізованого ламбда калкулусу (System Fω), що уже немає нічого спільного з універсальністю. А як це може покращити мислення про системи виводу типів, серед яких HM це один з багатьох існуючих алгоритмів, — для мене загадка

Тобто з одного боку ви мені закидаєте, що HM використовують 2.5 землекопи, а з іншого — що я не зробив тут переклад Lambda Calculus with Types Barendregt-а, не розповів про лямбда-куб, та System Fω, і використав слово «універсальний» не у строгому математичному сенсі?

Це мені чомусь нагадує xkcd.com/2501 :)

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

Ну ось ви ж знаете про System Fω якимось чином. Ризикну припустити, що воно десь стало на пригоді.

Я пропущу ad hominem

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

Ну ось ви ж знаете про System Fω якимось чином. Ризикну припустити, що воно десь стало на пригоді.

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

Для 99.99% розробників ці знання не мають жодного прикладного значення. Чому? Тому, що Хіндлі-Мільнер не вирішує бізнес-проблем (завжди є виключення, але ми не говоримо тут про один випадок на мільйон). Розвинута система виводу типів не покращує твій time-to-market. Знання абстрактних математичних сутностей, які в результаті будуть начисто вирізані компілятором, не продають твій продукт. І все в цьому дусі. Я жодному разі не говорю, що ентузіасти повинні зараз перестати цікавитися теорією типів. Я тільки говорю, що звичайному мимо-крокодилу це не потрібно.

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

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

Що ж, шкода, що ваш особистий досвід вийшов таким — мій досить інший.

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

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

Ви кажете «Clojure, Haskel, Ocaml, Flutter, R і т.д. це усе мови на погратись», і тут же у вашому переліку Typescript, Scala, Swift, в яких є тайпчекер і з ним можна багато чого цікавого робити.

То може «на погратись» — воно не так далеко від мейнстріму, як вам здається?

Справа не в цьому, яка технологія і мова програмування буде виведена на ринок відповідно на неї буде попит, та виникне пропозиція — тобто програмісти почнуть її вивчати, це взагалі не питання теорії, це чистий технологічний бізнес з маркетингом і т.д Якщо ви подивитесь перелічені то популярність технологій для рішень тих чи інших типів бізнесу, це направлена комерційна діяльність конкретних Big Tech. Скажімо мова програмування D теж дуже цікава з теоретичної точки зору, але маркетингових можливостей Digital Mars боло не достатньо щоби вивести технологію на ринок. При тому Apple легко вивели на ринок Swift, Microsoft — C# та TypeScript. Google — Node.JS та в партнерстві із JetBrains — Kotlin.
Розробка засобів розробки, під те що є частиною бізнесів Big Tech само по собі — бізнес. І якщо засоби розробки навіть надають безкоштовно, бо це маркетингова стратегія головної компанії для якої треба щоби під платформу яку вона продає скажімо PC/MS DOS, Windows, Android і т.д. по факту підрядники створювали якомога більше програмного забезпечення.
Тому питання що треба вчити — те що виводять на ринки Big Tech та роблять ці технології модними. Якщо завтра буде маркетингова компанія скажімо Microsoft або Google — з Ocaml, так це означає що негайно треба на це подивитись і подивитись чи є попит на у кінцевих споживачів.
Якщо ви самі виводите на ринок Ocaml, то це технологія яка по суті низконішева — їх вже багато років, це умовний ровесник як Pyhton так і Java та JavaScript, але суттєву долю ринку вона так і не опанувала за весь цей час і десь на дні різних TIOBE index і т.д. Не дивлячисть на усі «плюшки» того самого контрактного програмування із аспектами як наприклад у Java Spring Framework воно просто не надає, тобто Ocaml і близко так не розвивалася як Java або Python ніхто не вбухував мільярди долларів і робочих годин на розвиток цієї технології. Тобто на ринку нема попиту, відповідно за економічними законами — ціна теж буде ніяка. А «розвиток за ради розвитку» то таке собі, нас оцінюють по результатах — а в бізнесі він вимірюється заробленими грошима.

Сподіваюсь, що це було достатньо близько до реального життя

Так, а то «числа фібоначі» дуже далеко від життя :)

Теперь у нас є зоопарк: частина assets

Так, історія типова.
І вирішується створенням гетера, в якому і буде виданий якийсь з заповнених ID
і — валідаціями.
Звісно це все — у рантаймі

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

Наприклад, можна додати до Jdbc.connection якійсь boolean, який вказує, чи ми зараз в транзакціі, чи ні

А з цим уже цікавіше.
Дуже залежить від ORM яка використовується.
І найкраща практика — створення не змінної, а інтерфейсів навколо (Jdbc.connection)
Один з яких для роботи з транзакціями
Другій — без.
Щоб просто неможливо було викликати метод який потребує транзакції, бо його просто немає у інтерфейса для роботи поза транзакціями.

Таким самим чином ми можемо розрізняти файли, відкриті на запис та на читання

Аналогічно.

Причому я такі інтерфейси, бувало не раз, створював просто щоб автокпомліт добре працював, а не для «безпеки».

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

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

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

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

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

const userSchema = z.object({
  name: z.string(),
  age: z.number(),
  email: z.string().email()
});
type User = z.infer<typeof userSchema>; // { name: string, age: number, email: string }

Звісно, сам TS чимало ідей взяв з ФЯ.
Інші мейнстрім мови не мають таких можливостей по роботі з типами

І найкраща практика — створення не змінної, а інтерфейсів навколо (Jdbc.connection)
Один з яких для роботи з транзакціями
Другій — без.
Щоб просто неможливо було викликати метод який потребує транзакції, бо його просто немає у інтерфейса для роботи поза транзакціями.

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

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

Плюс ці інтерфейси — це бойлерплейт код, який треба буде писати самому, так? ;)

Це начебто підкреслюює мій point про те, що система типів зробить теж саме, і навіть краще, і задарма (без boilerplate коду)

Як за допомогою цих двох інтерфейсів написати report 2 із мого прикладу

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

Плюс ці інтерфейси — це бойлерплейт код,

А муть та заумь з створенням типів — ні?

який треба буде писати самому, так? ;)

а ваши приклади — не ви писали, а воно саме написалось?

і задарма

Це точно неправда.
Нічого не буває задарма.

І якби можливості ФЯ були задарма, то стаття би називалась:
Чому програмісти мають вивчити мову з динамічною типізацією, наприклад Smalltalk.

А якщо адепти ФЯ вже років як 20 на моєй пам’яті показують переваги, та ще й які «задарма» — а чергі перейти на ФЯ не спостерігається, то щось з ціною цих переваг не те. Воно значить не тільки не задарма, а взагалі — у підсумку вийде дорожче.

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

Припустимо, що це функція із бібліотеки з 100500 користувань, і ми не можемо за них вирішувати, працювати і у транзакції

Я працював і мацав ОРМи різні, і на різних мовах.
І в них частенько або з коробки, або легко додати вказаних властивостей:
Щоб важко було написати небезпечний код.

Але це велика тема і офтоп — бест практики коду для роботи з БД.

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

На це важко щось відповісти, тож мабуть я і не буду :)

Ваш контраргумент

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

я не дуже розумію, що ви тут пишете

А немає значення що ви пишите.
Головна тема — що хто може перевірити статично, а хто що не може.
Ну, і якою ціною.

На це важко щось відповісти, тож мабуть я і не буду :)

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

Так, історія типова.
І вирішується створенням гетера, в якому і буде виданий якийсь з заповнених ID
і — валідаціями.
Звісно це все — у рантаймі

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

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

якщо у сьому іншому коді у нас ніколи ніколи немає поведінки, яка залежить від типу id

у іншому коді звісно завжди можна придумати якусь фігню.

Чи не так?

Так. Любий код може зламатися, якщо десь у іншому коді — фігня є.

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

Це ж від конкретної задачі залежить.
Так само як null для ID може бути істинним значенням, що вказує наприклад що об’єкт новий, ще не збережений. Не дуже рішення, але для «AWS лямбди» ок.

Програмування не математика.
В ньому не існує одного істинного рішення. А є декілька компромісно прийнятних, і купа гівняних :)

але дискутувати про них не буде :)

На OCaml не писав, не можу про нього дискутувати :)

А приклади ваші сподобались. Вони так, з життя.
От і написав як в житті роблю.

Отличная статья, о том, что нужно развивать эмпирические практики написания кода для достижения его качества и компактности («хороший код, подобен реке — не остановить, не взломать» ©), а не учить кучу паттернов (хотя, это производная эмпирического подхода), потом ломать голову, какой применить или ставить кучу tools и co-pilot. вот только написанное в этой статье поймет всего 1-3% разработчиков.

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