Pull to refresh

Comments 135

  1. Скорость разработки

А можно какой-то конкретный пример?

Мне вот кажется это скорее не вопрос типизации, а проблема (фича) архитектуры самого ЯП.

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

Есть у меня, к примеру, код:

class Person {
  name: string;
  // ...
}

class Book {
  name: string;
  // ...
}

И уже есть код, использующий оба этих класса. А потом - прототипирование же - я понимаю, что Book.name- это не лучший выбор имени, и что лучше это будет Book.title. Авторефакторинг, если он знает о типах, может мне одним нажатием F2 переименовать везде Book.name, но оставить нетронутым Person.name.

Аналогично - если я вдруг понял, что мне нужно иметь отдельно Person.firstName и Person.lastName - я удаляю старое поле, добавляю новое, а затем прохожусь по всем местам, где компилятор говорит мне про ошибку "поле Person.name не найдено", и правлю их.

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

Не думаю, что в случае рефакторинга нейминга property класса будут какие-то проблемы при отсутствии типов.

Как без типов и без ручного просмотра всего кода переименовать Book.name так, чтобы не задеть Person.name? Вот в таком коде, например (специально привел без типов):

function formatRating(bestsellers) {
  return `The top bestseller is: ${bestsellers[0].name}`;
}

function formatFamily(brother, sister) {
  return `${brother.name} + ${sister.name} are brother and sister`;
}

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

Аналогичный пример, где важны именно типы полей, тоже несложно придумать. Например, я хочу поменять Person.name: string на Person.name: { first: string, last: string }. Или Book.title: string на Book.title: { translated: string, original: string }.

абсолютно не понял связи переименования и типизации. Тип - это int, string, float и т.п.

В одном ЯП вы пишете

int a = 1;

в другом

a = 1;

или

let a = 1;

или

var a = 1;

При этом в типизированных ЯП строка

a += 0.1;

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

абсолютно не понял связи переименования и типизации.

Привел пример в комменте выше.

При этом в типизированных ЯП строка
a += 0.1;
сразу перевозбудит компилятор, а в нетипизированном это будет работать без проблем.

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

Или вы жалуетесь на то, что автовывод типов не понял по var a = 1, что эта переменная может быть дробной? Так и я не понял, на самом деле.

Привел пример в комменте выше.

Мне пример ничем не поможет, так как он не относится к предмету разговора.

Я не уверен, что здесь значит "без проблем"

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

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

В типизированном ЯП это ошибка, в нетипизированном - это само собой разумеющееся явление - оно так работает из коробки.

Если же это переменная допускает дробные числа - тогда просто не нужно объявлять ее как целую

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

Так и я не понял, на самом деле

У мня в целом ощущение что вы не понимаете что такое типизация.

Мне пример ничем не поможет, так как он не относится к предмету разговора.

Так ответьте на вопрос: как сделать такое переименование поля автоматически без статических типов?

В типизированном ЯП это ошибка, в нетипизированном - это само собой разумеющееся явление - оно так работает из коробки.

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

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

Это понятие все равно существует на уровне логики вашей программы. Если в переменную записывается буквально "что угодно", то это повод поругаться на это на код-ревью, потому что обычно в конкретной переменной ожидают достаточно определенный набор значений. Вряд ли кто-то будет ожидать 1.5 в переменной count, например. Или юзера в переменной order.

Так ответьте на вопрос: как сделать такое переименование поля автоматически без статических типов?

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

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

Вот вам два примера работающего кода:

1)

10 A=1

20 B=A

25 PRINT(B)

30 B=B/A+0.1

40 PRINT (B)

>1

>1.1

2)

a = 4

print(a)

a = "hello"

print(a)

>4

>hello

В первом случае язык из 80-х, во втором - современный. И где тут какая-то логическая ошибка?

Это понятие все равно существует на уровне логики вашей программы

А мы про это сейчас не говорим.

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

Мы не говорим о качестве кода.

Вряд ли кто-то будет ожидать 1.5 в переменной count, например

Когда я почти 30 лет назад изучал "теорию и технологию программирования", то одна из вещей, которые на мой взгляд меня и сформировали как программиста, это то, что я обязан рассматривать все случае, например вот такие:

bool a = true;

case(a)

true: break;

false: break;

default: break;

А ожидание через именование это вообще капец конечно. Еще скажите что count не имеет права быть ничем кроме int.

Или юзера в переменной order

Это вообще не имет отношения к типизации и разговору. Ну и я как хочу так и именую переменные и нет никого на планете кто мог бы мне навязать свою точку зрения в этом вопросе.

Без понятия, у меня никогда не возникало такой задачи

Вы ни разу не рефакторили код? Или всегда делали это исключительно вручную, даже на больших кодовых базах?

я не представляю как эта задача и статья с этим связаны.

В языках с типами алгоритм автоматического рефакторинга "переименовать поле T.a в T.b" выглядит, грубо говоря, так: найти все выражения "доступ к полю объекта", где объект имеет тип T, а поле - имя a, и заменить в них a на b.

Как это сделать в языках без типов (без вывода этих самых типов самим алгоритмом рефакторинга) - я не представляю.

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

Вот вам два примера работающего кода (...) И где тут какая-то логическая ошибка?

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

А мы про это сейчас не говорим.
Мы не говорим о качестве кода.

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

А ожидание через именование это вообще капец конечно.

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

Еще скажите что count не имеет права быть ничем кроме int.
я обязан рассматривать все случае, например вот такие
я как хочу так и именую переменные и нет никого на планете кто мог бы мне навязать свою точку зрения в этом вопросе.

Я очень рад, что мне не придется читать ваш код.

Вашу точку зрения понял. Свою, думаю, уже объяснил достаточно хорошо. Не уверен, что смогу вас переубедить, поэтому спор дальше продолжать нет смысла :)

Вы ни разу не рефакторили код?

Скорее нет чем да.

Или всегда делали это исключительно вручную, даже на больших кодовых базах?

Я не в курсе что этот процесс можно автоматизировать. Большое это сколько строк кода? У меня это экранов 10 максимум, там сколько 300-400 строк наверное.

В языках с типами алгоритм автоматического рефакторинга "переименовать поле T.a в T.b" выглядит, грубо говоря, так: найти все выражения "доступ к полю объекта", где объект имеет тип T, а поле - имя a, и заменить в них a на b.

Теперь я понимаю о чем вы.

Логическая ошибка - это ошибка именно в построении алгоритма

Тип - это часть ЯП, а не алгоритма. Максимум можно говорить о реализации алгоритма на языке Х, в любом случае если в ЯП нет типов, то в чем тут логическая ошибка?

Я очень рад, что мне не придется читать ваш код

Код, который я пишу для себя и тот который удобен мне - никто и никогда не будет читать, а когда я что-то пишу для других, то я переоформляю так чтобы и школьник прочитал. В любом случае право что и как называть я оставляю за собой.

поэтому спор дальше продолжать нет смысла

Ну я не смог сразу понять то что вы пишете, возможно вам достаточно той информации, но не мне.

Тип - это часть ЯП, а не алгоритма

  1. В алгоритме "найти первые N" чисел фибоначчи аргумент N имеет тип "неотрицательное число" вне зависимости от используемого ЯП. И даже если вы руками будете считать эти числа - у N всегда будет тип "неотрицательное число". Если вы подадите на вход алгоритма переменную типа "строка" или переменную типа "пользователь" - это будет логическая ошибка

  2. Типы вы можете определять сами, это не обязательно только части ЯП. Например, вы можете самостоятельно определить тип "пользователь"

Чувак, ты не прав, с каждым ответом всё больше - посмотри на минусы, они объективны.

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

Нет проблемы провести явное преобразование типов, но в этом и суть - ты его полностью контролируешь. Абстрактный пример с 1+"1" отлично передаёт суть...

Отвратительно называть переменные, участвующие в бизнес логике именами "a" и "b", когда их суть совсем иная. И также отвратительно класть в переменную image изображение, а в некоторых случаях - ошибку... Мы такое можем позволить в строгой типизации, но тогда у нас image будет интерфейсом с описанными реализациями.

Так повелось что динамические программисты не очень любят создавать классы

Я не в курсе что этот процесс можно автоматизировать. Большое это сколько строк кода? У меня это экранов 10 максимум, там сколько 300-400 строк наверное.

Вот потому вы и не понимаете зачем нужна статическая типизация. Пока я писал программы на 300-400 строк максимум — я тоже не понимал.


Сейчас у нас в проекте полмиллиона строк кода, и это только бекенд. К слову, проект ну вот вообще не ощущается как крупный.

Когда я почти 30 лет назад изучал "теорию и технологию программирования", то одна из вещей, которые на мой взгляд меня и сформировали как программиста, это то, что я обязан рассматривать все случае, например вот такие [...]

Вот именно поэтому и нужна статическая типизация — для уменьшения числа возможных случаев. Чтобы не было нужны рассматривать вариант "не true и не false" для булевой переменной.


Вот вам два примера работающего кода [...]

Конкретно в этих примерах логической ошибки нет (скорее всего), и именно потому в языке Rust разрешено создавать несколько переменных с одним именем:


use std::println;

fn main() {
    let a = 4;
    println!("{a}");

    let a = "hello";
    println!("{a}");
}

Вам всё ещё кажется, будто статическая типизация — это так плохо?

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

А тогда вопрос, если я пишу нетипизированную
a=1
а потом
a=a+1

То что мне должно вернуть - число 2 или строку "а+1" или строку "1+1" ?
Как компилятор будет понимать, нужно хранить строку или число integer или число float?
Сколько памяти выделять, как с этим работать?

По-умолчанию число это int, строка же будет заключена в кавычки, для float можно ввести число с дробной частью 1.0, автоопределение типа к ошибкам не приводит, но где надо типы прописываешь всё равно, просто удобство и скорость написания кода.

Во многих языках строки и числа складываются разными операторами, например, в Nim строки соединяются через &, а не +, и есть удобная процедура приведения любого типа к строке $, для своих типов можно свои написать, или оставить универсальную, в итоге чего программировать на этом строго типизированном языке ничуть не сложнее, практически нет возни с преобразованием типов, зато ещё на этапе компиляции выявляется большая часть ошибок.

Вот что будет с числами и строками:

var a = 1 # приводится к var a: int + 1

var b = "2" # это будет как var b: string = "2"

let c = a + parseInt(b) # = 3, сложить a + b не получится

let d = $a & b # = "12", здесь же не получится сделать a & b, так как a не строка, а $a уже строка, по сути это вызов процедуры преобразования типа $(a)

По-умолчанию число это int, строка же будет заключена в кавычки, для float можно ввести число с дробной частью 1.0, автоопределение типа к ошибкам не приводит, но где надо типы прописываешь всё равно, просто удобство и скорость написания кода.

Ваше "по умолчанию" видимо определено тем, что вы знакомы с единственным нетипированным языком программирования.

Например в баш
a=1; a=a+1 будет вообще "a+1"

Шах и мат.

может мне одним нажатием F2

Но ведь это исключительно вопросы к вашему редактору. Мой вот по F2 не делает ничего. Ваш с типами умеет так, но без типов так не умеет. Редактор 3 будет уметь так и с типами и без оных. Получается качество редактора кода определяет ответ на вопрос: "что лучше типы или без типов?".

Магии не бывает. Не сможет никакой продвинутый редактор выполнить это переименование с гарантией корректности не имея информации о типах.


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

Редактор 3 будет уметь так и с типами и без оных.

Вот когда будет уметь - тогда и поговорим. Idea, например, при работе с JavaScript немного умеет, но проверять всё равно надо руками, и, по факту, умеет она лишь постольку, поскольку встроенный в неё тайпчекер в состоянии разметить код без условных any.

Очередное ненужное разъяснение.

Есть ровно один рабочий довод в пользу статической типизации: личный опыт. Статическую типизацию начинаешь любить, когда боль от динамической превышает какой-то порог.

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

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

а я не буду никого защищать ;)

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

хорошо же, когда есть выбор, и понимание сильных и слабых сторон инструментов

У нас есть замшелый легаси-сервис на ноде (а все остальное уже на котлине). Где-то 100-150 килострок. Залазить туда - дураков нет. Разбор любого бага - минимум день с рисованием на бумажке цепочек вызовов и типов. Любое вмешательство в код - это когда полчаса пишешь код и потом до конца дня проверяешь, что ничего не сломалось.

В общем я не знаю, что должно произойти, чтобы я сказал "да, слабая типизация на бэкенде - отличная идея"

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

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

где я просто не понимаю, что принимает данная функция

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

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

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

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

"У нас отличная документация: она вся на Си" (с)

Где описывать типы это вопрос синтаксиса на самом деле. Долгое время в python для этого использовали docstrings и комментарии. Потом добавили отдельную нотацию. Но сам питон туда практически не смотрит. То есть вы можете указывать какие угодно типы и на работоспособность программы это ни как не влияет.

Если по этим docstrings или специальной нотации можно прогнать линтер – это уже весьма неплохо.

Документация — штука неплохая, только вот аргументы за динамическую типизацию, как правило, сводятся к "лень описывать типы".


Не верю я в хорошую документацию от тех кому даже типы описать и то лень.


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

"лень описывать типы"

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

статическая типизация спасает вас от написания кода, который в принципе не работает ;) и сообщает вам об этом прямо на этапе компиляции.

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

А, еще не могу понять, почему адепты динамической типизации дают всякие словесные имена переменным, можно ж v1, v2 - столько лишних букв на наборе можно сэкономить!

Я вот как представлю как рефакторить большой проект на каком-нить Питоне или ЖС, где нужно будет изменить какое-то API или переименовать что-либо.

Малая надежда на IDE и всякие языковые сервера в ней, которые пройдут по всему этому аду и скажут где что сломалось.

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

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

Ну это вы прямо сильно

Тот же js уже приобрел typescript и нынче это почти стандарт

Известная всем "мантра" в данном контексте вообще не к месту.

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

утверждение с потолка.

У меня опыт чуть отличный от опыта товарища выше: часть проекта, написанную на js, перевели на ts. Это был такой праздник! (даже при том, что мне пришлось переводить на ts и свои куски, вычищая из них unknown). До того типовой проблемой было – где-то изменилось число аргументов функции, использующейся на нескольких платформах... и о том, что что-то сломано, я узнаю от тестировщиков (а если совсем не повезло – вообще от юзеров). Как же хорошо, когда вместо этого просто вылезает ошибка сборки!

Можно попробовать постепенно переползти в этом сервисе с js на ts, расставляя типы прямо по ходу работы с тикетами. Я так переводил огромное реактовое приложение с 150K+ строк с flow на typescript через JS. Т.к. не было нормального конвертера flow→typescript, я сначала убрал полностью все типы и потом прямо по ходу работы постепенно переименовывал файлики .js→.tsx и расставлял везде типы. За полгода перелез :)

У нас есть замшелый легаси-сервис на ноде (а все остальное уже на
котлине). Где-то 100-150 килострок. Залазить туда - дураков нет

А типы ли (или их отсутствие) причина тех бед, что вы описываете?
Я много раз натыкался на подобные сервисы, так вот ТОП моих причин подобных проблем включал в себя

  • Архизапутанный ООП

  • Странные (с современной точки зрения) интерфейсы функций и классов

  • Отсутствие автоматических тестов

  • Очень плохое логгирование

  • В дополнение к отсутствию тестов сложность частичного тестирования

  • Любовь автора к каким-то малоизвестным особенностям языка ("о, так можно, это ж круто! Будем вставлять такие конструкции повсюду!")

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

Полностью согласен. Было дело, понадобилось написать что-то на php. После С/С++ это было настоящим мучением. Как можно работать без типов? Хуже может быть только работа без явного объявления переменных (это когда в любом месте кода можно присвоить любому новому имени люое значение, и все будет ОК).

Справедливости ради, нынче в PHP завезли типы (и чем дальше, тем активнее их завозят). И это чёрт возьми очень удобно.

На php не все так ужасно, как на том же python. После kotlin/java думал что на php будет совсем боль из-за типизации, но оказалось, что все не так уж и плохо, даже захотелось в kotlin возможность вернуть из функции два разных типа данных aka string|bool. А возможности phpstorm просто невероятно помогают разобраться в чужом и своем коде, после vscode, который ощущался просто текстовым редактором с подсветкой.

Так в c/c++ тоже слабая типизация, а не сильная. Поэтому это явно не те языки, которые является эталонными на мой взгляд 😇

Ну её хотя бы можно сделать почти сильной при помощи-Werror

А вот остальные случаи, которые используются для избежания копирования данных в памяти (union, приведение указателей или reinterpret_cast) все же необходимость, которой просто нужно пользоваться с умом.

Спасибо, теперь мне не придётся это объяснять! Достаточно будет ссылки, чтобы люди прочувствовали, что их ждёт, когда решат реализовывать бизнес логику заказчика на динамических типах.

Молоток - очень крутая штука, я реально не понимаю людей, которые используют отвертку! Ведь молотком можно и гвозь забивать и шуруп!

Очередная статья из серии "Кто сильнее Акула или Медведь?".

Это все - инструменты. Умение их использовать открывает вам соответсвующие двери и наоборот.

Правильно настроенный линтер на языке со слабой типизацией сводит практически все описанные ошибки на нет. Скорость разработки POCов или каких-либо конверторов данных для разовых операций идет гораздо быстрее на языках со слабой типизацией - один из сильных вариантов их использования.

Тяжелый проект, сторящийся на века и поддерживаемый командой - да, нужно писать с типизацией - на дает соотвествующую выгоду, расплата за это - время написания кода.

Вот и все - но нет, опять появляются остроконечники vs тупоконечники.

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

Оттуда, что не надо описывать дополнительно типы, следить за ними и их использованием.

Кроме того, когда пишешь фии и еще неизвестно что она должна возвращать (POC например) - с этим тоже можно не заморачиваться и дописать позже и тд.

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

Надеюсь понятно изложил.

не надо описывать дополнительно типы

Написание кода, именно набор текста, занимает довольно небольшую долю времени погромиста. Дописывание всевозможных int, double, auto, my_cool_type увеличит суммарные времязатраты на задачу дай бог на процент.

следить за ними и их использованием

Компилятор следит.

и дописать позже

Компилятор выводит.

два рилма в голове держишь

Типизация "настраивается" 1 раз, а дальше оно само. Мне не нужно знать, какой там именно контейнер, мне достаточно знать, что у него есть begin и end (или аналоги). А если уж нужно знать, по проичине производительности, например, то тут слабая типизация никак не поможет, придётся этот второй "рилм" подтягивать.

Написание кода, именно набор текста, занимает довольно небольшую долю времени погромиста.

Кроме просто написания, это нужно еще продумать.

Компилятор следит.

Компилятор следит за верностью использования, а не за бизнес-логикой, "следить" в этом случае - это использовать согласно задаче, а не в плане правильно/неправильно

Типизация "настраивается" 1 раз, а дальше оно само.

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

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

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

И еще раз - мой посыл не в том, что слабая или сильная типизация плохи или хороши, а в том, что они дают разные преимущества.

это нужно еще продумать.

Так никто не мешает. Мне очень сложно представить, что существуют люди, занимающиеся погромированием дольше получаса, и у которых возникают дополнительные трудности, если вместо fetch_some_entries() им нужно написать container_type<entry_type> fetch_some_entries() и т.п.

Компилятор следит за верностью использования

Замечательно же! Про бизнес-логику не понял: да, не следит (хотя если бизнес-логику выразить в типах, то будет). А в динамически типизированных языках кто-то следит что ли? В чём их преимущество конкретно в этом вопросе?

Нет, само оно только подсказывает типы

Ну да. 1 раз "настроили" (описали всё), а дальше всё работает (подсказывает, если угодно).

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

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

В языка со слабой типизацией вы просто объявили контейнер и кладете туда все что хотите

Не задумываясь о том, что и куда? В конечном счёте вам же нужно не "что-то" положить в контейнер, а вполне конкретную нужную вещь. В этот момент вы автоматически "подгружаете" в мозг тип загружаемого. А дальше разница в десятке символов на объявление контейнера с типом хранимого вместо просто контейнера.

В языка со слабой типизацией вы просто объявили контейнер и кладете туда все что хотите.

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

Кроме просто написания, это нужно еще продумать.

Мой опыт показывает, что думать полезно.

Ээээ. Почему 2 рилма? Если типы правильно использовать они являются прямым отражением домена бизнес-сущностей и их связей. Технические типы всяких библиотек живут на самой окраине и безнес-код вообще про них не в курсе.

Да, я сторонник DDD и DSL

Правильно настроенный линтер на языке со слабой типизацией сводит практически все описанные ошибки на нет.

А знаете, как называется достаточно продвинутый линтер?


Тайпечекер.


И почему-то они есть у всех используемых на практике "динамически типизированных" ЯП.

И почему-то они есть у всех используемых на практике "динамически типизированных" ЯП

Побуду адвокатом дьявола динамической типизации: что мешает создателям ЯП добавить переключатель в компиляторе с динамической типизации на статическую?

Не для всех языков это возможно. Java и C# хранят типы в рантайме, там такое провернуть затруднительно. Хаскель и typescript избавляются от типов после компиляции, но если typescript «без типов» - это JavaScript, то для динамического хаскеля сложно будет найти применение

В C# для этого есть специальный тип dynamic.

Конечно, тут можно поспорить, но в целом согласен с Вами

Как говорил агент Смит из Матрицы: "Мы здесь, потому что нас лишили свободы". Выбрали game-dev и дорога вам в Unity или Unreal. Решили заниматься front-endом - привет, javascript! Конечно, есть ts - он помогает, но коварен, ибо soundness.

На заре интернета динамические языки были удобны для веба. Писать url-encoded парсер для на с++ было утомительно, на perl это буквально 2 строки. Исторически так сложилось, что php и Битрикс до сих пор живут (кто-то же должен поддерживать legacy). Пока остался на python (django, fastapi): довольно гибко, и типизацию какую-никакую подвезли.

Вот что сейчас выбрать для full-stack веба, если откинуть java и c#? Пробывал rust+rocket - ну сложный для меня раст. Go тоже "не зашёл".

Rocket не был хорошим фреймворком в своё время. Попробуйте раст посвежее. Какой-нибудь axum возьмите, например.

Я для себя определился со следующим стеком:

CSS - Bulma

Frontend - Vue (на JS компонентах, без TS)

Backend - Rust

Быстро и сердито. Сначала отбиваю всю логику и модель данных на фронте.

А после уже за пару проходов добавляю бек.

Как начал так делать, то скорость выхода на MVP сильно выросла.

Выписывать и поддерживать сложную бизнес-логику на Rust - не лучшая идея. C# или Java тут все же больше к месту. Причем не столько возможностями языка, сколько широким выбором уже готовых пакетов. Хотя бывают исключения, когда GS становится поперек горла и приходится переходить на тот же Rust или C++.

Сама инфраструктура вокруг Rust еще недостаточно развита. Базовые классы есть. А вот если копнуть глубже, то даже адекватной реализации фильтра Калмана не нашел, когда понадобилась.

Да даже с базой проблемы, когда родной драйвер к PostgreSQL молча(!) превращает DECIMAL с единичкой в 30-ом знаке после запятой в ноль. Можете считать это придиркой, но раз сам PostgreSQL поддерживает до 16383 знаков после запятой, то логично требовать если не такой же поддержки и от его драйвера, то хотя бы генерации исключения при переполнении.

Не всякий backend содержит сложную бизнес логику.

Мне возможностей стека на Rust хватает. После Java, конечно, бедновато, но работать можно.

В Java мне не нравится его жадность до памяти и CPU.

Интересно как, а я обычно стартую именно от структуры данных. А что используете в качестве фреймворка для раста? И отдаёте только json для vue? Шаблонизатор не используете? (Не смог найти нормальный типограф для русского языка на расте)

Отдаю json. Если нужен шаблон, то tera.

Веб фреймворк Actix.

Тоже раньше делал бд, потом бек.

Но, обычно, когда начинается UI приходит понимание, что все не так юзер френдли и надо переделать. И переделать весь стек выходит дольше, чем если сначала на мокап-данных сделать UI.

Интересно как, а я обычно стартую именно от структуры данных

Так я тоже. Сначала набросок модели данных. После делаю UI по нему.

И тут сразу получается понимание какая модель нужна для UI, а какая для БД. Они разные в общем случае.

Прикольно. А я с архитектуры решения начинаю крупным планом. Модели данных уже возникают потом, когда определились с видами БД (реляционные, OLAP, NoSQL, кеширующие), брокерами сообщений, видами контрактов и интеграцией со внешними системами.

Не боитесь сразу заоверинженирить? :)

Ваши предложения? Делать что ли модель данных под реляционные СУБД, а потом уже по ходу разработки растаскивать ее части, модифицируя, например, под Cassandra, Redis, ClickHouse?

В теории СУБД выбирается под модель данных, когда становится понятно, что за данные и какие запросы. Но на практике нередко приходится подстраиваться под ограничения существующей инфраструктуры.

В теории СУБД модели данных для реляционной, OLAP и NoSQL БД различаются принципиально. И дело далеко не в инфраструктуре, а в том, что, для примера, если для реляционной БД модель данных делается нормализованной, то для OLAP - наоборот, модель денормализуется для более эффективной работы COLUMNSTORE. Аналогично и в NoSQL (Cassandra или Redis) модель данных принципиально отличается от реляционной.

когда становится понятно, что за данные и какие запросы

Вот именно об этом я и написал:

А я с архитектуры решения начинаю крупным планом

И только после этого решается какие данные где и как хранить, какие запросы какой сервис должен выполнять и какие БД ему для этого нужны.

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

Пробывал

От слова "быть". Слово, которое вы ищете - "пробовать"

Пора ввести на Хабре синтаксический анализатор идентичный натуральному. ;-)

Посмотри Nim, причём на нём можно писать как backend (транпиляция в C и компиляция, есть кроссплатформенная), так и frontend (транпиляция в JS). На Go возня с типами утомляет, а на Nim приятно всё сделано, очень хорошо продуманный язык, библиотеки крошечные, можно изучать по ним язык, при этом довольно мощные, и своего кода будет гораздо меньше чем на других языках, много синтаксического сахара, программировать одно удовольствие.

Не спорю, сам за статическую типизацию, но заголовок тронул:

Я до последнего буду защищать ...

До последнего кого или чего? :)

В оригинале:

Strong static typing, a hill I'm willing to die on...

Красиво, а?

У неиспользования типов есть преимущества

Мы всегда используем типы, вопрос лишь в том, держим мы их в коде или только в голове

они гораздо удобнее при работе с REPL

Странное утверждение. Скажем, скала статически типизированный язык (с выводом типов). При этом у нее более чем приличный REPL, которому наличие типов нифига не мешает. И все там естественно.


Кто-то может пояснить, как наличие типов вообще может мешать REPL, или их отсутствие упрощать работу?

Думаю здесь имеются в виду реплы лиспов, которые на порядок лучше аналогов в статически типизированных языках

Так я и пытаюсь просянить, чем именно они лучше? Скажем прямо, я давненько не работал с реплом лиспа (и вообще кажется работал лишь с одним из них, который встроен в AutoCAD, и который ну как бы не совсем стандартный лисп. И я не могу вспомнить, что там такого прямо хорошего, чего я в скале не найду. Ну разве что — исполнение кода, который я тут же и сгенерировал, т.е. мета- или макро программирование. Так это не факт что прямое следствие отсутствия типов или другой системы типов. Для статически типизированных языков вполне существуют системы макропрограммирования.

Выполнение блоков кода прямо в редакторе, без необходимости копировать в репл. Кроме того, clojure flowstorm debugger может делать не только step in/into, но и еще step back.

Выполнение блоков кода прямо в редакторе
не только step in/into, но и еще step back.

Это все несомненно удобно, но не очень ясно, как это связано с типами или их отсутствием? Для выполнения блоков кода в редакторе все что нужно — это поднятый runtime языка, и в режиме отладки IDEA это умеет для Java. Ну то есть, тут скорее вопрос в том, в каком контексте выполняется этот ваш код в редакторе, что за окружение ему доступно в широком смысле? Когда мы в отладчике — то мы например остановились на какой-то строке кода в процессе выполнения, и с контекстом все более-менее ясно.


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


Ну то есть, скорее всего связь между удобством REPL и языком есть, но она какая-то не слишком очевидная.

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

Просто в lisp принято разрабатывать через REPL, потому что по-другому сложно. Это особенность среды - там есть ядро языка, и мы его расширяем. Поэтому и удобно интерактивно это делать и тестить в репле. И сама программа по-сути всегда целое ядро языка + наши функции. В smalltalk тоже так, там по-сути IDE выполняет роль интерпретатора и среды запуска программ, они без него не могут жить.

В остальных языках оно почти никогда не надо, потому что на выходе артефакт - бинарник для ос или байткод, который гоняется на виртуальной машине. Так-то REPL и в хаскеле есть.

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

impl<SinkT, ServerRpcVersion, ClientRpcVersion, T> Handler<SendClientRpc<ClientRpcVersion, T>> for Session<SinkT, ServerRpcVersion, ClientRpcVersion>
where
    SinkT: Sink<SinkItem = Bytes, SinkError = std::io::Error> + 'static,
    ServerRpcVersion: rpc::deserializer::ServerProtocol,
    ClientRpcVersion: rpc::serializer::ClientProtocol,
    T: Into<ClientRpcVersion::ClientRpcKind>,
{
    type Result = std::io::Result<()>;

    fn handle(&mut self, rpc: SendClientRpc<ClientRpcVersion, T>, _ctx: &mut Context<Self>) -> Self::Result {
        self.send(rpc.data).map(|_| ())
    }
}

Оставлю это здесь как аргумент против типов. Желающие могут попытаться найти ошибку

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

Меня в расте больше смущает "обратное" выведение типизации, когда ты там создаёшь какой-либо, ну допустим Vec::new() , без указания типа, и где-то там сильно дальше в него что-либо пихаешь. И это определяет его тип, снизу вверх.

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

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

Правда rust-analyzer иногда конечно тупит на сильно сложных выводах, с associated types, GAT и т.д. Особенно когда идет цепочка GAT 😅

И автоматический вывод типов на уровне функции очень удобен с поддержкой IDE. Ведь случаи описанные вами крайне редки. Функции обычно стараются делать маленькими, либо разбивать на блоки. Ну а входные и выходные данные функции должны быть прописаны точно

Правда rust-analyzer иногда конечно тупит на сильно сложных выводах, с associated types, GAT и т.д. Особенно когда идет цепочка GAT

Да, а особенно тупит на достаточно больших монорепах (у нас около 7к файлов), вплоть до невозможности использования.

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

Функции обычно стараются делать маленькими, либо разбивать на блоки.

В идеальном мире - да... :)

Не имею ни малейшего представления, что тут происходит, но как минимум бросается в глаза, что у ServerRpcVersion стоит deserializer, а у ClientRpcVersion -- serializer, хотя из схожести названий интуитивно кажется, что и типы должны совпадать.

В чем же проблема? Ведь явно прописаны все ИНТЕРФЕЙСЫ типов. Прописывание конкретного типа ничего не изменит. Пользователь и так точно знает что может делать тот или иной входной аргумент.

Несовместимые типы и аргументы, также как их неправильное использование сразу даст ошибку компиляции, ну или в случае с Rust, начнет жаловаться rust-analyzer. Искать ошибку самому нет необходимости.

Желающие могут попытаться найти ошибку

Ошибку, которая не сводится к тому, что где-то не сошлись какие-то два типа, я так понимаю? Потому что иначе непонятно, зачем её искать нам, а не компилятору.

А что тут не так? Ну, кроме форматирования? А ошибку пусть компилятор ищет.


В ответ предлагаю вам найти ошибку вот в этом коде:


handle(rpc, _ctx) {
    return this.send(rpc).map(() => null)
}

Просто есть люди, которые фигачат в продакшен лишь бы работало. Они никогда не поймут, зачем нужны шашечки статические/строгие типы.

изучение типов — это одноразовое вложение труда.

Это изучение безтипного синтаксиса вещь одноразовая. k = k + 1 во многих языках вглядит одинаково. Выучив один раз, сразу открывается куча применений: TypeScript, С++, Python, Java, Go, ... вплоть до разной экзотики вроде Haskell или OCaml. Короче весь мир у ваших ног.

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

Ну строго говоря типы учить как то особо не надо. Просто используешь их и все.

Например в python есть классы, и их надо как то объявить, так и здесь тоже самое.

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

Можно пойти дальше. Ты объявляешь что функция принимает что угодно, если она реализует вон тот интерфейс (грубо говоря у нее есть какая-то определённая функция, которая возвращает определённое значение). Это уже дженерики.

Ну строго говоря типы учить как то особо не надо. Просто используешь их и все.

Ну вообще вы наверное правы, хотя проблем на самом деле хватает даже в python https://github.com/python/typing/issues. Т.е на практике интуитивно понятные вещи здесь довольно быстро заканчиваются.

После 10 лет работы программистом 1С(нет строгой типизации) фантомная боль в отстреленный ногах заставил перейти в Java\Kotlin, вроде потихоньку регенерирую. Когда общаюсь друзьями на той стороне, всегда интересно послушать как люди поддерживают решения на 3+ млн строк кода без типизации и тестов, какие у них новые веселые ситуации случаются.

После многих лет программирования на Typescript не представляю себе программирование на JS/PHP/Ruby/Python. Это будет (для меня) невыносимая боль. Но могу побыть адвокатом дьявола, ведь не всё так просто. Скажем, я, поставь предо мной выбор, язык с примитивной статической типизацией VS язык с динамической, точно выберу второе. Т.е. я бы не моргнув взял бы JS, а не Java. Но Haskel/TS/Flow/Kotlin, а не JS. Почему? Потому что языки с продвинутой системой типов позволяют взять лучшее из двух миров:


  • дают вам вменяемые гарантии надёжности вашего кода
  • огромные плюшки по рефакторингу
  • оставляют сопоставимую с дин. языками гибкость и скорость разработки

Приведу пример самого вкусного. Тесты. На Typescript вы можете не использовать DI, в тех случаях, когда он у вас был только ради mock-ания в тестах. Почему? Потому что мокать можно import-ы из файлов. И при замене подставлять минимально-приемлемый огрызок от заменяемой сущности. В сравнении с полноценным DI у вас и проект куда проще, и тесты писать можно одной левой. Тесты при этом не потеряли своей функциональности. Они всё также тестируют, всё то, что вы хотели. Но они дались вам, в каком-то смысле, бесплатно. Языки же с sound static типизацией заставят вас очень много приседать. Что скорее всего сведётся к тому, что если сущность, не сильно важная, то обойдётся она и без тестов.


Ну и ложка дёгтя. Sound static type языки гораздо быстрее в runtime-е. По очевидным причинам. Компилятор гарантировано знает о вашей программе всё, и может применить все мыслимые и немыслимые оптимизации. В случае со структурной типизацией, компилятор всегда будет исходить из "тут всё сложно, мои полномочия на этом всё".

Sound static type языки

Можете привести несколько примеров таких языков?

C++, Java, Rust, Pascal и пр. Понятно, что абсолютной soundness они не предоставляют (например позволяют обращаться к произвольному элементу в массиве, не проверяя его существования), но разумная доза soundness-ти там есть. Что позволяет компилятору, скажем, обращение к конкретному полю объекта в коде превратить в прямой переход по сдвигу в памяти.

Haskel/TS/Flow/Kotlin

А в Kotlin есть прям что-то принципиально более крутое чем в Java? Всегда считал что он больше про практичный сахар для всё той же java/c#-ной ООП парадигмы, нежели что-то фундаментально другое, как Scala.

Это надо котлинистов спрашивать. Я просто жертва их пропаганды :-) Знакомые android-разработчики очень нахваливали. И правда стоило указать Scala, вылетело из головы.

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


Проблема тут в другом. Во-первых, такие тесты медленнее (кстати, это и JS касается). Во-вторых, это получаются тесты реализации, а не спецификации.

Во-вторых, это получаются тесты реализации, а не спецификации.

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

Все же как-то обошли стороной случаи, когда грубая сила (union, приведение типов указателей/reinterpret_cast) все же необходима. И если язык это не поддерживает вообще и никак, то некоторые задачи на нем эффективно не реализуешь.

На одной типизации все равно до конца не доедешь - рано или поздно придётся расставлять костыли и писать тесты. Потому, что в пределе логика упирается в Теорему Гёделя о неполноте: любая формальная система либо неполна, либо противоречива. На практике лучше иметь дело с неполнотой. Это значит, что какие-то утверждения во всякой непротиворечивой системе типов будут невыводимыми.

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

Это какие, например?

Вы серьезно? Никогда, например, не приходилось работать со страницами БД? Особенно если они уже закешированы в оперативке.

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

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

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

Вижу статью в поддержку строгой типизации - ставлю плюс.

Сколько языков вы знаете, поддерживающих одновременно и сильную и статическую? Я вот всего два. И только один практикую. В упомянутой жабе - типизация статическая, но слабая. Однако автобрекетинг и прочее неявное приведение не делает ее менее пригодной для промышленного применения чем сильные но динамические пхп и жс, а выведения у нее появились для тех кто не залип на мобайле (>1.8).

Так вот собственно вопрос. Что именно вы все-таки будете защищать до последнего? Явные типы в компайл-тайме или отсутствие неявных приведений? Потому что оба двое - редкое явление.

Если у языка статическая слабая типизация, обычно, только ключами компилятора это легко изменить, сохраняя при этом возможности type punning. А вот если язык не поддерживает статическую типизацию - это только частично можно исправить костылями через статическую типизацию в комментариях.

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

Несомненно, строгая типизация уменьшает ошибки.

Я бы ещё дальше пошёл: надо сделать возможность объявлять подтипы простых типов. Скажем это не double, а литры и с килограммами их никак не сложить.

Или, допустим, это не просто строка, а SQL запрос и для присвоения строки запросу надо пропустить через escape функцию.

Такое вроде бы в Aда есть - возможно, ещё в каких-то языках присутствует?

сделать возможность объявлять подтипы простых типов. Скажем это не double, а литры и с килограммами их никак не сложить.

А какие проблемы это сделать на C++?

В функциональных это очень частая фича, да и в другие языки её активно тянут.


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

Рассмотрим следующие четыре примера

Непонятно где четвёртый. Смотрю и в оригинале так же.

Sign up to leave a comment.

Articles