Открыть список
Как стать автором
Обновить

Комментарии 68

Отличный слог. Автор, пиши еще!


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


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

Спасибо! А то как закопаешься в свой Энтерпрайз и не думаешь ни о чем.
Буду рад видеть ещё ваши статьи.

Неполнота

Я совсем не настоящий CS, но мне кажется, что тут причина ближе не к Гёделю, а к теореме Райса

Именно так (ну, с небольшой коррекцией: не существует разрешимых систем типов, которые типизируют любую корректную программу и не типизируют некорректную).


На Гёделя ссылаться вообще довольно странно, потому что для начала придётся определить, что значит «корректно решить задачу».

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

Я тут тоже ещё подумал, и как-то внезапно понял, что Гёдель-то ведь на самом деле про обратное: если ваша логика (то есть, система типов) позволяет выразить арифметику, то существует утверждение (то есть, некий тип Ty), который нельзя доказать (то есть, не существует терма с этим типом), и который нельзя опровергнуть (то есть, не существует терма с типом Ty → ⊥).


В каком-то смысле он про неполноту языка термов, а не языка типов.


А то, что вы рядом пишете — это стандартный Карри-Говард.

Говорю же тут немного плавую. Знаю что изоморфизм есть, но не знал даже как он называется:) Про термы не понял, что такое терм в данном случае? Какое-то объявление типа или выражение?
И кстати как тебе статья в целом? Твое мнение было бы мне важно т.к. я на несколько твоих статей опираюсь здесь и даю на них ссылки:)
Про термы не понял, что такое терм в данном случае? Какое-то объявление типа или выражение?

По большому счёту тело функции.


И кстати как тебе статья в целом? Твое мнение было бы мне важно т.к. я на несколько твоих статей опираюсь здесь и даю на них ссылки:)

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

С утра ещё подумал и надо наверное пояснить почему программа это теорема. Теорема по сути это набор аксиом и последовательность применённых к ним правил вывода. А программа это набор входных типов и последовательность функций которые переводят один тип в другой. Компилятор верифицирует, что действительно есть такие функции, которые могут так трансформировать типы. Верификация теоремы проверяет, что действительно все правила вывода применены корректно. Один и тот же процесс по сути, только вместо типов утверждения. Легко показать, что для любой теоремы существует эквивалентная ей программа, так как надо всего лишь задать по типу для каждого утверждения. В обратную сторону это тоже работает. Если выписать все функции как правила вывода, а все типы как утверждения.
Без Гёделя возникает мысль что раз доказательство наличия свойства я пишу сам, то может быть удастся его построить даже если в общем виде такая задача невычислима. Гëдель же говорит, что доказательства нет вовсе.
Там кстати метод обхода похожий: делаешь функцию с кастом, но безопасным — как бы добавляешь геделевскую теорему как аксиому.

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

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


Отдельное спасибо за упомянутый мимоходом момент с Map<K, ? extends List<? extends X>>. Я однажды пытался сделать что-то такое, но оно у меня даже с List<? extends X> не завелось, и мы решили сделать три функции с разными именами, не совладав с мощью джененриков. Надо будет попробовать сделать через более всеобъемлющщую ковариантность.


Спасибо за статью и за ссылки, это было прекрасно. Осталось только усвоить прочитанное. :D

Ради таких отзывов и хочется писать :)
Зачем мне проверять, пусть даже статически, что я кладу совместимый объект? Я и так держу в голове всю программу и помню что можно класть в какие методы, а что нельзя.
Я не помню, а понимаю. Точно так же, как я не помню книжку на 500 страниц, но понимаю концепции, изложенные в ней, и могу пользоваться ими, если я их понял.

Согласен, я ниже немного детальнее это описал но ваш подход к пояснению хорошо это дополняет. Кажется я ещё не доконца взломал все детали особеднностей мышления при работе с СТ и ДТ.

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

Что касается концепций, признаться не могу понять в чем аргумент. У меня например постоянно такое, что концепции одинаковые (по крайней мере в моей голове), а код всё таки приходится подправлять.
Что касается концепций, признаться не могу понять в чем аргумент.
Я имею в виду, что я не программу держу в голове, а концептуальную репрезентацию (ближайшая аналогия — направленный граф, от main() к функциям самого низкого уровня). Достаточно взять конкретные ветки этого графа, чтобы знать, откуда пришли данные, что с ними происходило, и в какой форме они могут быть в конкретном месте в программе.

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

Самое интересное, что я не читал ваш комментарий когда ответил точно так же ниже. Это важный ключевой момент.


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

Да, это и похоже на перемещение контекста вместе с процессом исполнения.

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


А как ты понимаешь глядя в локальную точку графа откуда пришли данные и какова их природа без типов? Вот если ты пришел в проект новый и тебе дали задачу на шаге X сделать ещё одно действие Y. Или все таки держать в голове весь путь от инпута до текущего шага исполнения обязательно? Тогда кажется я все корректно описал про «держать всю программу в голове»:-)
Или все таки держать в голове весь путь от инпута до текущего шага исполнения обязательно?
Иметь представление обязательно. О том, с чем связан кусок кода, с которым я работаю прямо сейчас. И в любой системе, работая с её частью нужно иметь представление, о том, как изменения коснутся системы в целом. По крайней мере в моей картине мира.

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

Для имён это и правда просто. А когда у вас есть gulp, который использует vinyl-fs, который использует vinyl, а вам надо всего-то понять как прочитать имя и содержимое файла, пришедшего к вам через пайп…


Или когда у вас есть API Яндекс-карт, и вы пытаетесь передать опции компоненту Clusterer, но создаёте вы его не напрямую, а через ObjectManager…

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


К слову в Rollup с этим по проще. ) Это я к тому что ДТ куда более чувствителен к хорошему дизайну, увы в JS люди не так сильно знакомы с Zen Python. Собственно я уже говорил тут что "простота" и "явность" особенно важны для ДТ.

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


Скажем, в первом моём примере поддерживающая тайпинги (при их наличии) IDE сразу же подскажет, что пайплайн gulp — это стандартный нодовский стрим, по которому передаются объекты Vinyl из пакета vinyl. И дальше можно сразу же смотреть документацию на vinyl, пропустив этап ознакомления с gulp и vinyl-fs.


К слову в Rollup с этим по проще. ) Это я к тому что ДТ куда более чувствителен к хорошему дизайну

А откуда он возьмётся, этот хороший дизайн, если писать код в соответствии с вашими советами — без проектирования и строго в порядке его исполнения?


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

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

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


А откуда он возьмётся, этот хороший дизайн, если писать код в соответствии с вашими советами — без проектирования и строго в порядке его исполнения?

Я не призывал отказываться от проектирования, просто это не должно быть самоцелью. Если уж говорить про советы то это KISS, DRY, YAGNI and premature generalization is the root of all evil.


от же rollup — это всего лишь бандлер, в то время как gulp — система сборки.

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

Видимо стоить раскрыть ещё одну тему из моей гипотетической статьи — уверенность.
На тему СТ и ДТ я очень много спорил с моим коллегой и он был ярым поклонником СТ. После очень долгих и детальных обсуждений мы выяснили что мы по разному относимся к риску и понятию "уверенности". Для него было не комфортно быть не уверенным в типе переменной или в поведении, тогда как я не чувствовал никакого дискомфорта. Скорее всего вы пытаетесь «держать всю программу в голове» именно из-за того что не полная уверенность вам некомфортна и вызывает негативные эмоции. Разное отношение к риску основано во многом на вашей генетике и воспитании, сюда же входит насколько импульсивно вы принимаете решения.
Если вам нравится всё контролировать, строить в голове абстракции, любите строгость (в том числе в искусстве) то скорее всего ДТ вам будет крайне не удобно.
Все люди мыслят по разному, но судя по всему есть разные патерны исходящие из сильных и слабых сторон вашего мозга. У кого то хорошо с памятью (причём по отдельности быстрой и долгой), у кого то с аналитическими способностями а кто то силён в творчестве (программирование это в том числе творчество). И это всё влияет на выбор вами ЯП, технологий и прочего. Я надеюсь эти идеи не покажуться вам слишком радикальными.

Что касается концепций, признаться не могу понять в чем аргумент. У меня например постоянно такое, что концепции одинаковые (по крайней мере в моей голове), а код всё таки приходится подправлять.

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

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


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

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


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


В итоге программы получаются разные и когнитивные навыки задействуются по разному при написании и мы приходим к тому что СТ vs ДТ зависит от психологии, от образа мыслей конкретного человека (если конечно убрать проблему производительности за скобки).

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

То же самое можно использовать в языках со статической типизацией и получить также плюсом и проверки компилятора.

Я не очень понял на что вы отвечаете… я не говорил что так нельзя писать на ЯП с СТ, я говорил что это очень важно для ЯП с ДТ, а для СТ это не настолько критично (типы дают ту избыточность которая позволяет вам проводить навигацию даже в тонне оверинженерингого кода)

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

Звучит так, как будто программа пишется так же, как исполняется её исходный код, в виде некоторого пайплайна. Однако, на практике написание программы скорее напоминает переусложнённую версию игры в Дженга, где вам приходится расширять программу изнутри периодически выделяя общие паттерны в мини-фреймворки, попутно удаляя неиспользуемое легаси. Более того сам характер взаимосвязей между компонентами не похож на поток: низкоуровневые компоненты переиспользуются многократно причем на разных абстрактных уровнях.
Сам я сталкивался с таким, что разработчик, не осилив написать типобезопасную версию апи, оставлял торчать Object-ы в декларациях или явные касты в коде и в результате тривиальный рефакторинг ломал всю программу, но понятно это было только при запуске тестов, а хочется же во время компиляции.
в этих языках важно писать как можно проще (явно как завещал Гвидо), компактнее и выбирать максимально читабельные и полные названия для всего

Писать так программы хорошо на любом языке, но тут мне кажется есть некоторая подмена понятий: хорошо писать код, который выглядит просто и понятно, но писать такой код совсем не просто: от того и идет «когнитивная нагрузка». Более того, если вы пишете библиотечный код, то самое главное, что бы им было просто и удобно пользоваться, а это зачастую значит писать очень сложный код внутри самой библиотеки.
Звучит так, как будто программа пишется так же, как исполняется её исходный код, в виде некоторого пайплайна.

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


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

Для ДТ языков это естественно и кроме того вы просто обречёте себя на боль если не будете делать так в ДТ языках (которые это позволяют делать лучше). Ну и у вас меняется мышление по этому особо нагрузки нету.


версию игры в Дженга

именно так выглядит написание программы на языках с СТ. Это особенно чувствуется в Java/C# и возможно в Rust. В C#/Java это усугубляется обязательностью классов и в целом классовым ООП.

Ну вот вы написали пайплайн, а потом уволились и мне надо добавить изменений в середину? Или скажем изменить какую-то функцию, которой пользуетесь вы и ещё в нескольких разных местах разные люди? И начинается Дженга, где одно неловкое движение и башня упадет. Или вы просто выбрасываете весь ранее написанный код и переписываете его заново?:-) Я честно не понимаю.
Я честно не понимаю.

Надеюсь смогу пояснить.


Ещё один хороший аргумент который постоянно всплывает. С моим коллегой мы выяснили что он в СТ 80% времени тратит на рефакторинг и только 20% на написание нового кода, у меня же обратная пропорция. Очень странно правда? Мы стали выяснять и поняли что:


  1. В СТ вы пишите сильно связанный код, где изменения в одном месте обязывает вас менять много других кусков.
  2. В ДТ как правило пишут слабо связанный код, где мало связей между модулями (к примеру хендлер у API endpoint).

Ну вот вы написали пайплайн, а потом уволились и мне надо добавить изменений в середину?

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


Или скажем изменить какую-то функцию, которой пользуетесь вы и ещё в нескольких разных местах разные люди?

Берёте и меняете… если вы меняете поведение функции то вы либо создаёте новую функцию с новым именем либо (что геморойно и зачем такое вообще?) бегаете поиском по проекту и меняете поведени, но вообще это моветон такое делать не соглосовав с коллегами. Если вы только добавляете аргумент то ничего менять не надо.


И начинается Дженга, где одно неловкое движение и башня упадет.

Не вижу где она тут начинается. Ну и если где то подправить забыли то на тесте упадёт и вы это подчистите.


Или вы просто выбрасываете весь ранее написанный код и переписываете его заново?

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


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

Ещё один хороший аргумент который постоянно всплывает. С моим коллегой мы выяснили что он в СТ 80% времени тратит на рефакторинг и только 20% на написание нового кода, у меня же обратная пропорция. Очень странно правда? Мы стали выяснять и поняли что:

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

В ДТ как правило пишут слабо связанный код, где мало связей между модулями (к примеру хендлер у API endpoint).

Это возможно только в двух случаях. Либо у вас такая задача, которая позволяет писать настолько слабо связный код (и тогда вам никакая СТ не помешает делать то же самое) — либо вы просто копи-пастите один и тот же код каждый раз, когда он вам нужен вместо того чтобы использовать общие модули. Ну или пишете заново, что ещё хуже.

Либо у вас такая задача, которая позволяет писать настолько слабо связный код (и тогда вам никакая СТ не помешает делать то же самое)

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


  1. У ДТ нету болилерплейта связанного с типами, и код получается чище (и компилировать не надо).
  2. Почему то решения для СТ (библиотеки и прочее) толкают тебя к писанию и сильно связаного кода. Вместо callback функции к примеру, обьявлять класс… или для того что бы записать что то в файл из сети надо реализовать целый конвеер по сохранению.

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

Что-то вы всё в кучу намешали.

1) Благодаря выводу типов, во многих современных языках программирования не так часто требуется явно указывать типы переменных.
2) Динамическая/статическая типизация и компилируемость/интерпретируемость вещи ортогональные — ЯП может быть компилируемым и с ДТ, а может быть интерпретируемым, но с СТ.
3) Какая связь между объявлением класса и связанностью кода?
4) Не уверен, что правильно понял вашу мысль про конвейер, но если вы о чём-то вроде этого, то я совершенно не понимаю, что вы понимаете под связанностью кода. Потому что подобные подходы как раз и предназначены для уменьшения этой самой связанности.

Заодно было бы интересно узнать ваше мнение по вот какому вопросу: если динамическая типизация настолько хороша, почему во многих ДТ-языках (python, php, ruby) тренд на добавление возможностей СТ?
ЯП может быть компилируемым и с ДТ
Можно пример?

В зависимости от того, как развернуть понятие "компилируемый", можно разные примеры найти:
Objective-C, Common Lisp, Chez Scheme, Julia, C#, Erlang/HiPE, Lua/LuaJIT...

В принципе, за компилируемый язык с ДТ вполне себе сойдёт ассемблер.
В целом же хочу обратить внимание, что вид типизации — это свойство языка, а компилируемость — свойство реализации.
Например, для Basic существовали и компилятор, и интерпретатор.

Мало смысла от интерпретатора языка с СТ так как тогда вы не сможете проверить полноценно типы до запуска. Ну и СТ позволяет как правило производить относитльно легко эффективный машинный код чем грех не воспользоваться.
А компилируемый ДТ не будет во время такой операци опущен до машинного кода по настоящему. (т.е a+b не будет выглядить как комманда процессору a+b ) По сути вы просто смержите интерпретатор или его части с вашей программой. Тут только JIT может помочь хотя это уже runtime.

Сколько в этом смысла — вопрос другой.

Главное, что мы определились, что компилируемость/интерпретируемость и динамическая/статическая типизация — вещи ортогональные.
1) Благодаря выводу типов, во многих современных языках программирования не так часто требуется явно указывать типы переменных.

Вывод помогает при работе с перменными внутри функций, но боилерплейт остаётся при объявлении интерфейсов (это и функции если что).


2) Динамическая/статическая типизация и компилируемость/интерпретируемость вещи ортогональные — ЯП может быть компилируемым и с ДТ, а может быть интерпретируемым, но с СТ.

Вопросс во времени, запуск Python, JS, PHP почти моментальный, сборка большого проекта с шаблонами на C++/Rust уже может занять десятки минут (и даже если вы не с 0 собираете это дольше). У C#/Java по лучше дела но старт и прогрев VM это отдельная боль.


3) Какая связь между объявлением класса и связанностью кода?

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


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

Мы не про паттерны тут вообще.


Заодно было бы интересно узнать ваше мнение по вот какому вопросу: если динамическая типизация настолько хороша, почему во многих ДТ-языках (python, php, ruby) тренд на добавление возможностей СТ?

А ещё TS для JS. Во многом это связано с тем что в эти языки переходят люди с СТ языков и чувствуют дискомфорт, пишут плохой код и т.д. эти подпорки позволяют им писать в их стиле более или менее комфортно. Другая важная вещь это то что уже упоминалось тут — навигация по коду.
Если у библиотеки описан СТ интерфейс то при наличии хорошего редактора этим пользоваться становится удобнее и нужно меньше смотреть в документацию и исходники.
К счатью это опционально и можно употреблять только там где от этого выгода привышает минусы.


Ну и если вы меня записываете в противники СТ — то нет, я сам этим пользуюсь, пишу на C/C++ и активно ковыряю Rust (а до этого много писал на C# и Java).

Вывод помогает при работе с перменными внутри функций, но боилерплейт остаётся при объявлении интерфейсов (это и функции если что).

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

Вопросс во времени, запуск Python, JS, PHP почти моментальный, сборка большого проекта с шаблонами на C++/Rust уже может занять десятки минут

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

От класса могут унаследоваться, у класса более сложный интерфейс

В js/python/php тоже есть классы, а в С их нету. Из ваших сообщений я всё ещё не могу уяснить, какие связи между утверждениями «X — компилируемый/интерпретируемый», «в X динамическая/статическая типизация» и «в X используются классы».
Опять же, не могу не отметить, что не вижу «прямой» связи между классами и связанностью, если под связанностью мы понимаем это.
при этом большой проблемы в указании типов аргументов/результата я не вижу
Я лично вижу большую проблему делать это несколько тысяч раз за пару месяцев.

Лично мне комфортнее видеть сигнатуру функции, чтобы понимать её ограничения и область применения
Ну а мне, если я читаю уже написанный код, нужно понимать не как функция _может_ использоваться, а как она _используется_ в этой конкретной программе. Типы с этим слабо помогают, хорошо помогает граф вызывов (call graph).
хорошо помогает граф вызывов (call graph)

…который для методов в языках с динамической типизацией фиг построишь. И уж точно его не получится построить автоматически.


А вот статические типы как раз помогают его строить.

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

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

нужно понимать не как функция _может_ использоваться, а как она _используется_ в этой конкретной программе

Вот как раз в этом вопросе статическая типизация помогает гораздо лучше — нет нужды заглядывать в исходники функции, потому что ваша IDE легко подскажет вам, результат какого типа она возвращает.
Я не знаю, какой код вы пишете, что вам приходится несколько тысяч раз за пару месяцев менять сигнатуры функций.
Не менять, а писать. ~500 функций за пару месяцев разработки либы с нуля => несколько тысяч переменных в их сигнатурах.
По моим субъективным ощущениям, менять сигнатуру приходится довольно редко.
По моим тоже.
нет нужды заглядывать в исходники функции
Что и как конкретно она делает тогда как узнать? Какие другие функции она вызывает? В моём понимании чтение кода — и есть заглядывание в исходники.
Поясню свою мысль.
Допустим, я читаю код, в котором есть вызов функции foo().

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

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

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

Само собой, этот недостаток можно сгладить, например, подходящим наименованием — вроде методов с ! и ? в ruby. Но это всегда останется лишь соглашением, которое требует определённой работы, а при наличии статической типизации всё это есть у вас из коробки.
ведь я знаю главное — её результат
Я не могу сказать, что я знаю результат, только посмотрев на сигнатуру. Я могу предположить, а предположение может быть неверным. Чтобы знать, нужно читать, что функция делает (или доки, если есть). Как-то так.

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

Возвращаясь к моему примеру, рассмотрим функцию dest из пакета vinyl-fs, представив что документация на неё куда-то делась.


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


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

Только если тот, кто писал код, не ставил целью написать простую в понимании программу.


В хорошем коде назначение большинства функций должно быть очевидно.

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

Я лично вижу большую проблему этого не делать. Это ж самому полезно: написал foo :: String -> Int -> Maybe Float, и не нужно помнить, где что ожидается, где что возвращается.


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

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

Вопросс во времени, запуск Python, JS, PHP почти моментальный, сборка большого проекта с шаблонами на C++/Rust уже может занять десятки минут (и даже если вы не с 0 собираете это дольше). У C#/Java по лучше дела но старт и прогрев VM это отдельная боль.

Язык со статической типизацией вполне себе может иметь репл, в котором запуск приложения тоже почти моментальный (просто выполняется без оптимизаций).

он в СТ 80% времени тратит на рефакторинг и только 20% на написание нового кода

Очень забавно, но иногда мне кажется, что я 100% времени трачу на рефакторинг: мне часто дают задачи взять кусок кода написанный ранее левой пяткой и сделать его быструю и надежную версию к тому же добавив новый функционал.
Но свой код я рефачу достаточно редко:)

Берёте и добавляете, Python или хорошый JS читается как книга

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

Берёте и меняете… если вы меняете поведение функции то вы либо создаёте новую функцию с новым именем либо (что геморойно и зачем такое вообще?) бегаете поиском по проекту и меняете поведени, но вообще это моветон такое делать не соглосовав с коллегами.


Ну вот допустим стал ты перформенс инженером, сидишь профилируешь код: видишь какие-то умники наслушались лекций про ФП и везде возвращают значения обернутые в Optional и один такой метод горячий прям реально нагружает GC, т.к. где ФП, а где джава. Идешь и меняешь метод, он, скажем, теперь возвращает не Optional, а String но может и null теперь вернуть. Коллега, который этот метод написал давно уволился, а те кто им пользовались половина либо уволились либо стали менеджерами и забыли как программировать. Другие же трогали его год назад и уже забыли где и почему. А тимлид хочет ступеньку на графике таймингов уже завтра. Вот в такой ситуации без типов будет очень больно.
Очень забавно, но иногда мне кажется, что я 100% времени трачу на рефакторинг: мне часто дают задачи взять кусок кода написанный ранее левой пяткой и сделать его быструю и надежную версию к тому же добавив новый функционал.
Но свой код я рефачу достаточно редко:)

Такое и у меня бывает, но редко как правило пишу новый хендлер или новый плагин к системе. Если кто то написал совсем кривой хендлер/плагин то его может быть и впрямь проще написать с 0.


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

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


(на остальное по позже отвечу)

Ну вот допустим стал ты перформенс инженером, сидишь профилируешь код: видишь какие-то умники наслушались лекций про ФП и везде возвращают значения обернутые в Optional и один такой метод горячий прям реально нагружает GC, т.к. где ФП, а где джава. Идешь и меняешь метод, он, скажем, теперь возвращает не Optional, а String но может и null теперь вернуть. Коллега, который этот метод написал давно уволился, а те кто им пользовались половина либо уволились либо стали менеджерами и забыли как программировать. Другие же трогали его год назад и уже забыли где и почему. А тимлид хочет ступеньку на графике таймингов уже завтра. Вот в такой ситуации без типов будет очень больно.

Не очень корректный пример так как Optional это свойство СТ языков и в ДТ у вас такой вопросс просто не встанет.

Ну это не часть языка а с боку прикреплённая конструкция, работающая только в runtime.

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

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

Ковариантность и контравариантность вообще нормально/внятно хотя бы в одном ЯП сделаны? В каком?

По идее должно хорошо и просто работать в ФП языках так как там нет мутаций. Если разрешать мутации, то сразу много выплывает тонких кейсов, так что всё равно придется какие-то корректные вещи запрещать.
Ну вот например удалять из коллекции можно всегда — это не приведёт к проблемам типизации. Это на самом деле чудовищная тема если начать её копать, особенно если отойти от абстрактных вещей к конкретным. Я советую для разминки потратить где-то пол часа жизни что бы разобраться почему метод HashMap.get в Rust принимает не тип ключа K, а новый тип Q, который такой что
K: Borrow<Q> 
и
Q: Hash + Eq

doc.rust-lang.org/std/collections/struct.HashMap.html#method.get
По идее должно хорошо и просто работать в ФП языках так как там нет мутаций.

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


Впрочем, ради полноты — в обмазанном типами ФП есть своя ко(нтра)вариантность на уровне параметрического полиморфизма. Пример, отсюда: если даны


g :: ((forall a. a -> a) -> R) -> S
f :: (Int -> Int) -> R

то должно ли g f типизироваться?

Встречный вопрос: а что вы понимаете под "внятно" и в каких языках они сделаны невнятно?

Eiffel?

Заметил случайно...


Правильный же способ, минимизирующий логические ошибки, это создать три класса: RemovedColumn, AddedColumn и TypeChanged. Стоит унаследовать их от общего класса SchemaDiff, чтобы было удобнее обрабатывать их вместе.

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

НЕТ. Это никакой не АТД.


Главное отличие от АТД — в том, что у АТД внешняя расширяемость по операциям, а у подобных иерархий классов — по вариантам данных.


Иными словами, если у нас есть тип RemovedColumn | AddedColumn | TypeChanged, то мы в других модулях можем добавить любое число разбирающих этот тип функций, и в некоторых языках компилятор даже проверит, что мы не пропустили ни одного варианта. Но при этом в другом модуле мы не можем добавить конструктор TableAdded для этого типа.


А если у нас есть абстрактный класс SchemaDiff, то в другом модуле мы можем добавить наследника под названием TableAdded, но при этом если в базовом классе была только toString — то и никаких других операций определить над типом SchemaDiff не получится.

Обратите внимание на аккуратность формулировки «здесь я фактически руками реализую алгебраический тип данных» целых две оговорки в предложении.
Понятно что в Java нет АТД и нет синтаксических возможностей его выразить (но кстати в новых версиях там есть sealed классы, что бы нельзя было объявлять новых наследников, но я решил писать для статьи на Java 11). Здесь происходит такого же рода симуляция, как например люди делали в свое время ООП на чистом С, тоже корявое и рукописное.

Действительно классы и АТД удобно расширяются в разные стороны, но здесь мы ничего не расширяем и отличие как бы не заметно. Я думал об этом написать, но тогда пришлось бы объяснять double dispatch и всякие tagless final. Но не стал, т.к. во-первых статья уже огромная, во-вторых её тема это не «пишем ФП на Java». Ну и цель примера показать как можно убрать логику в декларации, а то что оно похоже на АТД в некотором роди приятный бонус.
Извините, если мои слова покажутся еретическими, но мне кажется, что со статической типизацией мы привносим состояния для данных в явном виде, т.е. наши функции переводят данные из одного состояния (типа) в другой. Этакий КА навыворот.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.