Объясните мне, как вы для себя разобрались в моделях типизаций — они же все размыты

в 14:37, , рубрики: .net, C#, F#, javascript, TypeScript, Программирование

Объясните мне, как вы для себя разобрались в моделях типизаций — они же все размыты - 1

Когда я был начинающим, я мог писать простые приложения на C# и C++. Долго игрался с консольными прогами, пощупал десктопные, и в какой-то момент захотел сделать сайт. Меня ждал большой сюрприз — чтобы делать сайты, одного сишарпа мало. Надо ещё знать жс, хтмл, цсс и прочую фронтовую хрень. Я потратил около недели на эти вещи, и понял — не мое. Я мог написать какой то код на джаваскрипт, но он не содержал типов, и я никак не мог взять в толк — как к этому вообще подходить. Это какое-то игрушечное программирование. Ну и забросил к чертям.

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

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

Самих параметров типизаций несколько.

Есть статическая/динамическая типизация. Статическая — это когда типы данных известны на этапе компиляции. Компилятор знает типы переменных, и на их основании проверяет корректность программы. Динамическая — это когда типы известны только на стадии выполнения, и компилятор при сборке не проверяет ничего.

При этом все статически типизированные ЯП, которые я использовал, дают возможности для динамической типизации. Any в TS, Dynamic в сишарпе. В конце концов тот же тип Object — по идее, я запихиваю в него все что угодно, и говорю, что у меня статически типизированный код. Но строго говоря, это не совсем так. Потому что я могу принять Object извне, и покастить его к чему угодно, не делая никаких проверок — это ли не элемент дин типизации?

Я не знаю, делает ли язык более динамическим возможность динтипизировать куски кода. Если VSCode статически чекает мой js код, в котором даже определений типов нет — делает ли это джаваскрипт статически типизированным? А если да, то как тогда это ужать в бинарный параметр?

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

Когда я начинал писать на тайпскрипте, я знал, что у него статическая типизация. Тогда я не понимал, что это значит, и думал что статическая — это как в сишарпе. Я называл её строгой. Я писал свой код на ts, и не понимал — какого хрена происходит. Почему я объявил тип

type Lead = {
  id: string
}

сделал себе функцию, которая с ним работает — и этой функции можно скормить вообще все, у чего есть такое же поле. Я объяснил себе это очень просто — тайпскрипт дерьмовый. В нем плохая типизация. Со мной работали опытные коллеги, и я вывалил на них негодование — пацаны, ну что за дерьмо. Пацаны объяснили мне, что у ts статическая типизация, но не номинативная. Я послушал их, погуглил и узнал, что есть номинативная/структурная типизация.

Номинативная — это когда для нас значимо имя типа, его номинал. А структурная — это когда мы проверяем типы не по именам, а по их свойствам. То-есть, если у меня есть вот такие классы

class A {
 string name;
}

class B {
 string name;
}

То для компилятора (или среды исполнения) они будут разными типами. А при структурной типизации между двумя этими типами нет никакой разницы. Это, как мне кажется, самый сложный момент. Я точно не могу сказать однозначно, какая модель мне подходит больше.

Дело в том, что теоретически номинативная даёт больше гарантий. Но это сложные гарантии. Если я написал функцию, которая работает с классом А из примера, то и с инстансом класса B она отработает корректно — в том смысле, что она не свалится на этапе исполнения с ошибкой. Поэтому, когда я программирую на языке со структурной типизацией, я пишу функцию, и говорю — вот она работает с чем угодно, у чего есть вот такие поля или методы.

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

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

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

Я так до конца и не понял несколько важных моментов. Во первых, вот структурная типизация — она же может проверять типы на полное соответствие и на достаточное. Допустим у меня есть такой код

type A = {
  id: string
}

type B = {
  id: string;
  name: string;
}

function test(a: A) {}

Тайпскрипт разрешит мне передать в test инстанс B — потому что его структура достаточна с точки зрения типа А. Но по идее, я ведь могу захотеть такую модель типизации, которая будет структурной, но при этом не разрешит считать B удовлетворяющим A. Я не знаю, есть ли такая типизация хоть где-то, нужна ли она, и как бы она называлась.

Причем теоретически, я могу заставить тайпскрипт валидировать полное соответствие — как тогда мне называть его модель типизации? Понятия не имею.

Мой друг Паша как то сказал мне, что в тайпскрипте типизация — дерьмо. Я ответил, что в тайпскрипе я могу написать тип, который описывает число меньше ста, и получу компайл тайм гарантию, что число было проверено. А в сишарпе я так сделать не смогу. Паша впечатлился. Но он тоже прав — в куче кейсов типизация в ts — дырявая.

Есть сильная/слабая типизация. Её легко спутать со структурной/номинативной, но это разные вещи. Этот параметр определяет, делает ли среда исполнения неявные касты из одного типа в другой. Я не скажу за все япы, но все, на которых писал код я, иногда делают такие касты. То есть я не знаю ни одного абсолютно сильно типизированного языка программирования, и поэтому, когда мы говорим "слаботипизированный" — мы имеем в виду такой, который делает такие касты до неприличия часто.

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

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

Когда меня спрашивают, сильная ли типизация в C# я говорю да. А когда спрашивают то же самое про тайпскрипт, я говорю нет. Но на деле, оба этих языка содержат имплиситные касты, и большой вопрос, в каком их больше. Я не знаю, какой тут алгоритм — как определить, сильная или строгая типизация у того или иного языка. Я даже не знаю, какая она должна быть, если честно. Думаю, меня вполне устроит, если функция, которая ждет строку, получит число и скастит его к строке, и думаю, меня совсем не устроит, если компилятор скастит к строке например boolean.

Есть ещё один параметр типизации, для которого я не знаю подходящего термина. Допустим у меня есть вот такой тип

type Person = {
  id: string;
  name: string;
}

Есть языки программирования, которые потащат в рантайм метаинформацию этого типа. Например C# позволяет на этапе исполнения посмотреть, какие поля и каких типов есть у такого-то класса. Это важная вещь — так я могу автоматизировать валидацию сторонних данных. А вот в тайпскрипте вся информация о типах есть только на этапе компиляции. Я раньше путал этот параметр с номинативной/структурной, но по факту я могу себе представить структурный яп, который тащит метаинформацию в рантайм.

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

Это навело меня на мысль, что весь вопрос в комбинации этих параметров.

Вот тайпскрипт — статический, слабый, структурный и не тащит типы в рантайм. Такая конфигурация дает много плюсов — мы пишем гибкий код, у нас сохраняется проверка соответствия программы в компилтайме, наши доменные модули легко переносимы куда угодно. И у нас на порядок меньше бойлерплейта по приведению типов друг другу, чем в том же C#.

Более того, команда разработки тайпскрипта по сути работает только над статическим анализом — и это позволяет им сделать его по-настоящему мощным. Во всяких сишарпах, джавах, котлинах и т.д. невозможно сделать на этапе компиляции вещи, которые для тайпскрипта обыденность. Я имею в виду магию с conditional и mapped types. Я никогда не достигну такого уровня проверки корректности на этапе компиляции в C#.

Но есть гигантский минус. При кодировании на тайпскрипте система типов защищает код на тайпскрипте от кода на тайпскрипте — и никак не помогает с внешними данными. Получается, что если я описал тип User, и получил список таких юзеров из JSON, мне придётся руками перечислять все свойства, и делать все проверки. Это объективно — говно.

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

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

Вот так и в тайпскрипте. С помощью хитрых костылей мы можем заставить его вести себя как номинативный ЯП — есть брендированные типы, есть приватные тайпгарды у классов. А ещё мы можем написать ещё больше костылей, и сделать себе инструмент для протаскивания типов в рантайм. Есть крутая либа — runtypes.ts, которая позволяет описать свои типы как объекты, выводить из них обычные тайпскриптовые типы, и при этом сохранять номинативные и рантайм проверки.

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

Что ещё больше размывает границы всех этих типизационных терминов.

Вот F# — номинативно типизированный ЯП. Но. Но. В нем можно написать функцию, которая работает с чем угодно, у чего есть такое-то поле, такого-то типа. Более того, я могу весь свой код писать в таком стиле — это будет структурно типизированный код на номинативно типизированном F#. Как с этим быть?

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

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

Смотрите мой подкаст

Автор: Philipp Ranzhin

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js