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"?
fonte
Respostas:
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.
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 )
fonte
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
Object
tipo 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:
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á chamandosendData()
? O que está passando? Como é essa variável? De onde veio? Se não for local, você deve rastrear toda a históriavalue
para rastrear o que está acontecendo. Talvez você esteja passando outra coisa que também tem umasomeProperty
propriedade, 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:
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!)
fonte
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:
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.
fonte
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!)"IDynamicMetaObjectProvider"
? Isso está relacionado àdynamic
palavra-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.dynamic
realiza 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.A citação está correta, mas também é realmente falsa. Vamos analisar detalhadamente o motivo:
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.
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).
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 .
fonte
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 umCat
objeto à 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].content
muito mais puro do que digamosobj.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 chamadaUser.find_by_email('[email protected]')
sem nunca declarar ofind_by_email
método. É claro que não é nada que não possa ser obtido comoUserRepository.FindBy("email", "[email protected]")
em uma linguagem estaticamente tipada, mas você não pode negar sua limpeza.fonte
O padrão Dynamic Proxy é um atalho para implementar objetos proxy sem precisar de uma classe por tipo que você precisa fazer proxy.
Usando isso,
Proxy(someObject)
cria um novo objeto que se comporta da mesma forma quesomeObject
. 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.
fonte
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.
fonte
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:
Eu sei que
someMap.get(T.class)
retornará aFunction<T, String>
, por causa de como eu construí alguns mapas. Mas Java só tem certeza de que eu tenho uma função.Outro exemplo:
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:
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:
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:
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.
fonte
data = parseJSON<SomeSchema>(someJson); print(data.properties.rowCount);
e, se não houver uma classe para desserializar, podemos recorrer adata = parseJSON(someJson); print(data["properties.rowCount"]);
- que ainda é digitado e expressa a mesma intenção.data.properties
era um objeto e sabia quedata.properties.rowCount
era um número inteiro e poderia simplesmente escrever código que os usasse. Sua propostadata["properties.rowCount"]
não fornece a mesma coisa.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étodostripPrefixIfPresent:
, para poder dizer[@"foo/bar/baz" stripPrefixIfPresent:@"foo/"]
(observe o uso dosNSSring
literais@""
).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.
fonte
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?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.