Существуют ли шаблоны проектирования, которые возможны только в динамически типизированных языках, таких как Python?

30

Я прочитал соответствующий вопрос. Существуют ли какие-либо шаблоны проектирования, которые не нужны в динамических языках, таких как Python? и запомнил эту цитату на Wikiquote.org

Замечательная вещь о динамической типизации - она ​​позволяет вам выражать все, что можно вычислить. А системы типов - системы типов, как правило, разрешимы, и они ограничивают вас подмножеством. Люди, которые предпочитают статические системы типов, говорят: «Это хорошо, это достаточно хорошо; все интересные программы, которые вы хотите написать, будут работать как типы ». Но это смешно - если у вас есть система типов, вы даже не знаете, какие там интересные программы.

--- Радио-Эпизод 140 по программной инженерии: Newspeak и сменные типы с Gilad Bracha

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

user7610
источник
3
Я обнаружил, что двойную диспетчеризацию и шаблон Visitor очень трудно выполнить в статически типизированных языках, но легко выполнимо в динамических языках. Посмотрите этот ответ (и вопрос), например: programmers.stackexchange.com/a/288153/122079
user3002473
7
Конечно. Например, любой шаблон, который включает создание новых классов во время выполнения. (это также возможно в Java, но не в C ++; есть скользящая шкала динамизма).
user253751
1
Это будет во многом зависеть от того, насколько сложна ваша система типов :-) Функциональные языки обычно очень хороши в этом.
Берги
1
Кажется, что все говорят о системах типов, таких как Java и C # вместо Haskell или OCaml. Язык с мощной системой типов может быть таким же лаконичным, как динамический язык, но сохранять безопасность типов.
Эндрю говорит восстановить Монику
@immibis Это неправильно. Системы статических типов могут абсолютно создавать новые «динамические» классы во время выполнения. См. Главу 33 «Практические основы языков программирования».
садовод

Ответы:

4

Первоклассные типы

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

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

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

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

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

Узоры

Шаблоны, основанные на динамических типах, являются шаблонами, которые включают динамические среды: открытые классы, диспетчеризацию, базы данных объектов в памяти, сериализацию и т. Д. Работают простые вещи, такие как универсальные контейнеры, потому что вектор не забывает во время выполнения о типе объектов, которые он содержит. (нет необходимости в параметрических типах).

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

;;
;; Fetching systems, installing them, etc. 
;; ASDF and QL provide provide resp. a Make-like facility 
;; and system management inside the runtime: those are
;; not distinct programs.
;; Reflexivity allows to develop dedicated tools: for example,
;; being able to find the transitive reduction of dependencies
;; to parallelize builds. 
;; https://gitlab.common-lisp.net/xcvb/asdf-dependency-grovel
;;
(ql:quickload 'trivial-timeout)

;;
;; Readtables are part of the runtime.
;; See also NAMED-READTABLES.
;;
(defparameter *safe-readtable* (copy-readtable *readtable*))
(set-macro-character #\# nil t *safe-readtable*)
(set-macro-character #\: (lambda (&rest args)
                           (declare (ignore args))
                           (error "Colon character disabled."))
                     nil
                     *safe-readtable*)

;; eval-when is necessary when compiling the whole file.
;; This makes the result of the form available in the compile-time
;; environment. 
(eval-when (:compile-toplevel :load-toplevel :execute)
  (defvar +WHITELISTED-LISP-SYMBOLS+ 
    '(+ - * / lambda labels mod rem expt round 
      truncate floor ceiling values multiple-value-bind)))

;;
;; Read-time evaluation #.+WHITELISTED-LISP-SYMBOLS+
;; The same language is used to control the reader.
;;
(defpackage :sandbox
  (:import-from
   :common-lisp . #.+WHITELISTED-LISP-SYMBOLS+)
  (:export . #.+WHITELISTED-LISP-SYMBOLS+))

(declaim (inline read-sandbox))

(defun read-sandbox (stream &key (timeout 3))
  (declare (type (integer 0 10) timeout))
  (trivial-timeout:with-timeout (timeout)
    (let ((*read-eval* nil)
          (*readtable* *safe-readtable*)
          ;;
          ;; Packages are first-class: no possible name collision.
          ;;
          (package (make-package (gensym "SANDBOX") :use '(:sandbox))))
      (unwind-protect
           (let ((*package* package))
             (loop
                with stop = (gensym)
                for read = (read stream nil stop)
                until (eq read stop)
                ;;
                ;; Eval at runtime
                ;;
                for value = (eval read)
                ;;
                ;; Type checking
                ;;
                unless (functionp value)
                do (error "Not a function")
                ;; 
                ;; Compile at run-time
                ;;
                collect (compile nil value)))
        (delete-package package)))))

;;
;; Static type checking.
;; warning: Constant 50 conflicts with its asserted type (MOD 11)
;;
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in :timeout 50)))

;; get it right, this time
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in)))

#| /tmp/plugin.lisp
(lambda (x) (+ (* 3 x) 100))
(lambda (a b c) (* a b))
|#

(read-sandbox-file #P"/tmp/plugin.lisp")

;; 
;; caught COMMON-LISP:STYLE-WARNING:
;;   The variable C is defined but never used.
;;

(#<FUNCTION (LAMBDA (#:X)) {10068B008B}>
 #<FUNCTION (LAMBDA (#:A #:B #:C)) {10068D484B}>)

Ничто из вышеперечисленного не является «невозможным» для других языков. Подход с использованием плагинов в Blender, в музыкальных программах или IDE для статически скомпилированных языков, которые выполняют перекомпиляцию на лету и т. Д. Вместо внешних инструментов динамические языки предпочитают инструменты, которые используют информацию, которая уже есть. Все известные абоненты FOO? все подклассы BAR? все методы, которые специализированы по классу ZOT? это усвоенные данные. Типы являются еще одним аспектом этого.


(см. также: CFFI )

CoreDump
источник
39

Краткий ответ: нет, потому что эквивалентность по Тьюрингу.

Длинный ответ: этот парень тролль. Хотя системы типов «ограничивают вас подмножеством», это правда, но вещи вне этого подмножества, по определению, не работают.

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

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

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

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

Во-первых, потому что код без аннотаций типов трудно читать. Рассмотрим следующий Python:

def sendData(self, value):
   self.connection.send(serialize(value.someProperty))

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

Все зависит от структуры value.someProperty. Но как это выглядит? Хороший вопрос! Что звонит sendData()? Что это проходит? Как выглядит эта переменная? Откуда это? Если это не локально, вы должны проследить всю историю, valueчтобы отследить, что происходит. Может быть, вы передаете что-то еще, что также имеет somePropertyсвойство, но оно не делает то, что вы думаете, что делает?

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

def SendData(value as MyDataType):
   self.Connection.Send(Serialize(value.SomeProperty))

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

Вторая причина основана на первой: в большом и сложном проекте у вас, скорее всего, есть несколько участников. (А если нет, то вы создаете его самостоятельно в течение длительного времени, что, по сути, одно и то же. Попробуйте прочитать код, написанный 3 года назад, если не верите мне!) Это означает, что вы не знаете, что было проходя через голову человека, который написал почти любую конкретную часть кода в то время, когда они его написали, потому что вас там не было или вы не помните, был ли это ваш собственный код давным-давно. Наличие объявлений типов действительно помогает вам понять, каково было намерение кода!

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

Мейсон Уилер
источник
24
«Этот парень тролль». - Я не уверен, что атака ad hominem поможет вашему хорошо представленному делу. И хотя мне хорошо известно, что аргумент от авторитета является столь же плохой ошибкой, что и ad hominem, я все же хотел бы отметить, что Гилад Брача, вероятно, разработал больше языков и (что наиболее важно для этого обсуждения) больше систем статических типов, чем большинство. Небольшая выдержка: он является единственным дизайнером Newspeak, со-дизайнером Dart, соавтором Спецификации языка Java и Спецификации виртуальной машины Java, работал над дизайном Java и JVM, разработанным…
Йорг Миттаг
10
Strongtalk (система статических типов для Smalltalk), система типов Dart, система типов Newspeak, его докторская диссертация по модульности является основой практически каждой современной модульной системы (например, Java 9, ECMAScript 2015, Scala, Dart, Newspeak, Ioke , Seph), его статья о миксинах произвела революцию в том, как мы думаем о них. Теперь, это не значит , что он прав, но я действительно думаю , что, разработав несколько систем статического типа делает его немного больше , чем «троллей».
Йорг Миттаг,
17
«Хотя это правда, что системы типов« ограничивают вас подмножеством », вещи вне этого подмножества, по определению, являются вещами, которые не работают». - Это не правильно. Из результатов «Неразрешимость проблемы остановки», теоремы Райса и множества других результатов «Неразрешимость и невычислимость» мы знаем, что средство проверки статического типа не может решить для всех программ, являются ли они безопасными по типу или небезопасными по типу. Он не может принять эти программы (некоторые из которых небезопасны по типу), поэтому единственный разумный выбор - отклонить их (однако некоторые из них являются безопасными по типу). Кроме того, язык должен быть разработан в ...
Йорг Миттаг
9
… Таким образом, чтобы программист не мог писать эти неразрешимые программы, но, опять же, некоторые из них действительно безопасны для типов. Таким образом, независимо от того, как вы это делаете: программисту запрещено писать программы, безопасные для типов. А так как есть на самом деле бесконечно много из них ( как правило), мы можем быть почти уверены , что по крайней мере некоторые из них являются не только вещами , которые делают работу, но и полезно.
Йорг Миттаг,
8
@MasonWheeler: проблема остановки возникает постоянно, именно в контексте статической проверки типов. Если языки не разработаны тщательно, чтобы помешать программисту писать определенные виды программ, статическая проверка типов быстро становится эквивалентной решению проблемы остановки. Либо вы в конечном итоге с программами вы не можете писать , потому что они могут запутать проверки типов, или вы в конечном итоге с программами вы которые разрешается писать , но они принимают бесконечное количество времени , чтобы проверки типа.
Йорг Миттаг,
27

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

Например, я написал класс на python, который оборачивает элемент XML DOM и превращает его в объект первого класса. То есть вы можете написать код:

doc.header.status.text()

и у вас есть содержимое этого пути из проанализированного XML-объекта. вроде аккуратно и аккуратно, ИМО. И если нет головного узла, он просто возвращает фиктивные объекты, которые не содержат ничего, кроме фиктивных объектов (черепахи все время вниз). Нет реального способа сделать это, скажем, в Java. Вы должны были бы заранее скомпилировать класс, основанный на некотором знании структуры XML. Не говоря уже о том, хорошая ли это идея, такого рода вещи действительно меняют способ решения проблем на динамическом языке. Я не говорю, что это меняется таким образом, что всегда всегда лучше, однако. Есть определенные издержки для динамических подходов, и ответ Мейсона дает достойный обзор. Являются ли они хорошим выбором, зависит от многих факторов.

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

JimmyJames
источник
4
Вы не можете сделать это в Java, потому что Java плохо спроектирован. В C # это будет не так сложно IDynamicMetaObjectProvider, а в Boo - просто. ( Вот реализация в менее чем 100 строк, включенная как часть стандартного дерева исходников на GitHub, потому что это так просто!)
Мейсон Уилер
6
@MasonWheeler "IDynamicMetaObjectProvider"? Это связано с dynamicключевым словом C # ? ... который фактически просто привязывает динамическую типизацию к C #? Не уверен, что ваш аргумент верен, если я прав.
jpmc26
9
@MasonWheeler У тебя семантика. Не вдаваясь в споры о мелочах (здесь мы не разрабатываем математический формализм на SE), динамическая типизация - это практика предшествующих решений времени компиляции вокруг типов, особенно проверка того, что у каждого типа есть конкретные члены, к которым программа обращается. Это цель, которая dynamicдостигается в C #. «Отражение и поиск в словаре» происходят во время выполнения, а не во время компиляции. Я действительно не уверен, как вы можете сделать так, чтобы это не добавляло динамической типизации к языку. Я хочу сказать, что последний абзац Джимми охватывает это.
jpmc26
44
Несмотря на то, что я не являюсь большим поклонником Java, я также осмелюсь сказать, что называть Java «плохо спроектированным», в частности потому, что в нем не добавлена ​​динамическая типизация ... это слишком усердно.
jpmc26
5
Помимо немного более удобного синтаксиса, чем он отличается от словаря?
Теодорос Чатзигианнакис
10

Цитата верна, но тоже неискренна. Давайте разберемся, чтобы понять почему:

Замечательная вещь о динамической типизации - она ​​позволяет вам выражать все, что можно вычислить.

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

А системы типов - системы типов, как правило, разрешимы, и они ограничивают вас подмножеством.

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

Люди, которые предпочитают статические системы типов, говорят: «Это хорошо, это достаточно хорошо; все интересные программы, которые вы хотите написать, будут работать как типы ». Но это смешно - если у вас есть система типов, вы даже не знаете, какие там интересные программы.

Проблема в том, что языки динамического типа имеют статический тип. Иногда все является строкой, и чаще всего существует какой-либо теговый союз, где каждая вещь является либо сумкой свойств, либо значением типа int или double. Беда в том, что статические языки тоже могут это делать, исторически это было немного сложнее, но современные статически типизированные языки делают это почти так же просто, как и использование языков динамического типа, так как же может быть разница в что программист может увидеть как интересную программу? Статические языки имеют точно такие же помеченные объединения, как и другие типы.

Чтобы ответить на вопрос в заголовке: Нет, нет шаблонов проектирования, которые не могут быть реализованы на языке со статической типизацией, потому что вы всегда можете реализовать достаточно динамической системы, чтобы получить их. Могут быть шаблоны, которые вы получаете за «бесплатно» на динамическом языке; это может или не может стоить мириться с недостатками этих языков для YMMV .

JK.
источник
2
Я не совсем уверен, ответили ли вы просто да или нет. Звучит больше как нет для меня.
user7610
1
@TheodorosChatzigiannakis Да, как еще будут реализованы динамические языки? Во-первых, вы сочтете за архитектора астронавта, если вы когда-нибудь захотите внедрить систему динамических классов или что-то еще немного вовлеченное. Во-вторых, у вас, вероятно, нет ресурса, чтобы сделать его отлаживаемым, полностью интроспективным, производительным («просто используйте словарь» - это то, как медленные языки реализованы). В-третьих, некоторые динамические функции лучше всего использовать при интеграции во весь язык, а не просто в виде библиотеки: например , сборщик мусора ( GC являются библиотеками, но они не используются широко).
coredump
1
@ Theodoros Согласно статье, которую я здесь уже однажды связал, все, кроме 2,5% структур (в модулях Python, на которые смотрели исследования), можно легко выразить на типизированном языке. Возможно, 2,5% оправдывают затраты на динамическую печать. По сути, это был мой вопрос. neverworkintheory.org/2016/06/13/polymorphism-in-python.html
user7610
3
@JiriDanek Насколько я могу судить, ничто не мешает статически типизированному языку иметь полиморфные точки вызова и поддерживать статическую типизацию в процессе. Посмотрите Статическую Проверку Типа Мультиметодов . Может быть, я неправильно понимаю вашу ссылку.
Теодорос Чатзигианнакис,
1
«Язык с динамической типизацией позволяет вам выражать что угодно до тех пор, пока он завершен по Тьюрингу, а это большинство». Хотя это, конечно, верное утверждение, на самом деле оно не выполняется в «реальном мире», потому что объем текста написать может быть очень большим.
Даниэль Жур
4

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

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

Вы можете добавить новый метод в библиотечный класс Ruby и получить доступ к его закрытым полям. Могут быть случаи, когда такой взлом может быть полезен, но это будет нарушением инкапсуляции. (Я не против добавления методов, полагающихся только на общедоступный интерфейс, но это то, что статически типизированные методы расширения C # не могут сделать.)

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

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

В чем динамические языки хороши - это синтаксический сахар. Например, при чтении десериализованного объекта JSON вы можете ссылаться на вложенное значение просто как obj.data.article[0].content- намного точнее, чем сказать obj.getJSONObject("data").getJSONArray("article").getJSONObject(0).getString("content").

Разработчики Ruby особенно могли бы подробно рассказать о магии, которая может быть достигнута путем реализации method_missing, которая представляет собой метод, позволяющий обрабатывать попытки вызова необъявленных методов. Например, ActiveRecord ORM использует его, чтобы вы могли сделать вызов, User.find_by_email('joe@example.com')даже не объявляя find_by_emailметод. Конечно, ничего такого, чего нельзя достичь, как UserRepository.FindBy("email", "joe@example.com")в статически типизированном языке, но нельзя отрицать его аккуратность.

kamilk
источник
4
Конечно, есть вещи, которые вы можете делать только в статически типизированных языках. Но они не обязательно будут хорошим дизайном.
coredump
2
Суть синтаксического сахара очень мало связана с динамической типизацией и всем с синтаксисом.
оставлено около
@leftaroundabout Шаблоны имеют все, что связано с синтаксисом. Системы типов также имеют много общего с этим.
user253751
4

Шаблон Dynamic Proxy - это ярлык для реализации прокси-объектов без использования одного класса для каждого типа, который вам нужен для прокси.

class Proxy(object):
    def __init__(self, obj):
        self.__target = obj

    def __getattr__(self, attr):
        return getattr(self.__target, attr)

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

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

Жюль
источник
Я думаю, что я мог бы подражать этому в Go. Существует набор методов, которые должны иметь все прокси-объекты (в противном случае утка может не крякнуть и все развалится). Я могу создать интерфейс Go с помощью этих методов. Мне придется больше об этом думать, но я думаю, что то, что я имею в виду, сработает.
user7610
Вы можете сделать что-то похожее на любом .NET языке с RealProxy и дженериками.
LittleEwok
@LittleEwok - RealProxy использует генерацию кода во время выполнения - как я уже сказал, многие современные статические языки имеют обходной путь, подобный этому, но все еще проще в динамическом языке.
Жюль
Методы расширения C # похожи на исправление обезьян, сделанное безопасным. Вы не можете изменить существующие методы, но вы можете добавить новые.
Эндрю говорит восстановить Монику
3

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

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

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

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

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

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

C #, начиная с версии 4, поддерживает динамически типизированные объекты. Очевидно, что дизайнеры языка видят выгоду в том, что оба вида печати доступны. Но это также показывает, что вы не можете иметь свой пирог и есть я тоже: когда вы используете динамические объекты в C #, вы получаете возможность делать что-то вроде исправления обезьян, но вы также теряете статическую проверку типов для взаимодействия с этими объектами.

JacquesB
источник
+1 ваш второй до последнего абзаца, я думаю, является решающим аргументом. Я до сих пор утверждаю, что есть разница, хотя со статическими типами у вас есть полный контроль над тем, где и что вы можете сделать,
jk.
2

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

Да и нет.

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

Позвольте мне показать несколько примеров этого:

Map<Class<?>, Function<?, String>> someMap;
someMap.get(object.getClass()).apply(object);

Я знаю, что someMap.get(T.class)вернет Function<T, String>, из-за того, как я построил SomeMap. Но Java уверена только в том, что у меня есть функция.

Другой пример:

data = parseJSON(someJson)
validate(data, someJsonSchema);
print(data.properties.rowCount);

Я знаю, что data.properties.rowCount будет действительной ссылкой и целым числом, потому что я проверил данные по схеме. Если бы это поле отсутствовало, возникло бы исключение. Но компилятор может знать только, что он генерирует исключение или возвращает какой-то типовой JSONValue.

Другой пример:

x, y, z = struct.unpack("II6s", data)

«II6s» определяет способ, которым данные кодируют три переменные. Поскольку я указал формат, я знаю, какие типы будут возвращены. Компилятор будет знать только, что он возвращает кортеж.

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

Вот к чему приводит оригинальная цитата:

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

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

Однако вернемся к вашему вопросу:

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

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

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

Уинстон Эверт
источник
1
почему java - это точка отсечения, при которой вы не должны переходить на «более продвинутую / сложную систему типов»?
JK.
2
@jk, что заставляет тебя думать, что я это говорю? Я явно избегал сторонников того, стоит ли более продвинутая / сложная система типов.
Уинстон Эверт
2
Некоторые из них являются ужасными примерами, а другие, похоже, являются скорее языковыми решениями, чем типизированными, а не типизированными. Меня особенно смущает, почему люди думают, что десериализация настолько сложна в типизированных языках. Типизированный результат будет следующим: data = parseJSON<SomeSchema>(someJson); print(data.properties.rowCount); если у человека нет класса для десериализации, мы можем вернуться к data = parseJSON(someJson); print(data["properties.rowCount"]);- который все еще набран и выражает то же самое намерение.
NPSF3000
2
@ NPSF3000, как работает функция parseJSON? Казалось бы, использовать отражение или макросы. Как данные ["properties.rowCount"] могут быть набраны на статическом языке? Как он мог знать, что полученное значение является целым числом?
Уинстон Эверт
2
@ NPSF3000, как вы планируете использовать его, если вы не знаете его целое число? Как вы планируете зацикливание элементов в списке в JSON, не зная, что это массив? Суть моего примера в том, что я знал, что data.propertiesэто объект, и я знал, что data.properties.rowCountэто целое число, и я мог просто написать код, который их использовал. Ваше предложение data["properties.rowCount"]не обеспечивает то же самое.
Уинстон Эверт
1

Вот несколько примеров из Objective-C (динамически типизированных), которые невозможны в C ++ (статически типизированные):

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

  • Расширение класса без создания подклассов.
    В Objective-C вы можете определить новые функции-члены для существующих классов, в том числе такие, как язык NSString. Например, вы можете добавить метод stripPrefixIfPresent:, чтобы вы могли сказать [@"foo/bar/baz" stripPrefixIfPresent:@"foo/"](обратите внимание на использование NSSringлитералов @"").

  • Использование объектно-ориентированных обратных вызовов.
    В статически типизированных языках, таких как Java и C ++, вы должны приложить значительные усилия, чтобы позволить библиотеке вызывать произвольный член предоставленного пользователем объекта. В Java обходной путь - это пара интерфейс / адаптер плюс анонимный класс, в C ++ обходной путь обычно основан на шаблонах, что подразумевает, что код библиотеки должен быть представлен пользовательскому коду. В Objective-C вы просто передаете ссылку на объект плюс селектор метода в библиотеку, и библиотека может просто и напрямую вызывать обратный вызов.

cmaster
источник
Я могу сделать первое в C ++, приведя к void *, но это обходит систему типов, поэтому она не считается. Я могу сделать второй в C # с методами расширения, идеально в системе типов. В-третьих, я думаю, что «селектор для метода» может быть лямбда-выражением, поэтому любой статически типизированный язык с лямбдами может сделать то же самое, если я правильно понимаю. Я не знаком с ObjC.
user7610
1
@JiriDanek «Я могу сделать первое в C ++, приведя к void *», не совсем так: код, который читает элементы, не может самостоятельно извлечь фактический тип. Вам нужны теги типа. Кроме того, я не думаю, что высказывание «я могу сделать это на <language>» является подходящим / продуктивным способом взглянуть на это, потому что вы всегда можете подражать им. Что важно, так это выигрыш в выразительности по сравнению со сложностью реализации. Кроме того, вы, кажется, думаете, что если язык обладает как статическими, так и динамическими возможностями (Java, C #), он принадлежит исключительно к «статическому» семейству языков.
coredump
1
Один void*только @JiriDanek - это не динамическая типизация, это недостаток типизации. Но да, dynamic_cast, виртуальные таблицы и т. Д. Делают C ++ не просто статически типизированным. Это плохо?
coredump
1
Это говорит о том, что полезно иметь возможность подрывать систему типов при необходимости. Наличие запасного люка, когда вам это нужно. Или кто-то посчитал это полезным. Иначе они не стали бы переводить это на язык.
user7610
2
@JiriDanek Я думаю, вы в значительной степени прибили это своим последним комментарием. Эти аварийные люки могут быть чрезвычайно полезны, если использовать их с осторожностью. Тем не менее, с большой властью приходит большая ответственность, и есть много людей, которые злоупотребляют ею ... Таким образом, гораздо лучше использовать указатель на базовый базовый класс, из которого все другие классы получены по определению (как в случае как в Objective-C и Java), так и полагаться на RTTI для разграничения случаев, а не на void*конкретный тип объекта. Первый выдает ошибку во время выполнения, если вы все испортили, позднее приводит к неопределенному поведению.
мастер