Какие функциональные возможности позволяет динамическая типизация? [закрыто]

91

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

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

Justin984
источник
5
Теоретически в этом нет ничего невозможного, если языки Turing Complete . Более интересный вопрос для меня, что легко или естественно в одном против другого. Есть вещи, которые я регулярно делаю в Python, которые я бы даже не рассматривал в C ++, хотя знаю, что это возможно.
Марк Рэнсом
28
Как пишет Крис Смит в своем превосходном эссе « Что нужно знать перед тем, как обсуждать системы типов» : «Проблема, в данном случае, заключается в том, что большинство программистов имеют ограниченный опыт и не пробовали много языков. Для контекста, здесь шесть или семь не считается «много» ... Два интересных следствия этого: (1) Многие программисты использовали очень плохие статически типизированные языки. (2) Многие программисты очень плохо использовали динамически типизированные языки ».
Даниэль Приден
3
@suslik: Если языковые примитивы имеют бессмысленные типы, то, конечно, вы можете делать бессмысленные вещи с типами. Это не имеет никакого отношения к разнице между статической и динамической типизацией.
Джон Перди
10
@CzarekTomczak: Да, это особенность некоторых динамически типизированных языков. Но язык со статической типизацией может быть изменяемым во время выполнения. Например, Visual Studio позволяет переписать код C #, находясь в точке останова в отладчике, и даже перемотать указатель инструкции, чтобы повторно выполнить код с новыми изменениями. Как я цитировал Криса Смита в моем другом комментарии: «Многие программисты использовали очень плохие языки со статической типизацией» - не судите все языки со статической типизацией по тем, которые вы знаете.
Даниэль Приден
11
@WarrenP: Вы утверждаете, что «системы динамических типов уменьшают количество лишних слов, которые я должен набирать», но затем вы сравниваете Python с C ++. Это несправедливое сравнение: конечно, C ++ более многословен, чем Python, но это не из-за различий в их системах типов, а из-за различий в их грамматике. Если вы просто хотите уменьшить количество символов в исходной программе, изучите J или APL: я гарантирую, что они будут короче. Более справедливым было бы сравнение Python с Haskell. (Для справки: я люблю Python и предпочитаю его C ++, но мне больше нравится Haskell.)
Даниэль Приден

Ответы:

50

Поскольку вы попросили конкретный пример, я дам вам один.

Роб Конери в Массивная ORM 400 строк кода. Это так мало, потому что Роб может отображать таблицы SQL и предоставлять результаты объекта, не требуя большого количества статических типов для зеркалирования таблиц SQL. Это достигается с помощью dynamicтипа данных в C #. На веб-странице Роба подробно описан этот процесс, но очевидно, что в данном конкретном случае динамическая типизация в значительной степени отвечает за краткость кода.

Сравните с Dapper Сэма Шафрона , который использует статические типы; SQLMapperв одиночку класс 3000 строк кода.

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


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

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

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

C # имеет dynamicключевое слово, которое позволяет вам отложить решение о типе до времени выполнения, не теряя преимущества статической безопасности типов в остальной части вашего кода. Тип inference ( var) устраняет большую часть трудностей написания на статически типизированном языке, устраняя необходимость всегда явно объявлять типы.


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

Роберт Харви
источник
22
Если бы я добавил две числовые строки вместе, я бы все равно не ожидал числового результата.
PDR
22
@ Роберт Я согласен с большинством твоего ответа. Однако обратите внимание, что существуют статически типизированные языки с интерактивными циклами read-eval-print, такие как Scala и Haskell. Может быть, C # просто не особо интерактивный язык.
Андрес Ф.
14
Нужно научить меня Haskell.
Роберт Харви
7
@RobertHarvey: Вы можете быть удивлены / впечатлены F #, если вы еще не пробовали это. Вы получаете всю безопасность типов (во время компиляции), которую вы обычно получаете на языке .NET, за исключением того, что вам редко когда-либо приходится объявлять какие-либо типы. Вывод типа в F # выходит за рамки того, что доступно / работает в C #. Кроме того: аналогично тому, на что указывают Андрес и Дэниел, F # Interactive является частью Visual Studio ...
Стивен Эверс
8
«Вы не собираетесь добавлять две строки вместе и ожидать числового ответа, если строки не содержат числовых данных, а если их нет, вы получите неожиданные результаты», извините, это не имеет ничего общего с динамической или статической типизацией , это сильно против слабой типизации.
vartec
26

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

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

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

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

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

Например, у меня есть скрипт Python, который должен запускаться как Python 2 и Python 3. Некоторые функции изменили свои типы параметров между Python 2 и 3, поэтому у меня есть такой код:

if sys.version_info[0] == 2:
    wfile.write(txt)
else:
    wfile.write(bytes(txt, 'utf-8'))

Средство проверки статического типа Python 2 будет отклонять код Python 3 (и наоборот), даже если он никогда не будет выполнен. Моя типобезопасная программа содержит статическую ошибку типа.

В качестве другого примера рассмотрим программу для Mac, которая хочет работать на OS X 10.6, но использовать преимущества новых функций в 10.7. Методы 10.7 могут существовать или не существовать во время выполнения, и я, программист, могу их обнаружить. Средство проверки статического типа вынуждено либо отклонить мою программу, чтобы обеспечить безопасность типов, либо принять программу вместе с возможностью создания ошибки типа (функция отсутствует) во время выполнения.

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

Вот еще одно ограничение:

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

Предполагая, что статические типы являются «правильными», предоставляет много возможностей для оптимизации, но эти оптимизации могут быть ограничивающими. Хорошим примером являются прокси-объекты, например, удаленное взаимодействие. Скажем, вы хотите иметь локальный прокси-объект, который перенаправляет вызовы методов к реальному объекту в другом процессе. Было бы хорошо, если бы прокси был универсальным (чтобы он мог маскироваться под любой объект) и прозрачным (чтобы существующий код не знал, что он разговаривает с прокси). Но для этого компилятор не может сгенерировать код, который предполагает, что статические типы являются правильными, например, путем статически встроенных вызовов методов, потому что это не удастся, если объект на самом деле является прокси.

Примеры такого удаленного взаимодействия в действии включают NSXPCConnection ObjC или TransparentProxy C # (реализация которого потребовала нескольких пессимизаций во время выполнения - см. Здесь для обсуждения).

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

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

ridiculous_fish
источник
2
«Средство проверки статического типа в Python 2 отклоняет код Python 3 (и наоборот), даже если он никогда не будет выполнен. Моя программа, безопасная для типов, содержит ошибку статического типа». Похоже, что вам действительно нужно, есть какое-то «статическое if», когда компилятор / интерпретатор даже не видит код, если условие ложно.
Дэвид Стоун
@davidstone, который существует в c ++
Milind R
A Python 2 static type checker would reject the Python 3 code (and vice versa), even though it would never be executed. My type safe program contains a static type error. На любом приемлемом статическом языке вы можете сделать это с помощью IFDEFоператора препроцессора типа, сохраняя при этом безопасность типов в обоих случаях.
Мейсон Уилер
1
@MasonWheeler, davidstone Нет, трюки препроцессора и static_if слишком статичны. В моем примере я использовал Python2 и Python3, но это могло быть просто как AmazingModule2.0 и AmazingModule3.0, где некоторые версии менялись между версиями. Самое раннее, что вы можете знать об интерфейсе, это время импорта модуля, которое обязательно во время выполнения (по крайней мере, если у вас есть желание поддерживать динамическое связывание).
ridiculous_fish
18

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

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

>>> d = JSON.parse(foo)
>>> d['bar'][3]
12
>>> d['baz']['qux']
'quux'

Итак, какой тип JSON.parseвозвращает? Словарь массивов целых или словарей строк? Нет, даже этого недостаточно.

JSON.parseдолжен возвращать какое-то «вариантное значение», которое может быть нулевым, bool, float, string, массивом любого из этих типов рекурсивно или словарем из строки в любой из этих типов рекурсивно. Основные сильные стороны динамической типизации проистекают из наличия таких типов вариантов.

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

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

оборота abarnert
источник
21
Ваш пример синтаксического анализа JSON может быть легко обработан статически с помощью алгебраического типа данных.
2
ОК, мой ответ не был достаточно ясен; Благодарю. Это JSValue является явным определением динамического типа, именно то, о чем я говорил. Полезны динамические типы, а не языки, требующие динамической типизации. Тем не менее, по-прежнему актуально, что динамические типы не могут быть автоматически сгенерированы какой-либо реальной системой вывода типов, в то время как в большинстве распространенных примеров люди тривиально выводимы. Я надеюсь, что новая версия объясняет это лучше.
августа
4
@MattFenwick Алгебраические типы данных в значительной степени ограничены функциональными языками (на практике). А как насчет языков, таких как Java и C #?
spirc
4
ADT существуют в C / C ++ как теговые объединения. Это не уникально для функциональных языков.
Кларк Гебель
2
@spirc вы можете эмулировать ADT на классическом языке OO, используя несколько классов, которые все происходят из общего интерфейса, вызовы во время выполнения getClass () или GetType () и проверки на равенство. Или вы можете использовать двойную диспетчеризацию, но я думаю, что это окупается больше в C ++. Таким образом, у вас может быть интерфейс JSObject и классы JSString, JSNumber, JSHash и JSArray. Затем вам потребуется некоторый код, чтобы превратить эту «нетипизированную» структуру данных в структуру данных «типизированная приложением». Но вы, вероятно, захотите сделать это и на языке с динамической типизацией.
Даниэль Янковский
12

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

Я приведу пример. Предположим, что вы реализуете простую модель данных для описания объектов данных, их коллекций и т. Д., Которая статически типизирована в том смысле, что, если модель говорит, что атрибут xобъекта типа Foo содержит целое число, он всегда должен содержать целое число. Поскольку это конструкция времени выполнения, вы не можете набирать ее статически. Предположим, вы храните данные, описанные в файлах YAML. Вы создаете хеш-карту (которая будет передана в библиотеку YAML позже), получаете xатрибут, сохраняете его на карте, получаете другой атрибут, который так же бывает строкой, ... держите секунду? Какой тип the_map[some_key]сейчас? Хорошо, стреляйте, мы знаем, что some_keyэто так, 'x'и поэтому результат должен быть целым числом, но система типов не может даже начать рассуждать об этом.

Некоторые активно исследуемые системы типов могут работать для этого конкретного примера, но они чрезвычайно сложны (как для разработчиков компиляторов, так и для программистов), особенно для чего-то такого «простого» (я имею в виду, я только что объяснил это в одном параграф).

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

user7043
источник
Общие типы не имеют требования к боксу.
Роберт Харви
@RobertHarvey Да. Я не говорил о боксе в Java C #, я говорил о том, чтобы «обернуть его в некоторый класс-обертку, единственная цель которого - представить значение T в подтипе U». Параметрический полиморфизм (то, что вы называете универсальной типизацией), однако, не относится к моему примеру. Это абстракция во время компиляции над конкретными типами, но нам нужен механизм типизации во время выполнения.
Возможно, стоит отметить, что система типов Scala является полной по Тьюрингу. Таким образом, системы типов могут быть менее тривиальными, чем вы себе представляете.
Андреа
@ Андреа Я намеренно не сводил свое описание к полноте Тьюринга. Вы когда-нибудь были запрограммированы в тарпинге Тьюринга? Или пытались закодировать эти вещи в типы? В какой-то момент это становится слишком сложным, чтобы быть осуществимым.
@delnan Я согласен. Я просто указывал, что системы типов могут делать довольно сложные вещи. У меня сложилось впечатление, что ваш ответ означал, что система типов может выполнять только тривиальную проверку, но во втором чтении вы не написали ничего подобного!
Андреа
7

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

Краткий пример на Haskell:

data Data = DString String | DInt Int | DDouble Double

-- defining a '+' operator here, with explicit promotion behavior
DString a + DString b = DString (a ++ b)
DString a + DInt b = DString (a ++ show b)
DString a + DDouble b = DString (a ++ show b)
DInt a + DString b = DString (show a ++ b)
DInt a + DInt b = DInt (a + b)
DInt a + DDouble b = DDouble (fromIntegral a + b)
DDouble a + DString b = DString (show a ++ b)
DDouble a + DInt b = DDouble (a + fromIntegral b)
DDouble a + DDouble b = DDouble (a + b)

В достаточном количестве случаев вы можете реализовать любую данную динамическую систему типов.

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

Изменить: я хотел бы сделать это простым, но вот больше деталей об объектной модели

Функция принимает список данных в качестве аргументов и выполняет вычисления с побочными эффектами в ImplMonad и возвращает данные.

type Function = [Data] -> ImplMonad Data

DMember является либо значением члена, либо функцией.

data DMember = DMemValue Data | DMemFunction Function

Расширьте, Dataчтобы включить объекты и функции. Объекты - это списки именованных членов.

data Data = .... | DObject [(String, DMember)] | DFunction Function

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

NovaDenizen
источник
Это совсем не одно и то же, потому что вы не можете добавлять новые типы, не пересмотрев определение Data.
Джед
5
В вашем примере вы смешиваете понятия динамической типизации со слабой типизацией. Динамическая типизация - это работа с неизвестными типами, а не определение списка разрешенных типов и операции перегрузки между ними.
hcalves
2
@Jed После того, как вы реализовали объектную модель, фундаментальные типы и примитивные операции, никаких других основ не требуется. Вы можете легко и автоматически переводить программы на оригинальном динамическом языке на этот диалект.
NovaDenizen
2
@hcalves Поскольку вы имеете в виду перегрузку в моем коде на Haskell, я подозреваю, что вы не совсем поняли его семантику. Там я определил новый +оператор, который объединяет два Dataзначения в другое Dataзначение. Dataпредставляет стандартные значения в динамической системе типов.
NovaDenizen
1
@Jed: большинство динамических языков имеют небольшой набор «примитивных» типов и некоторый индуктивный способ введения новых значений (структуры данных, такие как списки). Схема, например, довольно далеко заходит только с атомами, парами и векторами. Вы должны быть в состоянии реализовать их так же, как и остальные элементы данного динамического типа.
Тихон Джелвис
3

Мембраны :

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

введите описание изображения здесь

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

Майк Самуэль
источник
4
Да, Haskell действительно может для этого использовать экзистенциальные типы. Если у вас есть некоторый класс типов Foo, вы можете создать оболочку для любого типа, создающего экземпляр этого интерфейса. class Foo a where ... data Wrapper = forall a. Foo a => Wrapper a
Джейк Макартур
@JakeMcArthur, спасибо за объяснение. Это еще одна причина для меня, чтобы сесть и выучить Haskell.
Майк Сэмюэль
2
Ваша мембрана является «интерфейсом», а типы объектов «экзистенциально типизированы», то есть мы знаем, что они существуют под интерфейсом, но это все, что мы знаем. Экзистенциальные типы абстракции данных известны с 80-х годов. Хорошая ссылка - cs.cmu.edu/~rwh/plbook/book.pdf глава 21.1
Дон Стюарт,
@DonStewart. Являются ли прокси-классы Java механизмом экзистенциального типа? Единственное место, где мембраны становятся трудными, это языки с системами номинальных типов, в которых имена конкретных типов видны вне определения этого типа. Например, нельзя обернуть, Stringтак как это конкретный тип в Java. Smalltalk не имеет этой проблемы, потому что он не пытается печатать #doesNotUnderstand.
Майк Сэмюэль
1

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

Лучший вопрос - почему динамическая типизация более подходит и более уместна в определенных ситуациях и проблемах?

Во-первых, давайте определимся

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

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

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

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

Статическая типизация - поведение всех объектов в вашей программе проверяется во время компиляции, прежде чем код будет запущен. Это означает, что если вы хотите, например, чтобы ваша сущность типа Person имела поведение (чтобы вести себя как) Magician, вам нужно было бы определить сущность MagicianPerson и присвоить ей поведение мага, такого как throwMagic (). Если вы в своем коде, ошибочно скажете обычному компилятору Person.throwMagic () скажет вам"Error >>> hell, this Person has no this behavior, dunno throwing magics, no run!".

Динамическая типизация - в средах динамической типизации доступное поведение объектов не проверяется, пока вы действительно не попытаетесь что-то сделать с определенным объектом. Выполнение кода Ruby, который запрашивает Person.throwMagic (), не будет перехвачено, пока ваш код действительно не появится там. Это звучит расстраивающе, не так ли? Но это звучит откровенно. На основании этого свойства вы можете делать интересные вещи. Например, предположим, вы разрабатываете игру, в которой все может превратиться в Волшебника, и вы на самом деле не знаете, кто это будет, пока не дойдете до определенного момента в коде. А потом приходит Лягушка и ты говоришьHeyYouConcreteInstanceOfFrog.include Magicи с тех пор эта Лягушка становится одной конкретной Лягушкой, обладающей магическими способностями. Других лягушек до сих пор нет. Видите ли, в языках статической типизации вы должны были бы определить это отношение с помощью некоторого стандартного среднего значения комбинации поведения (например, реализация интерфейса). В языке динамической типизации вы можете сделать это во время выполнения, и никто не будет заботиться.

Большинство языков динамической типизации имеют механизмы, обеспечивающие общее поведение, которое будет перехватывать любое сообщение, передаваемое их интерфейсу. Например, Ruby method_missingи PHP, __callесли я хорошо помню. Это означает, что вы можете делать любые интересные вещи во время выполнения программы и принимать решение о типе на основе текущего состояния программы. Это дает инструменты для моделирования проблемы, которые намного более гибки, чем, скажем, в консервативном статическом языке программирования, таком как Java.

ivanjovanovic
источник