Existem padrões de design que são possíveis apenas em linguagens dinamicamente tipadas como Python?

30

Eu li uma pergunta relacionada. Existem padrões de design desnecessários em linguagens dinâmicas como Python? e lembrei-me desta citação no Wikiquote.org

O maravilhoso da digitação dinâmica é que ela permite expressar qualquer coisa que seja computável. E sistemas de tipos sistemas de tipos não são tipicamente decidíveis e restringem você a um subconjunto. As pessoas que favorecem os sistemas de tipo estático dizem: “tudo bem, é bom o suficiente; todos os programas interessantes que você deseja escrever funcionarão como tipos ”. Mas isso é ridículo - uma vez que você possui um sistema de tipos, nem sabe quais programas interessantes existem.

--- Software Engineering Radio Episódio 140: Tipos de newspeak e conectáveis ​​com Gilad Bracha

Eu me pergunto, existem padrões ou estratégias de design úteis que, usando a formulação da citação, "não funcionam como tipos"?

user7610
fonte
3
Eu descobri que o envio duplo e o padrão Visitor são muito difíceis de realizar em idiomas tipicamente estatísticos, mas facilmente realizáveis ​​em idiomas dinâmicos. Veja esta resposta (e a pergunta) por exemplo: programmers.stackexchange.com/a/288153/122079
user3002473
7
Claro. Qualquer padrão que envolva a criação de novas classes em tempo de execução, por exemplo. (isso também é possível em Java, mas não em C ++; há uma escala deslizante de dinamismo).
user253751
11
Isso dependeria muito de quão sofisticado é o seu sistema de tipos :-) As linguagens funcionais costumam ser muito boas nisso.
Bergi 13/08/16
11
Todo mundo parece estar falando de sistemas de tipos como Java e C # em vez de Haskell ou OCaml. Um idioma com um sistema de tipos poderoso pode ser tão conciso quanto um idioma dinâmico, mas mantenha a segurança do tipo.
Andrew diz Reinstate Monica
@immibis Isso está incorreto. Os sistemas do tipo estático podem absolutamente criar novas classes "dinâmicas" em tempo de execução. Veja o Capítulo 33 de Fundamentos Práticos para Linguagens de Programação.
gardenhead

Respostas:

4

Tipos de primeira classe

A digitação dinâmica significa que você tem tipos de primeira classe: é possível inspecionar, criar e armazenar tipos em tempo de execução, incluindo os tipos do próprio idioma. Isso também significa que os valores são digitados, não variáveis .

A linguagem de tipo estaticamente pode produzir código que também depende de tipos dinâmicos, como despacho de método, classes de tipo etc., mas de uma maneira geralmente invisível para o tempo de execução. Na melhor das hipóteses, eles oferecem uma maneira de realizar a introspecção. Como alternativa, você pode simular tipos como valores, mas depois tem um sistema de tipos dinâmicos ad-hoc.

No entanto, sistemas de tipo dinâmico raramente têm apenas tipos de primeira classe. Você pode ter símbolos de primeira classe, pacotes de primeira classe, primeira classe ... tudo. Isso contrasta com a separação estrita entre o idioma do compilador e o idioma de tempo de execução em idiomas estaticamente tipados. O que o compilador ou intérprete pode fazer o tempo de execução também pode fazer.

Agora, vamos concordar que a inferência de tipo é uma coisa boa e que eu gosto de ter meu código verificado antes de executá-lo. No entanto, também gosto de poder produzir e compilar código em tempo de execução. E também adoro pré-calcular as coisas em tempo de compilação. Em um idioma digitado dinamicamente, isso é feito com o mesmo idioma. No OCaml, você tem o sistema do tipo módulo / função, que é diferente do sistema do tipo principal, que é diferente do idioma do pré-processador. No C ++, você tem a linguagem de modelo que não tem nada a ver com a linguagem principal, que geralmente desconhece os tipos durante a execução. E isso é bom nesse idioma, porque eles não querem fornecer mais.

Por fim, isso não muda realmente o tipo de software que você pode desenvolver, mas a expressividade muda a maneira como você os desenvolve e se é difícil ou não.

Padrões

Padrões que dependem de tipos dinâmicos são padrões que envolvem ambientes dinâmicos: classes abertas, expedição, bancos de dados de objetos na memória, serialização etc. Coisas simples como contêineres genéricos funcionam porque um vetor não esquece em tempo de execução o tipo de objetos que contém (não há necessidade de tipos paramétricos).

Tentei apresentar as várias maneiras pelas quais o código é avaliado no Common Lisp, bem como exemplos de possíveis análises estáticas (esta é a SBCL). O exemplo da sandbox compila um pequeno subconjunto de código Lisp buscado em um arquivo separado. Para ser razoavelmente seguro, altero a tabela de leitura, permito apenas um subconjunto de símbolos padrão e encerro as coisas com um tempo limite.

;;
;; 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}>)

Nada acima é "impossível" fazer com outras línguas. A abordagem de plug-in no Blender, em software de música ou IDEs para linguagens estaticamente compiladas que fazem recompilação imediata etc. Em vez de ferramentas externas, as linguagens dinâmicas favorecem as ferramentas que fazem uso das informações que já existem. Todos os chamadores conhecidos do FOO? todas as subclasses de BAR? todos os métodos especializados pela classe ZOT? esses são dados internalizados. Os tipos são apenas outro aspecto disso.


(veja também: CFFI )

coredump
fonte
39

Resposta curta: não, porque a equivalência de Turing.

Resposta longa: esse cara está sendo um troll. Embora seja verdade que os sistemas de tipos "restrinjam você a um subconjunto", as coisas fora desse subconjunto são, por definição, coisas que não funcionam.

Tudo o que você é capaz de fazer em qualquer linguagem de programação completa de Turing (que é projetada para programação de uso geral, além de muitas que não são; é uma barra bastante baixa para limpar e existem vários exemplos de um sistema se tornando Turing- (não intencionalmente)), você pode fazer em qualquer outra linguagem de programação completa de Turing. Isso é chamado de "equivalência de Turing" e significa apenas exatamente o que diz. É importante ressaltar que isso não significa que você possa fazer a outra coisa com a mesma facilidade na outra linguagem - alguns argumentariam que esse é o objetivo de criar uma nova linguagem de programação em primeiro lugar: para oferecer a você uma maneira melhor de fazer certas coisas. coisas que idiomas existentes sugam.

Um sistema de tipo dinâmico, por exemplo, pode ser emulado no topo de um sistema de tipo OO estático, apenas declarando todas as variáveis, parâmetros e valores de retorno como o Objecttipo base e, em seguida, usando a reflexão para acessar os dados específicos, portanto, quando você perceber isso você vê que não há literalmente nada que você possa fazer em uma linguagem dinâmica que você não possa fazer em uma linguagem estática. Mas fazer dessa maneira seria uma grande bagunça, é claro.

O cara da citação está correto em que tipos estáticos restringem o que você pode fazer, mas esse é um recurso importante, não um problema. As linhas na estrada restringem o que você pode fazer no seu carro, mas você as acha restritivas ou úteis? (Eu sei que não gostaria de dirigir em uma estrada movimentada e complexa, onde não há nada que diga aos carros que sigam na direção oposta para se manterem do lado deles e não cheguem aonde estou dirigindo!) Estabelecendo regras que definem claramente o que é considerado comportamento inválido e garantindo que isso não aconteça, você diminui bastante as chances de ocorrer uma falha grave.

Além disso, ele está descaracterizando o outro lado. Não é que "todos os programas interessantes que você deseja escrever funcionem como tipos", mas "todos os programas interessantes que você deseja escrever exigirão tipos". Depois que você ultrapassa um certo nível de complexidade, fica muito difícil manter a base de código sem um sistema de tipos para mantê-lo alinhado, por dois motivos.

Primeiro, porque é difícil ler o código sem anotações de tipo. Considere o seguinte Python:

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

Como você espera que os dados pareçam que o sistema na outra extremidade da conexão receba? E se está recebendo algo que parece completamente errado, como você descobre o que está acontecendo?

Tudo depende da estrutura de value.someProperty. Mas como é isso? Boa pergunta! O que está chamando sendData()? O que está passando? Como é essa variável? De onde veio? Se não for local, você deve rastrear toda a história valuepara rastrear o que está acontecendo. Talvez você esteja passando outra coisa que também tem uma somePropertypropriedade, mas ela não faz o que você acha que faz?

Agora vamos dar uma olhada com anotações de tipo, como você pode ver na linguagem Boo, que usa sintaxe muito semelhante, mas é tipicamente estatizada:

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

Se houver algo de errado, de repente, seu trabalho de depuração ficou com uma ordem de magnitude mais fácil: procure a definição de MyDataType! Além disso, a chance de ter um comportamento ruim porque você passou por um tipo incompatível que também possui uma propriedade com o mesmo nome repentinamente chega a zero, porque o sistema de tipos não permite que você cometa esse erro.

O segundo motivo se baseia no primeiro: em um projeto grande e complexo, você provavelmente tem vários colaboradores. (E, se não, você está construindo por um longo tempo, o que é essencialmente a mesma coisa. Tente ler o código que você escreveu há 3 anos, se você não acredita em mim!) Isso significa que você não sabe o que foi. passando pela cabeça da pessoa que escreveu quase parte do código no momento em que o escreveu, porque você não estava lá ou não se lembra se era o seu próprio código há muito tempo. Ter declarações de tipo realmente ajuda a entender qual era a intenção do código!

Pessoas como o sujeito da citação frequentemente descaracterizam os benefícios da digitação estática como "ajudar o compilador" ou "tudo sobre eficiência" em um mundo onde recursos de hardware quase ilimitados tornam isso cada vez menos relevante a cada ano que passa. Mas, como mostrei, embora esses benefícios certamente existam, o principal benefício está nos fatores humanos, particularmente na legibilidade e manutenção do código. (A eficiência adicional é certamente um bom bônus!)

Mason Wheeler
fonte
24
"Esse cara está sendo um troll." - Não tenho certeza de que um ataque ad hominem ajude seu caso de outra forma bem apresentado. E embora eu esteja ciente de que o argumento da autoridade é uma falácia igualmente ruim como ad hominem, eu ainda gostaria de salientar que Gilad Bracha provavelmente projetou mais linguagens e (mais relevante para esta discussão) mais sistemas do tipo estático do que a maioria. Apenas um pequeno trecho: ele é o único designer do Newspeak, co-designer da Dart, co-autor da especificação de linguagem Java e especificação da máquina virtual Java, trabalhou no design de Java e da JVM, projetado…
Jörg W Mittag
10
Strongtalk (um sistema de tipo estático para Smalltalk), o sistema de tipo Dart, o sistema de tipo Newspeak, sua tese de doutorado em modularidade é a base de praticamente todos os sistemas de módulos modernos (por exemplo, Java 9, ECMAScript 2015, Scala, Dart, Newspeak, Ioke, por exemplo) , Seph), seu artigo sobre mixins revolucionou a maneira como pensamos sobre eles. Agora, isso não significa que ele está certo, mas eu não acho que ter projetado vários sistemas do tipo estático faz dele um pouco mais do que um "troll".
Jörg W Mittag
17
"Embora seja verdade que os sistemas de tipos" o restringem a um subconjunto ", as coisas fora desse subconjunto são, por definição, coisas que não funcionam". - Isto está errado. Sabemos pela indecidibilidade do problema de parada, pelo teorema de Rice e pela miríade de outros resultados de Indecidibilidade e Incomputabilidade que um verificador de tipo estático não pode decidir em todos os programas se é ou não seguro. Ele não pode aceitar esses programas (alguns dos quais não são seguros para o tipo); portanto, a única opção sensata é rejeitá- los (no entanto, alguns deles são seguros para o tipo). Como alternativa, o idioma deve ser projetado em…
Jörg W Mittag 12/08
9
… De maneira a tornar impossível ao programador escrever esses programas indecidíveis, mas, novamente, alguns deles são realmente seguros para o tipo. Portanto, não importa como você o divide: o programador é impedido de gravar programas com segurança de tipo. E uma vez que há realmente infinitamente muitos deles (geralmente), que pode ser quase certo que pelo menos alguns deles não são apenas coisas que faz o trabalho, mas também útil.
Jörg W Mittag 12/08
8
@MasonWheeler: o problema da parada surge o tempo todo, precisamente no contexto da verificação de tipo estático. A menos que as linguagens sejam cuidadosamente projetadas para impedir que o programador escreva certos tipos de programas, a verificação estática do tipo rapidamente se torna equivalente à solução do problema de parada. Ou você acabar com os programas que não estão autorizados a escrever porque eles podem confundir o verificador de tipos, ou você acabar com os programas que estão autorizados a escrever, mas eles tomam uma quantidade infinita de tempo para verificação de tipo.
Jörg W Mittag
27

Vou dar um passo à frente na parte 'padrão' porque acho que ela se volta para a definição do que é ou não um padrão e há muito tempo perdi o interesse nesse debate. O que vou dizer é que existem coisas que você pode fazer em alguns idiomas que não pode fazer em outros. Deixe-me ser claro, eu estou não dizer que há problemas que você pode resolver em um idioma que você não pode resolver em outro. Mason já apontou a integridade de Turing.

Por exemplo, escrevi uma classe em python que envolve um elemento XML DOM e o transforma em um objeto de primeira classe. Ou seja, você pode escrever o código:

doc.header.status.text()

e você tem o conteúdo desse caminho a partir de um objeto XML analisado. tipo de limpo e arrumado, IMO. E se não houver um nó principal, ele retornará apenas objetos fictícios que não contêm nada além de objetos fictícios (tartarugas até o fim). Não há maneira real de fazer isso, digamos, em Java. Você precisaria ter compilado uma classe com antecedência, com base em algum conhecimento da estrutura do XML. Deixando de lado se é uma boa ideia, esse tipo de coisa realmente muda a maneira como você resolve problemas em uma linguagem dinâmica. No entanto, não estou dizendo que isso muda de uma maneira que é necessariamente sempre melhor. Existem alguns custos definidos para abordagens dinâmicas e a resposta de Mason fornece uma visão geral decente. Se eles são uma boa escolha depende de muitos fatores.

Em uma nota lateral, você pode fazer isso em Java porque pode criar um interpretador python em Java . O fato de resolver um problema específico em um determinado idioma pode significar a construção de um intérprete ou algo semelhante a ele é frequentemente ignorado quando as pessoas falam sobre a integridade de Turing.

JimmyJames
fonte
4
Você não pode fazer isso em Java porque o Java é mal projetado. Não seria tão difícil em C # usar IDynamicMetaObjectProvider, e é simples no Boo. ( Aqui está uma implementação em menos de 100 linhas, incluído como parte da árvore fonte padrão no GitHub, porque é muito fácil!)
Mason Wheeler
6
@MasonWheeler "IDynamicMetaObjectProvider"? Isso está relacionado à dynamicpalavra-chave do C # ? ... que efetivamente adere à digitação dinâmica em C #? Não tenho certeza se seu argumento é válido se eu estiver certo.
Jpmc26 13/08/16
9
@MasonWheeler Você está entrando na semântica. Sem entrar em um debate sobre minúcias (não estamos desenvolvendo um formalismo matemático no SE aqui.), A digitação dinâmica é a prática de preceder as decisões de tempo de compilação em torno dos tipos, especialmente a verificação de que cada tipo tem os membros específicos acessados ​​pelo programa. Esse é o objetivo que dynamicrealiza em C #. "Pesquisas de reflexão e dicionário" acontecem em tempo de execução, não em tempo de compilação. Eu realmente não tenho certeza de como você pode argumentar que ele não adiciona digitação dinâmica ao idioma. O que quero dizer é que o último parágrafo de Jimmy cobre isso.
Jpmc26 13/08/16
44
Apesar de não ser um grande fã de Java, também ouso dizer que chamar Java de "mal projetado" especificamente porque não adicionou digitação dinâmica é ... excessivamente zeloso.
Jpmc26
5
Além da sintaxe um pouco mais conveniente, como isso é diferente de um dicionário?
Theodoros Chatzigiannakis
10

A citação está correta, mas também é realmente falsa. Vamos analisar detalhadamente o motivo:

O maravilhoso da digitação dinâmica é que ela permite expressar qualquer coisa que seja computável.

Bem, não exatamente. Um idioma com digitação dinâmica permite que você expresse qualquer coisa, desde que seja o Turing completo , o que é mais. O próprio sistema de tipos não permite que você expresse tudo. Mas vamos dar a ele o benefício da dúvida aqui.

E sistemas de tipos sistemas de tipos não são tipicamente decidíveis e restringem você a um subconjunto.

Isso é verdade, mas observe que agora estamos falando firmemente sobre o que o sistema de tipos permite, e não o que a linguagem que usa um sistema de tipos permite. Embora seja possível usar um sistema de tipos para calcular coisas em tempo de compilação, isso geralmente não é Turing completo (como o sistema de tipos geralmente é decidível), mas quase qualquer linguagem estaticamente tipificada também é Turing completa em seu tempo de execução (idiomas tipicamente dependentes são não, mas não acredito que estamos falando sobre eles aqui).

As pessoas que favorecem os sistemas de tipo estático dizem: “tudo bem, é bom o suficiente; todos os programas interessantes que você deseja escrever funcionarão como tipos ”. Mas isso é ridículo - uma vez que você possui um sistema de tipos, nem sabe quais programas interessantes existem.

O problema é que tipos de idiomas dinamicamente têm um tipo estático. Às vezes, tudo é uma string e, mais comumente, existe alguma união marcada, onde tudo é um pacote de propriedades ou um valor como int ou double. O problema é que as linguagens estáticas também podem fazer isso, historicamente foi um pouco complicado fazer isso, mas as linguagens modernas de tipo estatístico tornam isso mais fácil do que o uso de uma linguagem de tipos dinâmicos, então como pode haver uma diferença em o que o programador pode ver como um programa interessante? Os idiomas estáticos têm exatamente as mesmas uniões marcadas, além de outros tipos.

Para responder à pergunta no título: Não, não há padrões de design que não possam ser implementados em uma linguagem de tipo estaticamente, porque você sempre pode implementar um sistema dinâmico suficiente para obtê-los. Pode haver padrões que você obtém de graça em um idioma dinâmico; isso pode ou não valer a pena suportar as desvantagens desses idiomas para o YMMV .

jk.
fonte
2
Não tenho certeza se você acabou de responder sim ou não. Parece mais um não para mim.
user7610
11
@TheodorosChatzigiannakis Sim, de que outra forma as linguagens dinâmicas seriam implementadas? Primeiro, você passará por um arquiteto astronauta se quiser implementar um sistema de classe dinâmico ou qualquer outra coisa um pouco envolvida. Segundo, você provavelmente não tem o recurso para torná-lo depurável, totalmente introspectável, com desempenho ("apenas use um dicionário" é como as linguagens lentas são implementadas). Em terceiro lugar, algumas características dinâmicas são mais utilizados quando está a ser integrado em toda a língua, e não apenas como uma biblioteca: acho que a coleta de lixo, por exemplo (não são GCs como bibliotecas, mas eles não são comumente usados).
Coredump
11
@ Theodoros De acordo com o artigo que eu já vinculei aqui uma vez, quase 2,5% das estruturas (nos módulos Python pesquisados ​​pelas pesquisas) podem ser facilmente expressas em uma linguagem digitada. Talvez os 2,5% façam valer a pena pagar os custos da digitação dinâmica. Isso é essencialmente sobre o que minha pergunta era. neverworkintheory.org/2016/06/13/polymorphism-in-python.html
user7610
3
@JiriDanek Até onde eu sei, não há nada que impeça que uma linguagem de tipo estaticamente tenha pontos de chamada polimórficos e mantenha a digitação estática no processo. Consulte Verificação de tipo estático de vários métodos . Talvez eu esteja entendendo mal o seu link.
Theodoros Chatzigiannakis
11
“Um idioma com digitação dinâmica permite que você expresse qualquer coisa, desde que seja Turing completo, o que é mais.” Embora essa seja uma afirmação verdadeira, ela realmente não se aplica ao “mundo real” porque a quantidade de texto que se tem escrever pode ser extremamente grande.
22416 Daniel Jour
4

Certamente, há coisas que você só pode fazer em idiomas de tipo dinâmico. Mas eles não seriam necessariamente um bom design.

Você pode atribuir primeiro um número inteiro 5 e depois uma sequência de caracteres 'five'ou um Catobjeto à mesma variável. Mas você só está dificultando para um leitor de seu código descobrir o que está acontecendo, qual é o objetivo de cada variável.

Você pode adicionar um novo método a uma classe Ruby da biblioteca e acessar seus campos particulares. Pode haver casos em que esse hack pode ser útil, mas isso seria uma violação do encapsulamento. (Não me importo de adicionar métodos baseados apenas na interface pública, mas isso não é nada que os métodos de extensão C # digitados estaticamente não possam fazer.)

Você pode adicionar um novo campo a um objeto da classe de outra pessoa para transmitir alguns dados extras. Mas é melhor projetar apenas criar uma nova estrutura ou estender o tipo original.

Geralmente, quanto mais organizado você deseja que seu código permaneça, menor a vantagem de poder alterar dinamicamente as definições de tipo ou atribuir valores de diferentes tipos à mesma variável. Porém, seu código não é diferente do que você poderia obter em uma linguagem de tipo estaticamente.

O que as linguagens dinâmicas são boas é o açúcar sintático. Por exemplo, ao ler um objeto JSON desserializado, você pode se referir a um valor aninhado simplesmente como obj.data.article[0].contentmuito mais puro do que digamos obj.getJSONObject("data").getJSONArray("article").getJSONObject(0).getString("content").

Os desenvolvedores Ruby, em especial, podem falar longamente sobre mágica que pode ser alcançada com a implementação method_missing, que é um método que permite lidar com tentativas de chamadas para métodos não declarados. Por exemplo, o ActiveRecord ORM o utiliza para que você possa fazer uma chamada User.find_by_email('[email protected]')sem nunca declarar o find_by_emailmétodo. É claro que não é nada que não possa ser obtido como UserRepository.FindBy("email", "[email protected]")em uma linguagem estaticamente tipada, mas você não pode negar sua limpeza.

kamilk
fonte
4
Certamente, há coisas que você só pode fazer em idiomas estaticamente tipados. Mas eles não seriam necessariamente um bom design.
Coredump
2
O ponto sobre o açúcar sintático tem muito pouco a ver com a digitação dinâmica e tudo com, bem, sintaxe.
precisa saber é o seguinte
@leftaroundabout Os padrões têm tudo a ver com sintaxe. Os sistemas de tipos também têm muito a ver com isso.
precisa saber é o seguinte
4

O padrão Dynamic Proxy é um atalho para implementar objetos proxy sem precisar de uma classe por tipo que você precisa fazer proxy.

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

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

Usando isso, Proxy(someObject)cria um novo objeto que se comporta da mesma forma que someObject. Obviamente, você também desejará adicionar funcionalidades adicionais de alguma forma, mas é uma base útil para começar. Em uma linguagem estática completa, você precisa escrever uma classe Proxy por tipo que deseja proxy ou usar a geração dinâmica de código (que, é verdade, está incluída na biblioteca padrão de muitas linguagens estáticas, principalmente porque seus designers estão cientes de problemas em não conseguir fazer isso).

Outro caso de uso de linguagens dinâmicas é o chamado "patch de macaco". De muitas maneiras, esse é um antipadrão e não um padrão, mas pode ser usado de maneiras úteis se feito com cuidado. E, embora não exista uma razão teórica, o patch do macaco não pôde ser implementado em uma linguagem estática, nunca vi uma que realmente a tenha.

Jules
fonte
Acho que posso imitar isso no Go. Há um conjunto de métodos que todos os objetos em proxy devem ter (caso contrário, o pato pode não ser grasnado e tudo desmoronar). Eu posso criar uma interface Go com esses métodos. Vou ter que pensar mais, mas acho que o que tenho em mente funcionará.
user7610
Você pode algo semelhante em qualquer linguagem .NET com RealProxy e genéricos.
LittleEwok
@LittleEwok - O RealProxy usa geração de código de tempo de execução - como eu digo, muitas linguagens estáticas modernas têm uma solução alternativa como essa, mas ainda é mais fácil em uma linguagem dinâmica.
Jules
Os métodos de extensão C # são como patches de macaco protegidos. Você não pode alterar os métodos existentes, mas pode adicionar novos.
Andrew diz Reinstate Monica
3

Sim , existem muitos padrões e técnicas que só são possíveis em uma linguagem de tipo dinâmico.

O patch para macacos é uma técnica em que propriedades ou métodos são adicionados a objetos ou classes em tempo de execução. Essa técnica não é possível em uma linguagem de tipo estaticamente, pois isso significa que tipos e operações não podem ser verificados em tempo de compilação. Ou, dito de outra maneira, se uma linguagem suporta patches de macaco, é por definição uma linguagem dinâmica.

Pode-se provar que, se um idioma suportar patch de macaco (ou técnicas semelhantes para modificar tipos em tempo de execução), não poderá ser verificado estaticamente. Portanto, não é apenas uma limitação nos idiomas existentes atualmente, é uma limitação fundamental da digitação estática.

Portanto, a citação está definitivamente correta - mais coisas são possíveis em um idioma dinâmico do que em um idioma estaticamente tipado. Por outro lado, certos tipos de análise são possíveis apenas em uma linguagem estaticamente tipada. Por exemplo, você sempre sabe quais operações são permitidas em um determinado tipo, o que permite detectar operações ilegais no tipo de compilação. Nenhuma verificação é possível em um idioma dinâmico quando operações podem ser adicionadas ou removidas em tempo de execução.

É por isso que não há "melhor" óbvio no conflito entre linguagens estáticas e dinâmicas. As linguagens estáticas perdem certa energia no tempo de execução em troca de um tipo diferente de energia no momento da compilação, que eles acreditam reduzir o número de bugs e facilitar o desenvolvimento. Alguns acreditam que a troca vale a pena, outros não.

Outras respostas argumentaram que a equivalência de Turing significa que qualquer coisa possível em um idioma é possível em todos os idiomas. Mas isso não segue. Para dar suporte a algo como patch de macaco em uma linguagem estática, você basicamente precisa implementar uma sub-linguagem dinâmica dentro da linguagem estática. É claro que isso é possível, mas eu diria que você está programando em uma linguagem dinâmica incorporada, já que você também perde a verificação de tipo estático que existe na linguagem host.

C # desde a versão 4 tem suporte para objetos digitados dinamicamente. Claramente, os designers de idiomas vêem benefícios em ter ambos os tipos de digitação disponíveis. Mas também mostra que você não pode comer e comer também: quando você usa objetos dinâmicos em C #, obtém a capacidade de fazer algo como aplicar patches em macacos, mas também perde a verificação estática de tipo para interação com esses objetos.

JacquesB
fonte
+1 ao seu penúltimo parágrafo, eu acho que é o argumento crucial. Eu ainda argumentaria que há uma diferença, porém, como nos tipos estáticos, você tem total controle de onde e do que pode usar o patch
jk.
2

Eu me pergunto, existem padrões ou estratégias de design úteis que, usando a formulação da citação, "não funcionam como tipos"?

Sim e não.

Há situações em que o programador conhece o tipo de uma variável com mais precisão do que um compilador. O compilador pode saber que algo é um Objeto, mas o programador saberá (devido aos invariantes do programa) que na verdade é uma String.

Deixe-me mostrar alguns exemplos disso:

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

Eu sei que someMap.get(T.class)retornará a Function<T, String>, por causa de como eu construí alguns mapas. Mas Java só tem certeza de que eu tenho uma função.

Outro exemplo:

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

Eu sei que data.properties.rowCount será uma referência válida e um número inteiro, porque eu validei os dados em um esquema. Se esse campo estivesse faltando, uma exceção teria sido lançada. Mas um compilador saberia que está lançando uma exceção ou retornaria algum tipo de JSONValue genérico.

Outro exemplo:

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

Os "II6s" definem a maneira como os dados codificam três variáveis. Desde que especifiquei o formato, sei quais tipos serão retornados. Um compilador saberia apenas que ele retorna uma tupla.

O tema unificador de todos esses exemplos é que o programador conhece o tipo, mas um sistema de tipo no nível Java não será capaz de refletir isso. O compilador não conhece os tipos e, portanto, uma linguagem de tipo estaticamente não me permite chamá-los, enquanto uma linguagem de tipo dinâmico.

É assim que a citação original chega:

O maravilhoso da digitação dinâmica é que ela permite expressar qualquer coisa que seja computável. E sistemas de tipos sistemas de tipos não são tipicamente decidíveis e restringem você a um subconjunto.

Ao usar a digitação dinâmica, posso usar o tipo mais derivado que eu conheço, não apenas o tipo mais derivado que o sistema de tipos do meu idioma conhece. Em todos os casos acima, eu tenho um código que é semanticamente correto, mas será rejeitado por um sistema de digitação estático.

No entanto, para retornar à sua pergunta:

Eu me pergunto, existem padrões ou estratégias de design úteis que, usando a formulação da citação, "não funcionam como tipos"?

Qualquer um dos exemplos acima e, de fato, qualquer exemplo de digitação dinâmica pode ser validado na digitação estática adicionando-se conversões apropriadas. Se você conhece um tipo que seu compilador não conhece, basta informar ao compilador lançando o valor. Portanto, em algum nível, você não obterá padrões adicionais usando a digitação dinâmica. Você pode precisar converter mais para começar a trabalhar com código estático digitado.

A vantagem da digitação dinâmica é que você pode simplesmente usar esses padrões sem se preocupar com o fato de que é complicado convencer o sistema de tipos de sua validade. Ele não altera os padrões disponíveis, apenas os torna mais fáceis de implementar, porque você não precisa descobrir como fazer seu sistema de tipos reconhecer o padrão ou adicionar moldes para subverter o sistema de tipos.

Winston Ewert
fonte
11
por que java é o ponto de corte no qual você não deve ir para um 'sistema de tipo mais avançado / complicado'?
jk.
2
@ jk, o que leva você a pensar que é isso que estou dizendo? Eu evitei explicitamente tomar partido se um sistema do tipo mais avançado / complicado vale ou não.
Winston Ewert
2
Alguns desses são exemplos terríveis, e os outros parecem ser mais decisões de linguagem do que digitados ou não. Estou particularmente confuso com o motivo pelo qual as pessoas pensam que a desserialização é tão complexa nas linguagens digitadas. O resultado digitado seria data = parseJSON<SomeSchema>(someJson); print(data.properties.rowCount); e, se não houver uma classe para desserializar, podemos recorrer a data = parseJSON(someJson); print(data["properties.rowCount"]);- que ainda é digitado e expressa a mesma intenção.
NPSF3000 14/08/16
2
@ NPSF3000, como funciona a função parseJSON? Parece usar reflexão ou macros. Como os dados ["properties.rowCount"] podem ser digitados em um idioma estático? Como ele poderia saber que o valor resultante é um número inteiro?
Winston Ewert
2
@ NPSF3000, como você planeja usá-lo se não souber que é um número inteiro? Como você planeja repetir elementos em uma lista no JSON sem saber que era uma matriz? O ponto do meu exemplo era que eu sabia que data.propertiesera um objeto e sabia que data.properties.rowCountera um número inteiro e poderia simplesmente escrever código que os usasse. Sua proposta data["properties.rowCount"]não fornece a mesma coisa.
Winston Ewert
1

Aqui estão alguns exemplos de Objective-C (tipados dinamicamente) que não são possíveis em C ++ (tipicamente estatísticos):

  • Colocando objetos de várias classes distintas no mesmo contêiner.
    Obviamente, isso requer inspeção do tipo de tempo de execução para interpretar posteriormente o conteúdo do contêiner, e a maioria dos amigos de digitação estática objetará que você não deveria fazer isso em primeiro lugar. Mas descobri que, além dos debates religiosos, isso pode ser útil.

  • Expandindo uma classe sem subclassificar.
    No Objective-C, você pode definir novas funções de membro para classes existentes, incluindo as definidas por idioma, como NSString. Por exemplo, você pode adicionar um método stripPrefixIfPresent:, para poder dizer [@"foo/bar/baz" stripPrefixIfPresent:@"foo/"](observe o uso dos NSSringliterais@"" ).

  • Uso de retornos de chamada orientados a objetos.
    Em linguagens estaticamente tipadas, como Java e C ++, é necessário um grande esforço para permitir que uma biblioteca chame um membro arbitrário de um objeto fornecido pelo usuário. Em Java, a solução alternativa é o par de interface / adaptador mais uma classe anônima. Em C ++, a solução alternativa geralmente é baseada em modelos, o que implica que o código da biblioteca deve ser exposto ao código do usuário. No Objective-C, você apenas passa a referência do objeto mais o seletor do método para a biblioteca, e a biblioteca pode simples e diretamente invocar o retorno de chamada.

cmaster
fonte
Eu posso fazer o primeiro em C ++ convertendo para void *, mas isso está contornando o sistema de tipos, portanto não conta. Eu posso fazer o segundo em c # com métodos de extensão, perfeitamente dentro de um sistema de tipos. No terceiro, acho que o "seletor para o método" pode ser um lambda; portanto, qualquer linguagem de tipo estaticamente com lambdas pode fazer o mesmo, se bem entendi. Eu não estou familiarizado com ObjC.
user7610
11
@JiriDanek "Eu posso fazer o primeiro em C ++ lançando para void *", não exatamente, o código que lê os elementos não tem como recuperar o tipo real por si próprio. Você precisa de tags de tipo. Além disso, não acho que dizer "eu posso fazer isso em <idioma>" seja a maneira apropriada / produtiva de ver isso, porque você sempre pode imitá-los. O que importa é o ganho em expressividade versus complexidade da implementação. Além disso, você parece pensar que, se uma linguagem possui recursos estáticos e dinâmicos (Java, C #), pertence exclusivamente à família de linguagens "estática".
Coredump
11
O @JiriDanek por void*si só não é digitação dinâmica, é falta de digitação. Mas sim, dynamic_cast, tabelas virtuais etc. tornam C ++ não tipicamente estaticamente digitado. Isso é ruim?
Coredump
11
Isso sugere que ter a opção de subverter o sistema de tipos quando necessário é útil. Ter uma escotilha de escape quando você precisar. Ou alguém considerou útil. Caso contrário, eles não o colocariam no idioma.
user7610
2
@JiriDanek Eu acho que você acertou em cheio com seu último comentário. Essas escotilhas de escape podem ser extremamente úteis se usadas com cuidado. No entanto, com grande poder vem uma grande responsabilidade, e muitas são as pessoas que abusam dele ... Portanto, é muito melhor usar um ponteiro para uma classe base genérica da qual todas as outras classes são derivadas por definição (como é o caso no Objective-C e Java) e confiar no RTTI para diferenciar os casos, do que converter um void*em algum tipo de objeto específico. O primeiro produz um erro de tempo de execução, se você errou, o último resulta em um comportamento indefinido.
precisa saber é