Как статическая типизация действительно полезна в больших проектах?

9

Любопытствуя на главной странице сайта языка скриптового программирования, я натолкнулся на этот отрывок:

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

Это заставило меня вспомнить, что во многих религиозных войнах между статическими, скомпилированными языками (например, Java) и динамическими интерпретируемыми языками (в основном Python, потому что он более используется, но это «проблема», разделяемая большинством языков сценариев), одна из жалоб на статичность Поклонники типизированных языков по сравнению с динамически типизированными языками заключаются в том, что они плохо масштабируются для больших проектов, потому что «однажды вы забудете тип возврата функции и вам придется искать его, в то время как со статически типизированными языками все явно объявлено ".

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

Кроме того, так как функции объявляются с type funcname()..., без знания typeтого, что вам придется искать по каждой строке, в которой вызывается функция, потому что вы знаете funcname, в то время как в Python и т. П. Вы можете просто искать def funcnameили, function funcnameчто происходит только один раз, в декларация.

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

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

user6245072
источник
2
если вы прочитаете ответы на другой вопрос, вы, вероятно, получите ответы, которые вам нужны для этого, они в основном задают одно и то же с разных точек зрения :)
Сара
1
Swift и игровые площадки являются ответом на статически типизированный язык.
daven11
2
Языки не компилируются, реализации есть. Способ написания REPL для «скомпилированного» языка - это написать что-то, что может интерпретировать язык, или, по крайней мере, компилировать и выполнять его построчно, сохраняя необходимое состояние. Также Java 9 будет поставляться с REPL.
Себастьян Редл
2
@ user6245072: Вот как сделать REPL для переводчика: прочитать код, отправить его переводчику, распечатать результат. Вот как сделать REPL для компилятора: прочитать код, отправить его компилятору, запустить скомпилированный код , распечатать результат. Проще простого. Это именно то, что делают FSi (F♯ REPL), GHCi (GHC Haskell's REPL), Scala REPL и Cling.
Йорг Миттаг

Ответы:

21

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

Это не тривиально. Это не тривиальный вообще . Это тривиально только для тривиальных функций.

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

getAnswer(v) {
 return v.answer
}

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

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

getAnswer(x, y) {
   if (x + y.answer == 13)
       return 1;
   return "1";
}

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

Кроме того, поскольку функции объявляются с типом funcname () ..., без знания типа вам придется искать по каждой строке, в которой вызывается функция, потому что вы знаете только funcname, тогда как в Python и тому подобное вы можете просто ищите def funcname или функцию funcname, которая происходит только один раз, в объявлении.

В статически типизированных языках есть вещи, называемые «инструментами». Это программы, которые помогают вам делать что-то с вашим исходным кодом. В этом случае, я бы просто щелкнул правой кнопкой мыши и пошел к определению, благодаря Resharper. Или используйте сочетание клавиш. Или просто наведите курсор мыши, и он скажет мне, какие типы участвуют. Мне совершенно безразлично, как копать файлы. Текстовый редактор сам по себе является патетическим инструментом для редактирования исходного кода программы.

По памяти def funcnameне хватит в Python, так как функция может быть переназначена произвольно. Или может быть объявлен повторно в нескольких модулях. Или в классах. И т.п.

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

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

DeadMG
источник
2
Чтобы быть справедливым, эти «инструменты» были изобретены в динамических языках, и динамические языки имели их задолго до статических языков. Перейти к определению, дополнению кода, автоматизированному рефакторингу и т. Д. Существовали в графических IDE Lisp и Smalltalk еще до того, как статические языки имели даже графические или IDE, не говоря уже о графических IDE.
Йорг Миттаг
Знание возвращаемого типа функций не всегда говорит вам, какие функции ДЕЛАЮТ . Вместо написания типов вы могли бы написать тесты doc с примерами значений. например, сравните (words 'some words oue') => ['some', 'words', 'oeu'] с (словами string) -> [string], (zip {abc} [1..3]) => [(a, 1), (b, 2), (c, 3)] с его типом.
aoeu256
18

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

getAnswer(v) {
 return v.answer
}

Что на земле это делает? Что v? Откуда этот элемент answer?

getAnswer(v : AnswerBot) {
  return v.answer
}

Теперь у нас есть больше информации -; это нуждается в типе AnswerBot.

Если мы перейдем на язык классов, мы можем сказать,

class AnswerBot {
  var answer : String
  func getAnswer() -> String {
    return answer
  }
}

Теперь мы можем иметь переменную типа AnswerBotи вызывать метод, getAnswerи каждый знает, что он делает. Любые изменения фиксируются компилятором до того, как будет выполнено любое тестирование во время выполнения. Есть много других примеров, но, возможно, это дает вам идею?

daven11
источник
1
Выглядит уже понятнее - если только вы не укажете, что у такой функции нет причин существовать, но это, конечно, просто пример.
user6245072
Вот в чем проблема, когда у вас есть несколько программистов в большом проекте, такие функции существуют (и того хуже), это ночные кошмары. Также обратите внимание, что функции в динамических языках находятся в глобальном пространстве имен, поэтому со временем у вас может появиться пара функций getAnswer - и они обе работают, и они разные, потому что они загружаются в разное время.
daven11
1
Я полагаю, что это неправильное понимание функционального программирования. Однако что вы имеете в виду, говоря, что они находятся в глобальном пространстве имен?
user6245072
3
«функции в динамических языках по умолчанию находятся в глобальном пространстве имен», это специфическая деталь языка, а не ограничение, вызванное наличием динамической типизации.
Сара
2
@ daven11 «Я думаю, здесь есть javascript», но другие динамические языки имеют действительные пространства имен / модули / пакеты и могут предупреждать вас о переопределениях. Вы можете быть слишком обобщенным.
coredump
10

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

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

Большинство людей, работающих со статически типизированными языками, используют либо IDE для языка, либо интеллектуальный редактор (например, vim или emacs), который интегрирован с инструментами для конкретного языка. В таких инструментах обычно есть быстрый способ найти тип функции. Например, в Eclipse для проекта Java существует два способа, как обычно вы можете найти тип метода:

  • Если я хочу использовать метод для объекта, отличного от «this», я печатаю ссылку и точку (например someVariable.), и Eclipse ищет тип someVariableи предоставляет раскрывающийся список всех методов, определенных в этом типе; когда я прокручиваю вниз список, тип и документация каждого из них отображаются, пока он выбран. Обратите внимание, что это очень трудно сделать с помощью динамического языка, потому что редактору трудно (или в некоторых случаях невозможно) определить тип этого типа someVariable, поэтому он не может легко сгенерировать правильный список. Если я хочу использовать метод, thisя могу просто нажать Ctrl + пробел, чтобы получить тот же список (хотя в этом случае это не так сложно достичь для динамических языков).
  • Если у меня уже есть ссылка на конкретный метод, я могу навести на нее курсор мыши, и во всплывающей подсказке отображается тип и документация для метода.

Как вы можете видеть, это несколько лучше, чем типичный инструментарий, доступный для динамических языков (не то, что это невозможно в динамических языках, поскольку у некоторых есть довольно хорошая функциональность IDE - smalltalk - это тот, который приходит на ум, - но сложнее для динамический язык и, следовательно, менее вероятно, будет доступен).

Кроме того, поскольку функции объявляются с типом funcname () ..., без знания типа вам придется искать по каждой строке, в которой вызывается функция, потому что вы знаете только funcname, тогда как в Python и тому подобное вы можете просто ищите def funcname или функцию funcname, которая происходит только один раз, в объявлении.

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

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

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

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

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

Скорее всего, даже если вам нужно это сделать, вам не придется перекомпилировать все . Большинство современных статических языков имеют инкрементные компиляторы, которые будут компилировать только небольшую часть вашего кода, которая изменилась, так что вы можете получить почти мгновенную обратную связь для ошибок типа, если вы их сделаете. Например, Eclipse / Java будет выделять ошибки типа, пока вы их еще печатаете .

Жюль
источник
4
You seem to have a few misconceptions about working with large static projects that may be clouding your judgement.Ну, мне всего 14 лет, и я программирую на Android меньше года, так что это возможно, я думаю.
user6245072
1
Даже без IDE, если вы удаляете метод из класса в Java и есть вещи, которые зависят от этого метода, любой компилятор Java предоставит вам список каждой строки, которая использовала этот метод. В Python происходит сбой, когда исполняемый код вызывает отсутствующий метод. Я регулярно использую как Java, так и Python, и я люблю Python за то, как быстро вы можете запускать вещи, и за классные вещи, которые вы можете делать, которые Java не поддерживает, но реальность такова, что у меня есть проблемы в программах на Python, которых просто не бывает с (прямо) Ява. Рефакторинг в частности гораздо сложнее в Python.
JimmyJames
6
  1. Потому что статические контролеры проще для статически типизированных языков.
    • Как минимум, без возможностей динамического языка, если он компилируется, то во время выполнения нет неразрешенных функций. Это часто встречается в проектах ADA и C на микроконтроллерах. (Микроконтроллерные программы иногда становятся большими ... например, сотни клок.)
  2. Статические проверки ссылок на компиляцию являются подмножеством инвариантов функций, которые на статическом языке также могут быть проверены во время компиляции.
  3. Статические языки обычно имеют большую ссылочную прозрачность. В результате новый разработчик может погрузиться в один файл и понять, что происходит, исправить ошибку или добавить небольшую функцию, не зная всех странных вещей в базе кода.

Сравните с скажем, javascript, Ruby или Smalltalk, где разработчики переопределяют функциональность основного языка во время выполнения. Это усложняет понимание большого проекта.

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

Как ни странно, у моего знакомого есть безопасное программирование «Работа для жизни» на Лиспе. Никто, кроме команды, не может понять кодовую базу.

Тим Виллискрофт
источник
Anecdotally, an acquaintance of mine has a secure "Job For Life" programming in Lisp. Nobody except the team can understand the code-base.Это действительно настолько плохо? Разве персонализация, которую они добавили, не помогает им быть более продуктивными?
user6245072
@ user6245072 Это может быть преимуществом для людей, которые там сейчас работают, но это затрудняет набор новых людей. Требуется больше времени, чтобы найти человека, который уже знает неосновной язык, или научить его тому, кого он уже не знает. Это может затруднить масштабирование проекта в случае его успеха или восстановление после колебаний - люди действительно уходят, получают повышение на другие должности ... Через некоторое время это также может стать недостатком для самих специалистов - как только вы написали какой-нибудь язык в течение десяти лет или около того, может быть трудно перейти к чему-то новому.
Халк
Разве вы не можете просто использовать трассировщик для создания модульных тестов из запущенной программы на Лиспе? Как и в Python, вы можете создать декоратор (adverb) с именем print_args, который принимает функцию и возвращает модифицированную функцию, которая выводит свой аргумент. Затем вы можете применить его ко всей программе в sys.modules, хотя более простой способ сделать это - использовать sys.set_trace.
aoeu256
@ aoeu256 Я не знаком с возможностями среды выполнения Lisp. Но они активно использовали макросы, поэтому ни один нормальный программист на lisp не мог прочитать код; Вероятно, что попытка сделать «простые» вещи во время выполнения не может работать из-за макросов, меняющих все в Лиспе.
Тим
@TimWilliscroft Вы можете подождать, пока все макросы не будут расширены, прежде чем делать подобные вещи. Emacs имеет много горячих клавиш, чтобы позволить вам встроенные макросы расширения (и, возможно, встроенные функции).
aoeu256
4

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

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

Кроме того, поскольку функции объявляются с типом funcname()..., без знания типа вам придется искать по каждой строке, в которой вызывается функция, потому что вы знаете funcname, в то время как в Python и т. П. Вы можете просто искать def funcnameили function funcnameчто происходит только один раз. На декларации.

Это вопрос синтаксиса, который совершенно не связан со статической типизацией.

Синтаксис семейства C действительно недружелюбен, когда вы хотите посмотреть объявление, не имея в своем распоряжении специализированных инструментов. Другие языки не имеют этой проблемы. Смотрите синтаксис объявления Rust:

fn funcname(a: i32) -> i32

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

Любой язык может быть переведен, и любой язык может иметь REPL.


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

Я отвечу абстрактно.

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

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

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

Я хотел бы разделить предположения на два вида.

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

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

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

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

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

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

Теодорос Чатзигианнакис
источник
3

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

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

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

gbjbaanb
источник
Вам не нужно делать статический вывод типов (pylint), вы можете делать динамический вывод типов chrislaffra.blogspot.com/2016/12/…, который также делается JIT-компилятором PyPy. Существует также другая версия вывода динамических типов, когда компьютер случайным образом размещает фиктивные объекты в аргументах и ​​видит, что вызывает ошибку. Проблема остановки не имеет значения в 99% случаев, если вы тратите слишком много времени, просто останавливаете алгоритм (именно так Python обрабатывает бесконечную рекурсию, у него есть предел рекурсии, который можно установить).
aoeu256