O que exatamente faz com que o sistema do tipo Haskell seja tão reverenciado (por exemplo, Java)?

204

Estou começando a aprender Haskell . Eu sou muito novo e estou lendo alguns livros on-line para entender meus conceitos básicos.

Um dos "memes" sobre os quais as pessoas familiarizadas costumam falar é a coisa toda "se compilar, funcionará *" - que eu acho que está relacionada à força do sistema de tipos.

Estou tentando entender por que exatamente Haskell é melhor do que outras linguagens estaticamente tipadas nesse sentido.

Em outras palavras, suponho que em Java, você poderia fazer algo hediondo como enterrar ArrayList<String>()para conter algo que realmente deveria ser ArrayList<Animal>(). A coisa hedionda aqui é que você stringcontém elephant, giraffe, etc, e se alguém o inserir Mercedes- seu compilador não irá ajudá-lo.

Se eu fiz fazer ArrayList<Animal>()em seguida, em algum momento posterior, se eu decidir meu programa não é realmente sobre animais, é sobre veículos, então eu posso mudar, por exemplo, uma função que produz ArrayList<Animal>para produzir ArrayList<Vehicle>e minha IDE deve dizer-me em todos os lugares há é uma pausa na compilação.

Minha suposição é que é isso que as pessoas entendem por um sistema de tipos forte , mas não me é óbvio por que o Haskell é melhor. Dito de outra forma, você pode escrever Java bom ou ruim, presumo que você possa fazer o mesmo em Haskell (ou seja, colocar coisas em strings / ints que realmente devem ser tipos de dados de primeira classe).

Eu suspeito que estou perdendo algo importante / básico.
Eu ficaria muito feliz em ser mostrado o erro dos meus caminhos!

phatmanace
fonte
31
Vou deixar as pessoas com mais conhecimento do que escrever respostas reais, mas a essência é a seguinte: linguagens estáticas, como o C #, têm um sistema de tipos que tenta ajudá-lo a escrever código defensável ; digite sistemas como a tentativa de Haskell de ajudá-lo a escrever código correto (ou seja, comprovável). O princípio básico no trabalho é mover coisas que podem ser verificadas no estágio de compilação; Haskell verifica mais coisas no momento da compilação.
21815 Robert Harvey
8
Não sei muito sobre Haskell, mas posso falar sobre Java. Embora pareça fortemente digitado, ainda permite que você faça coisas "hediondas" como você disse. Para quase todas as garantias que Java faz em relação ao seu sistema de tipos, há uma maneira de contornar isso.
12
Não sei por que todas as respostas mencionam Maybeapenas no final. Se eu tivesse que escolher apenas uma coisa que os idiomas mais populares deveriam emprestar de Haskell, seria isso. É uma ideia muito simples (não muito interessante do ponto de vista teórico), mas isso por si só tornaria nosso trabalho muito mais fácil.
Paul
1
Haverá ótimas respostas aqui, mas, na tentativa de ajudar, assine o tipo de estudo. Eles permitem que seres humanos e programas raciocinem sobre programas de uma maneira que ilustre como o Java está no meio dos recursos.
Michael Páscoa
6
Para ser justo, devo salientar que "o todo, se compilar, funcionará" é um slogan, não uma declaração literal de fato. Sim, nós programadores da Haskell sabemos que passar no verificador de tipos oferece uma boa chance de correção, para algumas noções limitadas de correção, mas certamente não é uma declaração literal e universalmente "verdadeira"!
Tom Ellis

Respostas:

230

Aqui está uma lista não ordenada de recursos do sistema de tipos disponíveis no Haskell e indisponíveis ou menos agradáveis ​​em Java (que eu saiba, que é reconhecidamente fraco em Java)

  • Segurança . Os tipos de Haskell têm boas propriedades de "segurança de tipo". Isso é bastante específico, mas significa essencialmente que os valores de algum tipo não podem se transformar voluntariamente em outro tipo. Às vezes, isso está em desacordo com a mutabilidade (consulte restrição de valor do OCaml )
  • Tipos de dados algébricos . Os tipos em Haskell têm essencialmente a mesma estrutura que a matemática do ensino médio. Isso é escandalosamente simples e consistente, mas, ao que parece, o mais poderoso que você poderia desejar. É simplesmente uma excelente base para um sistema de tipos.
    • Programação genérica de tipo de dados . Não é o mesmo que tipos genéricos (consulte generalização ). Em vez disso, devido à simplicidade da estrutura de tipos, conforme observado anteriormente, é relativamente fácil escrever código que opera genericamente sobre essa estrutura. Mais tarde, falo sobre como algo como Equality pode ser derivado automaticamente para um tipo definido pelo usuário por um compilador Haskell. Essencialmente, a maneira como faz isso é percorrer a estrutura simples e comum subjacente a qualquer tipo definido pelo usuário e combiná-la entre os valores - uma forma muito natural de igualdade estrutural.
  • Tipos mutuamente recursivos . Este é apenas um componente essencial para escrever tipos não triviais.
    • Tipos aninhados . Isso permite definir tipos recursivos sobre variáveis ​​que se repetem em tipos diferentes. Por exemplo, um tipo de árvore equilibrada é data Bt a = Here a | There (Bt (a, a)). Pense com cuidado nos valores válidos Bt ae observe como esse tipo funciona. É complicado!
  • Generalização . Isso é tolo demais para não existir em um sistema de tipos (ahem, olhando para você, vá). É importante ter noções de variáveis ​​de tipo e a capacidade de falar sobre código, independente da escolha dessa variável. Hindley Milner é um sistema de tipos derivado do Sistema F. O sistema de tipos de Haskell é uma elaboração da tipagem HM e o Sistema F é essencialmente o coração da generalização. O que quero dizer é que Haskell tem uma história de generalização muito boa .
  • Tipos abstratos . A história de Haskell aqui não é ótima, mas também não existe. É possível escrever tipos que têm uma interface pública, mas uma implementação privada. Isso nos permite admitir alterações no código de implementação posteriormente e, mais importante, como é a base de todas as operações no Haskell, escreva tipos "mágicos" que possuem interfaces bem definidas, como IO. O Java provavelmente tem uma história do tipo abstrato mais agradável, para ser sincero, mas acho que até o Interfaces se tornar mais popular era isso genuinamente verdadeiro.
  • Parametricidade . Valores Haskell não tem quaisquer operações universais. Java viola isso com coisas como igualdade de referência e hash e ainda mais flagrantemente com coerções. O que isso significa é que você obtém teoremas gratuitos sobre tipos que permitem conhecer o significado de uma operação ou valor em um grau notável inteiramente a partir do seu tipo - certos tipos são tais que apenas pode haver um número muito pequeno de habitantes.
  • Tipos de tipos mais altos aparecem todo o tipo ao codificar coisas mais complicadas. Functor / Applicative / Monad, Foldable / Traversable, todo o mtlsistema de digitação de efeitos, pontos de correção generalizados de functor. A lista continua e continua. Há muitas coisas que são melhor expressas em tipos superiores e relativamente poucos sistemas de tipos até permitem que o usuário fale sobre essas coisas.
  • Digite classes . Se você pensa nos sistemas de tipos como lógicas - o que é útil -, geralmente é exigido que você prove as coisas. Em muitos casos, isso é essencialmente ruído de linha: pode haver apenas uma resposta certa e é uma perda de tempo e esforço para o programador afirmar isso. As aulas tipográficas são uma maneira de o Haskell gerar as provas para você. Em termos mais concretos, isso permite que você resolva "sistemas de equações de tipo" simples como "Em que tipo pretendemos fazer as (+)coisas juntos? Ah Integer, ok! Vamos alinhar o código certo agora!". Em sistemas mais complexos, você pode estar estabelecendo restrições mais interessantes.
    • Cálculo de restrição . As restrições em Haskell - que são o mecanismo para acessar o sistema de prólogo da classe - são tipificadas estruturalmente. Isso fornece uma forma muito simples de relacionamento de subtipagem, que permite montar restrições complexas a partir das mais simples. A mtlbiblioteca inteira é baseada nessa ideia.
    • Derivando . Para impulsionar a canonicidade do sistema de tipeclass, é necessário escrever muitos códigos triviais para descrever as restrições que os tipos definidos pelo usuário devem instanciar. Como na estrutura muito normal dos tipos Haskell, muitas vezes é possível solicitar ao compilador que faça esse padrão para você.
    • Digite o prólogo da classe . O solucionador de classes do tipo Haskell - o sistema que está gerando as "provas" a que me referi anteriormente - é essencialmente uma forma aleijada de Prolog com propriedades semânticas mais agradáveis. Isso significa que você pode codificar coisas realmente cabeludas no tipo prólogo e esperar que elas sejam tratadas em tempo de compilação. Um bom exemplo pode ser a prova de que duas listas heterogêneas são equivalentes se você esquecer a ordem - elas são "conjuntos" heterogêneos equivalentes.
    • Classes do tipo multiparâmetros e dependências funcionais . Estes são apenas refinamentos maciçamente úteis para basear o prólogo da classe. Se você conhece o Prolog, pode imaginar quanto aumenta a potência expressiva ao escrever predicados de mais de uma variável.
  • Muito boa inferência . Os idiomas baseados nos sistemas do tipo Hindley Milner têm uma inferência bastante boa. O próprio HM possui inferência completa, o que significa que você nunca precisa escrever uma variável de tipo. Haskell 98, a forma mais simples de Haskell, já joga isso fora em algumas circunstâncias muito raras. Geralmente, o moderno Haskell tem sido um experimento para reduzir lentamente o espaço de inferência completa, acrescentando mais poder ao HM e vendo quando os usuários reclamam. As pessoas raramente reclamam - a inferência de Haskell é muito boa.
  • Apenas subtipagem muito, muito, muito fraca . Mencionei anteriormente que o sistema de restrição do prólogo da classe de datilografia tem uma noção de subtipagem estrutural. Essa é a única forma de subtipagem em Haskell . A subtipagem é terrível para raciocínio e inferência. Isso torna cada um desses problemas significativamente mais difícil (um sistema de desigualdades em vez de um sistema de igualdades). Também é muito fácil entender mal (subclassificar é o mesmo que subtipar? É claro que não! Mas as pessoas freqüentemente confundem isso e muitos idiomas ajudam nessa confusão! Como chegamos aqui? Suponho que ninguém nunca examine o LSP.)
    • Observe recentemente (início de 2017) Steven Dolan publicou sua tese sobre o MLsub , uma variante da inferência do tipo ML e Hindley-Milner, que tem uma história de subtipagem muito boa ( veja também ). Isso não impede o que eu escrevi acima - a maioria dos sistemas de subtipagem está quebrada e tem uma inferência ruim - mas sugere que ainda hoje descobrimos algumas maneiras promissoras de que a inferência e a subtipagem completas funcionem bem. Agora, para ser totalmente claro, as noções de subtipagem de Java não são capazes de tirar proveito dos algoritmos e sistemas de Dolan. Exige repensar o que significa subtipagem.
  • Tipos de classificação mais alta . Eu falei sobre generalização anteriormente, mas mais do que mera generalização, é útil poder falar sobre tipos que possuem variáveis ​​generalizadas dentro deles . Por exemplo, um mapeamento entre estruturas de ordem superior que é inconsciente (veja parametridade ) para o que essas estruturas "contêm" tem um tipo semelhante (forall a. f a -> g a). Em HM reta você pode escrever uma função para este tipo, mas com tipos de alto classificá-lo exigir tal função como um argumento assim: mapFree :: (forall a . f a -> g a) -> Free f -> Free g. Observe que a avariável está vinculada apenas ao argumento. Isso significa que o definidor da função mapFreedecide o que aé instanciado quando a utiliza, e não o usuário mapFree.
  • Tipos existenciais . Enquanto tipos-maior pontuação nos permitem falar de quantificação universal, tipos existenciais vamos falar sobre quantificação existencial: a ideia de que apenas existe algum tipo desconhecido satisfazer algumas equações. Isso acaba sendo útil e durar mais tempo levaria muito tempo.
  • Digite famílias . Às vezes, os mecanismos da classe de tipo são inconvenientes, pois nem sempre pensamos no Prolog. Famílias de tipos vamos escrever relações funcionais diretas entre os tipos.
    • Famílias de tipo fechado . Por padrão, as famílias de tipos são abertas, o que é irritante porque significa que, embora você possa estendê-las a qualquer momento, não é possível "invertê-las" com nenhuma esperança de sucesso. Isso ocorre porque você não pode provar a injetividade , mas com famílias de tipo fechado é possível.
  • Tipos de tipos indexados e promoção de tipos . Estou ficando realmente exótico neste momento, mas estes têm uso prático de tempos em tempos. Se você quiser escrever um tipo de identificador aberto ou fechado, faça isso muito bem. Observe no fragmento a seguir que Stateé um tipo algébrico muito simples que também teve seus valores promovidos no nível de tipo. Então, posteriormente, podemos falar de construtores tipo como Handlecomo tomar argumentos em específicos tipos como State. É confuso entender todos os detalhes, mas também muito bem.

    data State = Open | Closed
    
    data Handle :: State -> * -> * where
      OpenHandle :: {- something -} -> Handle Open a
      ClosedHandle :: {- something -} -> Handle Closed a
  • Representações de tipo de tempo de execução que funcionam . Java é notório por ter apagamento de tipo e ter essa característica chover nos desfiles de algumas pessoas. No entanto, o apagamento de tipo é o caminho certo a seguir, como se você tivesse uma função getRepr :: a -> TypeRepre, no mínimo, viole a parametridade. O pior é que, se essa é uma função gerada pelo usuário, usada para desencadear coerções inseguras em tempo de execução ... então você tem uma enorme preocupação com a segurança . O Typeablesistema de Haskell permite a criação de um cofre coerce :: (Typeable a, Typeable b) => a -> Maybe b. Esse sistema depende de Typeableser implementado no compilador (e não na área do usuário) e também não pode receber uma semântica tão agradável sem o mecanismo de classe de tipo de Haskell e as leis que é garantido que ele siga.

Mais do que apenas estes, no entanto, o valor do sistema de tipos de Haskell também se relaciona com a forma como os tipos descrevem o idioma. Aqui estão alguns recursos do Haskell que agregam valor ao sistema de tipos.

  • Pureza . Haskell não permite efeitos colaterais para uma definição muito, muito, muito ampla de "efeito colateral". Isso força você a colocar mais informações em tipos, uma vez que os tipos controlam entradas e saídas e sem efeitos colaterais, tudo deve ser levado em consideração nas entradas e saídas.
    • IO . Posteriormente, Haskell precisava de uma maneira de falar sobre efeitos colaterais - já que qualquer programa real deve incluir alguns -, então uma combinação de classes, tipos superiores e tipos abstratos deu origem à noção de usar um tipo especial e superespecial chamado IO apara representar cálculos de efeito colateral que resultam em valores do tipo a. Esta é a base de um sistema de efeitos muito bom incorporado em uma linguagem pura.
  • Falta denull . Todo mundo sabe que esse nullé o erro de bilhões de dólares das linguagens de programação modernas. Tipos algébricos, em particular a capacidade de acrescentar apenas um estado "não existe" aos tipos que você possui, transformando um tipo Aem um tipo Maybe A, atenuam completamente o problema de null.
  • Recursão polimórfica . Isso permite definir funções recursivas que generalizam variáveis ​​de tipo, apesar de usá-las em tipos diferentes em cada chamada recursiva em sua própria generalização. É difícil falar sobre isso, mas especialmente útil para falar sobre tipos aninhados. Olhar para trás para o Bt atipo de antes e tentar escrever uma função para calcular o seu tamanho: size :: Bt a -> Int. Vai parecer um pouco com size (Here a) = 1e size (There bt) = 2 * size bt. Operacionalmente, isso não é muito complexo, mas observe que a chamada recursiva sizena última equação ocorre em um tipo diferente , mas a definição geral tem um bom tipo generalizado size :: Bt a -> Int. Observe que esse é um recurso que quebra a inferência total, mas se você fornecer uma assinatura de tipo, o Haskell permitirá.

Eu poderia continuar, mas esta lista deve ajudá-lo a começar.

J. Abrahamson
fonte
7
Nulo não foi um "erro" de um bilhão de dólares. Há casos em que não é possível verificar estaticamente que o ponteiro não será desreferenciado antes que qualquer significado possa existir ; ter uma tentativa de interceptação de desreferência nesse caso geralmente é melhor do que exigir que o ponteiro identifique inicialmente um objeto sem sentido. Eu acho que o maior erro relacionadas com o nulo foi ter implementações que, dada char *p = NULL;, vai prender em *p=1234, mas não vai armadilha em char *q = p+5678;nem*q = 1234;
supercat
37
Isso é citado diretamente de Tony Hoare: en.wikipedia.org/wiki/Tony_Hoare#Apologies_and_retractions . Embora eu tenha certeza de que há momentos em que nullé necessário na aritmética de ponteiros, em vez disso, interpreto isso para dizer que a aritmética de ponteiros é um lugar ruim para hospedar a semântica do seu idioma, não que nulo ainda não seja um erro.
19379 J. Abrahamson
18
@ supercat, você pode realmente escrever um idioma sem nulo. Permitir ou não é uma escolha.
Paul Draper
6
@ supercat - Esse problema também existe em Haskell, mas de uma forma diferente. Haskell é normalmente preguiçoso e imutável e, portanto, permite que você escreva p = undefineddesde que pnão seja avaliado. Mais útil, você pode colocar undefinedalgum tipo de referência mutável, novamente, desde que não a avalie. O desafio mais sério é com cálculos preguiçosos que podem não terminar, o que obviamente é indecidível. A principal diferença é que essas são falhas de programação inequívocas e nunca são usadas para expressar lógica comum.
Christian Conkle
6
@supercat Haskell carece totalmente de semântica de referência (essa é a noção de transparência referencial que implica que tudo é preservado substituindo-se as referências por seus referentes). Portanto, acho que sua pergunta está incorreta.
11788 J. Abrahamson
78
  • Inferência de tipo completo. Você pode realmente usar tipos complexos onipresentemente, sem se sentir como "Caramba, tudo o que faço é escrever assinaturas de tipo".
  • Os tipos são totalmente algébricos , o que facilita a expressão de algumas idéias complexas.
  • Haskell tem classes de tipo, que são como interfaces semelhantes, exceto que você não precisa colocar todas as implementações de um tipo no mesmo local. Você pode criar implementações de suas próprias classes de tipo para tipos de terceiros existentes, sem precisar acessar sua fonte.
  • Funções de ordem superior e recursivas tendem a colocar mais funcionalidade no âmbito do verificador de tipos. Veja o filtro , por exemplo. Em uma linguagem imperativa, você pode escrever um forloop para implementar a mesma funcionalidade, mas não terá as mesmas garantias de tipo estático, porque um forloop não tem conceito de um tipo de retorno.
  • A falta de subtipos simplifica bastante o polimorfismo paramétrico.
  • Tipos de tipo mais alto (tipos de tipos) são relativamente fáceis de especificar e usar no Haskell, o que permite criar abstrações em torno de tipos que são completamente insondáveis ​​em Java.
Karl Bielefeldt
fonte
7
Boa resposta - você poderia me dar um exemplo simples de um tipo mais elevado? Pense que isso me ajudaria a entender por que é impossível fazer em java.
phatmanace
3
Existem alguns bons exemplos aqui .
Karl Bielefeldt
3
A correspondência de padrões também é realmente importante, significa que você pode usar o tipo de um objeto para tomar decisões super facilmente.
Benjamin Gruenbaum
2
@BenjaminGruenbaum Acho que não chamaria isso de recurso de sistema de tipos.
Doval
3
Enquanto ADTs e HKTs são definitivamente parte da resposta, duvido que alguém que faça essa pergunta saiba por que eles são úteis, sugiro que as duas seções precisem ser expandidas para explicar isso
jk.
62
a :: Integer
b :: Maybe Integer
c :: IO Integer
d :: Either String Integer

Em Haskell: um número inteiro, um número inteiro que pode ser nulo, um número inteiro cujo valor veio do mundo exterior e um número inteiro que pode ser uma sequência, são todos tipos distintos - e o compilador aplicará isso . Você não pode compilar um programa Haskell que não respeite essas distinções.

(No entanto, você pode omitir as declarações de tipo. Na maioria dos casos, o compilador pode determinar o tipo mais geral para suas variáveis, o que resultará em uma compilação bem-sucedida. Isso não é legal?)

WolfeFan
fonte
11
+1 enquanto esta resposta não estiver completa, acho que é muito melhor no nível da pergunta
jk.
1
+1 Embora ajude a explicar que outras linguagens também têm Maybe(por exemplo, Java Optionale Scala Option), mas nessas linguagens é uma solução incompleta, já que você sempre pode atribuir nulla uma variável desse tipo e fazer com que seu programa exploda em execução. Tempo. Isso não pode acontecer com Haskell [1], porque não há valor nulo , então você simplesmente não pode trapacear. ([1]: na verdade, você pode gerar um erro semelhante a uma NullPointerException usando funções parciais, como fromJustquando você tem um Nothing, mas essas funções provavelmente estão desaprovadas).
Andres F.
2
"um número inteiro cujo valor veio do mundo exterior" - não IO Integerestaria mais perto de 'subprograma que, quando executado, fornece número inteiro'? Como a) no main = c >> cvalor retornado por primeiro cpode ser diferente e depois por segundo, cenquanto aterá o mesmo valor, independentemente de sua posição (contanto que estejamos em escopo único) b) existem tipos que denotam valores do mundo exterior para impor sua sanatisação (ou seja, não para colocá-los diretamente, mas verifique primeiro se a entrada do usuário está correta / não é maliciosa).
Maciej Piechotka
4
Maciej, isso seria mais preciso. Eu estava lutando pela simplicidade.
WolfeFan 17/04
30

Muitas pessoas listaram coisas boas sobre Haskell. Mas, em resposta à sua pergunta específica "por que o sistema de tipos torna os programas mais corretos?", Suspeito que a resposta seja "polimorfismo paramétrico".

Considere a seguinte função Haskell:

foobar :: x -> y -> y

Existe literalmente apenas uma maneira possível de implementar essa função. Apenas pela assinatura de tipo, posso dizer com precisão o que essa função faz, porque há apenas uma coisa possível que ela pode fazer. [OK, não exatamente, mas quase!]

Pare e pense sobre isso por um momento. Isso é realmente um grande negócio! Significa que se eu escrever uma função com essa assinatura, é realmente impossível que a função faça algo diferente do que eu pretendia. (A assinatura do tipo em si ainda pode estar errada, é claro. Nenhuma linguagem de programação jamais evitará todos os erros.)

Considere esta função:

fubar :: Int -> (x -> y) -> y

Esta função é impossível . Você literalmente não pode implementar esta função. Eu posso dizer isso apenas a partir da assinatura do tipo.

Como você pode ver, uma assinatura do tipo Haskell diz muito a você!


Compare com C #. (Desculpe, meu Java está um pouco enferrujado.)

public static TY foobar<TX, TY>(TX in1, TY in2)

Existem algumas coisas que esse método pode fazer:

  • Retornar in2como resultado.
  • Faça um loop para sempre e nunca retorne nada.
  • Lance uma exceção e nunca retorne nada.

Na verdade, Haskell também tem essas três opções. Mas o C # também oferece as opções adicionais:

  • Retorno nulo. (Haskell não possui nulo.)
  • Modifique in2antes de devolvê-lo. (Haskell não possui modificação no local.)
  • Use reflexão. (Haskell não tem reflexão.)
  • Execute várias ações de E / S antes de retornar um resultado. (Haskell não permitirá que você execute E / S, a menos que você declare que executa E / S aqui.)

A reflexão é um martelo particularmente grande; usando a reflexão, posso construir um novo TYobjeto do nada, e devolvê-lo! Posso inspecionar os dois objetos e executar ações diferentes, dependendo do que encontro. Posso fazer modificações arbitrárias nos dois objetos passados.

A E / S é um martelo igualmente grande. O código pode estar exibindo mensagens para o usuário, ou abrindo conexões com o banco de dados, ou reformatando seu disco rígido, ou qualquer outra coisa.


A foobarfunção Haskell , por outro lado, pode pegar apenas alguns dados e retorná-los inalterados. Ele não pode "examinar" os dados, porque seu tipo é desconhecido no momento da compilação. Ele não pode criar novos dados, porque ... bem, como você constrói dados de qualquer tipo possível? Você precisaria de reflexão para isso. Ele não pode executar nenhuma E / S, porque a assinatura de tipo não declara que a E / S está sendo executada. Portanto, ele não pode interagir com o sistema de arquivos ou a rede, ou mesmo executar threads no mesmo programa! (Ou seja, é 100% garantido como thread-safe).

Como você pode ver, ao não permitir que você faça um monte de coisas, Haskell está permitindo que você faça garantias muito fortes sobre o que seu código realmente faz. Tão apertado, de fato, que (para um código realmente polimórfico) geralmente há apenas uma maneira possível de as peças se encaixarem.

(Para deixar claro: ainda é possível escrever funções Haskell nas quais a assinatura de tipo não diz muito. Int -> IntPode ser qualquer coisa. Mas mesmo assim, sabemos que a mesma entrada sempre produzirá a mesma saída com 100% de certeza. Java nem garante isso!)

MathematicsOrchid
fonte
4
+1 Ótima resposta! Isso é muito poderoso e frequentemente subestimado pelos recém-chegados ao Haskell. A propósito, uma função "impossível" mais simples seria fubar :: a -> b, não? (Sim, eu estou ciente de unsafeCoerceque eu supor que nós não estamos falando de qualquer coisa com "inseguro" em seu nome, e nem deve recém-chegados se preocupar com isso:.! D)
Andres F.
Existem muitas assinaturas de tipo mais simples que você não pode escrever, sim. Por exemplo, foobar :: xé bastante unimplementable ...
MathematicalOrchid
Na verdade, você não pode tornar o código puro inseguro, mas ainda pode torná-lo multiencadeado. Suas opções são "antes de avaliar isso, avalie isso", "quando você avalia isso, você também pode avaliar isso em um encadeamento separado" e "quando você avalia isso, também pode avaliar isso, possivelmente em um thread separado ". O padrão é "faça como quiser", o que significa essencialmente "avalie o mais tarde possível".
John Dvorak
Mais prosaicamente, você pode chamar métodos de instância em in1 ou in2 que tenham efeitos colaterais. Ou você pode modificar o estado global (que, concedido, é modelado como uma ação de E / S em Haskell, mas pode não ser o que a maioria das pessoas pensa como E / S).
Doug McClean
2
@ isomorphismes O tipo x -> y -> yé perfeitamente implementável. O tipo (x -> y) -> ynão é. O tipo x -> y -> ypega duas entradas e retorna a segunda. O tipo (x -> y) -> ytem uma função que opera em x, e de alguma forma tem que fazer uma yfora dessa ...
MathematicalOrchid
17

Uma questão SO relacionada .

Suponho que você possa fazer o mesmo em haskell (ou seja, colocar coisas em strings / ints que realmente devem ser tipos de dados de primeira classe)

Não, você realmente não pode - pelo menos não da mesma maneira que Java. Em Java, esse tipo de coisa acontece:

String x = (String)someNonString;

e o Java tentará felizmente converter seu não-String como um String. Haskell não permite esse tipo de coisa, eliminando toda uma classe de erros de tempo de execução.

nullfaz parte do sistema de tipos (as Nothing), portanto, precisa ser explicitamente solicitado e tratado, eliminando toda uma outra classe de erros de tempo de execução.

Também existem muitos outros benefícios sutis - especialmente em relação à reutilização e classes de tipos - que eu não tenho o conhecimento necessário para saber o suficiente para me comunicar.

Principalmente, porém, é porque o sistema de tipos de Haskell permite muita expressividade. Você pode fazer um monte de coisas com apenas algumas regras. Considere a sempre presente árvore Haskell:

data Tree a = Leaf a | Branch (Tree a) (Tree a) 

Você definiu uma árvore binária genérica inteira (e dois construtores de dados) em uma linha de código bastante legível. Tudo usando apenas algumas regras (com tipos de soma e tipos de produto ). São 3 a 4 arquivos de código e classes em Java.

Especialmente entre aqueles propensos a reverenciar sistemas de tipos, esse tipo de concisão / elegância é altamente valorizado.

Telastyn
fonte
Entendi apenas NullPointerExceptions da sua resposta. Você poderia incluir mais exemplos?
Jesvin Jose
2
Não necessariamente verdadeiro, JLS §5.5.1 : Se T é um tipo de classe, então | S | <: | T | ou | T | <: | S |. Caso contrário, ocorrerá um erro em tempo de compilação. Portanto, o compilador não permitirá que você faça tipos inconversíveis - obviamente existem maneiras de contorná-lo.
Boris the Spider
Na minha opinião, a maneira mais simples de colocar as vantagens das classes de tipo é que elas são como interfaces que podem ser adicionadas após o fato e não "esquecem" o tipo que as está implementando. Ou seja, você pode garantir que dois argumentos para uma função tenham o mesmo tipo, diferente de interfaces, onde dois List<String>s podem ter implementações diferentes. Tecnicamente, você poderia fazer algo muito semelhante em Java adicionando um parâmetro de tipo a todas as interfaces, mas 99% das interfaces existentes não o fazem e você confundirá seus colegas.
Doval
2
@BoristheSpider True, mas as exceções de elenco quase sempre envolvem downcasting de uma superclasse para uma subclasse ou de uma interface para uma classe, e não é incomum que a superclasse seja Object.
Doval
2
Eu acho que o ponto na pergunta sobre strings não tem a ver com erros de conversão e de tipo de tempo de execução, mas o fato de que se você não quiser usar tipos, o Java não fará com que você - como, na verdade, armazene seus dados em serializado forma, abusando de strings como um anytipo ad-hoc . Haskell também não vai impedi-lo de fazer isso, já que ... bem, ele tem cordas. Haskell pode lhe dar ferramentas, não pode impedi- lo de fazer coisas estúpidas se você insistir em Greens executar um intérprete suficiente para reinventar nullem um contexto aninhado. Nenhuma língua pode.
precisa saber é o seguinte
0

Um dos "memes" sobre os quais as pessoas familiarizadas costumam falar é a coisa toda "se compilar, funcionará *" - que eu acho que está relacionada à força do sistema de tipos.

Isso ocorre principalmente com pequenos programas. Haskell impede que você cometa erros fáceis em outros idiomas (por exemplo, comparar an Int32e a Word32e algo explode), mas não impede que você cometa todos os erros.

Haskell realmente facilita muito a refatoração . Se o seu programa estava correto anteriormente e verifica tipicamente, há uma boa chance de que ainda esteja correto após pequenas alterações.

Estou tentando entender por que exatamente Haskell é melhor do que outras linguagens estaticamente tipadas nesse sentido.

Os tipos no Haskell são bastante leves, pois é fácil declarar novos tipos. Isso contrasta com uma linguagem como Rust, onde tudo é um pouco mais complicado.

Minha suposição é que é isso que as pessoas entendem por um sistema de tipos forte, mas não me é óbvio por que o Haskell é melhor.

Haskell possui muitos recursos além dos tipos simples de soma e produto; também possui tipos universalmente quantificados (por exemplo id :: a -> a). Você também pode criar tipos de registro contendo funções, o que é bastante diferente de uma linguagem como Java ou Rust.

O GHC também pode derivar algumas instâncias baseadas apenas em tipos e, desde o advento dos genéricos, você pode escrever funções genéricas entre os tipos. Isso é bastante conveniente e é mais fluente que o mesmo em Java.

Outra diferença é que Haskell tende a ter erros de tipo relativamente bons (pelo menos na escrita). A inferência de tipo de Haskell é sofisticada e é muito raro que você precise fornecer anotações de tipo para obter algo para compilar. Isso contrasta com o Rust, onde a inferência de tipo às vezes pode exigir anotações, mesmo quando o compilador pode, em princípio, deduzir o tipo.

Por fim, Haskell tem classes tipográficas, entre elas a famosa mônada. As mônadas são uma maneira particularmente agradável de lidar com erros; eles basicamente oferecem quase toda a comodidade, nullsem a horrível depuração e sem abrir mão de qualquer tipo de segurança. Portanto, a capacidade de escrever funções nesses tipos realmente importa bastante quando se trata de incentivar-nos a usá-las!

Em outras palavras, você pode escrever Java bom ou ruim, presumo que você possa fazer o mesmo em Haskell

Talvez isso seja verdade, mas está faltando um ponto crucial: o ponto em que você começa a se dar um tiro no pé em Haskell é mais além do que o momento em que você começa a se dar um tiro no pé em Java.


fonte