Із власного досвіду проведення співбесід можу сказати, що більшість кандидатів мають досить поверхневе розуміння типів даних. Наприклад, мало хто розрізняю статичну (static), явну (explicit) та сувору (strict) типізації, які є ортогональними одна до одної і протиставлені відповідно динамічній (dynamic), неявній (implicit) та слабкій (weak) типізаціям. Наприклад, у Ruby типізація динамічна, неявна і сувора. У JavaScript – динамічна, неявна та слабка, у C – статична, явна, слабка. У Golang – статична, явна, сувора.
Плутанина з термінологією
Варто зазначити, що зовсім чіткої і сталої термінології не існує. Наприклад, дехто вважає, що статична типізація – це коли значення можна перевірити на початку процесу компіляції (так званим “compiler frontend”, який також називають “type checker” або навіть “static code analyzer”, який, щоправда, не завжди належить компілятору) і всі пов’язані з типами помилки будуть видані під час компіляції. Але як під час компіляції можливо перевірити усі значення, що можуть надходити від користувачів, інших сервісів чи прочитані з файлів? Така статична компіляція у переважній більшості корисних додатків буде суміщена з динамічною, оскільки багато помилок, пов’язаних з неправильним типом даних будуть виявлятися під час роботи застосунку.
Інші називають статичною типізацією таку типізацію, коли змінна посилається на ділянку пам’яті конкретного розміру. Я особисто підтримую саме таку позицію. Натомість, динамічна типізація – це коли змінна посилається на мінімальну структуру фіксованого розміру у пам’яті, яка зазвичай містить тип значення, його розмір та саме значення, якщо воно досить мале (число, коротка строка) чи показчик на значення, що знаходиться у іншій ділянці пам’яті, оскільки займає більше місця, аніж показчик на ділянку пам’яті.
Фанатизм стосовно суворої типізації
Інша плутанина із типами стосується фанатизму щодо суворої типізації. Багато людей вважають статичну типізацію панацеєю, що має ліквідувати ледве не усі помилки у коді. Звісно це не так!
Я вважаю Ruby однією з найбільш ефективних мов програмування для розробників, але її реалізації дуже не ефективними для машин за рахунок того, що це переважно інтерпретатори (хоча останні реалізації і з JIT-компіляцією), динамічної типізації та загортання усіх значень у об’єкти (відсутності примітивів).
З Ruby я вирішував завдання розробки в 2-3 раза ефективніше, але з Golang, мої рішення в 10-20 разів ефективніше використовують залізо. Якщо з Ruby я щось можу розробити за 3 місяці і запустити його на 10 серверах, то с Golang я витрачу півроку і запущу його лише на двох (насправді на одному, але другий необхідний про всяк випадок). Якщо 10 серверів будуть коштувати мені $80_000 на п’ять років, то за п’ять років постійної розробки на ще одного розробника я витрачу не менше $300_000. Для цього вигаданого проєкту Ruby виявляється більш ефективним аніж Golang з єдиної точки зору, що дійсно має значенна – бізнесової, але зараз не про це.
Golang – це мова з суворішою типізацією, аніж у Ruby, але це все одно не робить Golang більш вигідним вибором, оскільки більш-менш досвідчений розробник майже не робить таких помилок, що можуть бути відловлені системою типів Golang. Крім того, виправлення таких помилок потребує зовсім незначних витрат часу розробників.
Я деякий час вивчав OCaml та StandardML, і можу сказати, що бавитися з ними цікаво, але ефективність розробника погіршується ще у два рази відносно Golang.
Крім того, якщо Golang – це мова для посередностей, створена, аби Google легко міг винаймати тисячі розробників (а потім звільняти їх), які б писали більш-менш простий, одноманітний, безпечний та ефективний код, то OCaml та взагалі мови ML потребують значно більшого часу на те, аби навчитися з ними щось робити. Код на Golang значно одноманітніше та простіше для розуміння, а от код на OCaml в двох випадково взятих розробників буде значно відрізняєтися і вони будуть ще рік сперечатися, аж доки не випрацюють спільного стилю. При цьому код на Golang на синтетичних тестах буде навіть трохи більш ефективним за код на OCaml, але це вже інша тема.
Працюючи з Ruby, JavaScript та Golang я не зустрічав таких критичних помилок, для яких мені було б доцільно використовувати, наприклад OCaml зі значно більш багатою системою типів. Іншими словами, більш розвинута система типів та суворіша типізація не виправдовували себе у тих застосунках, над якими я працював.
Сліпі точки у суворій типізації
У суворій типізації є сліпі точки, тобто сувора типізація не така вже й сувора. Новий/експериментальний виток розвитку систем типів – це залежні типи, тобто типи, визначення яких мають посилання на значення, а самі типи є т.з. “first class citizen”, тобто можуть використовуватися як звичайні значення. Завдяки залежним типам ми тепер можемо перевіряти не лише те, що значення, що передається до функції – список елементів певного типу, а й те, що цей список має певну довжину, або що другий аргумент функції – це число, яке не більше за перший аргумент.
Це все дуже цікаво, але я не бачив жодного прикладу, коли б така особливість надавала дійсно суттевих переваг. Єдине, що можу придумати – це більш детальна та жива документація коду, але це досить слабка перевага, яка і так вирішується за допомогою тестів та/або BDD специфікацій.
Сам винахід залежних типів демонструє, що навіть такі розвинені системи типів, як ті що є у мовах сімейства ML мають багато сліпих зон та захищають лише від досить банальних/тупих помилок.
Слово про суворість/безпечність Rust
Я нічого не знаю про Rust окрім того, що в нього жахливий синтаксис, швидкість розробки ще гірше за швидкість розробки на OCaml, а проблеми того, що пам’ять погано звільняється набагато простіше вирішуються періодичними перевірками зайнятої пам’яті та перезапусками сервісу, якщо той потребує її забагато.
Висновки
По-перше, усе, що пов’язано із типами – це цікава тема, яку варто вивчати аби розвиватися та краще розумітися на своїй справі.
По-друге, сувора типізація за умови значних додаткових зусиль розробника здатна позбавити код лише досить тривіальних помилок.
По-трете, сувора типізація та memory-safety сильно переоцінені. Їх вартість виміряється у деградації ефективності розробника й майже завжди значно перевищує вигоду від уникнення якихось помилок чи проблем із перевикористанням пам’яті.
По-четверте, необхідно завжди робити економічні розрахунки та шукати вдалого балансу, а не кидатися у крайнощі.
Залишити відповідь