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ê string
conté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!
fonte
Maybe
apenas 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.Respostas:
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)
Eq
uality 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.data Bt a = Here a | There (Bt (a, a))
. Pense com cuidado nos valores válidosBt a
e observe como esse tipo funciona. É complicado!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.mtl
sistema 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.(+)
coisas juntos? AhInteger
, ok! Vamos alinhar o código certo agora!". Em sistemas mais complexos, você pode estar estabelecendo restrições mais interessantes.mtl
biblioteca inteira é baseada nessa ideia.(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 aa
variável está vinculada apenas ao argumento. Isso significa que o definidor da funçãomapFree
decide o quea
é instanciado quando a utiliza, e não o usuáriomapFree
.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 comoHandle
como tomar argumentos em específicos tipos comoState
. É confuso entender todos os detalhes, mas também muito bem.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 -> TypeRepr
e, 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 . OTypeable
sistema de Haskell permite a criação de um cofrecoerce :: (Typeable a, Typeable b) => a -> Maybe b
. Esse sistema depende deTypeable
ser 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.
IO a
para representar cálculos de efeito colateral que resultam em valores do tipoa
. Esta é a base de um sistema de efeitos muito bom incorporado em uma linguagem pura.null
. Todo mundo sabe que essenull
é 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 tipoA
em um tipoMaybe A
, atenuam completamente o problema denull
.Bt a
tipo de antes e tentar escrever uma função para calcular o seu tamanho:size :: Bt a -> Int
. Vai parecer um pouco comsize (Here a) = 1
esize (There bt) = 2 * size bt
. Operacionalmente, isso não é muito complexo, mas observe que a chamada recursivasize
na última equação ocorre em um tipo diferente , mas a definição geral tem um bom tipo generalizadosize :: 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.
fonte
char *p = NULL;
, vai prender em*p=1234
, mas não vai armadilha emchar *q = p+5678;
nem*q = 1234;
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.p = undefined
desde quep
não seja avaliado. Mais útil, você pode colocarundefined
algum 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.for
loop para implementar a mesma funcionalidade, mas não terá as mesmas garantias de tipo estático, porque umfor
loop não tem conceito de um tipo de retorno.fonte
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?)
fonte
Maybe
(por exemplo, JavaOptional
e ScalaOption
), mas nessas linguagens é uma solução incompleta, já que você sempre pode atribuirnull
a 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, comofromJust
quando você tem umNothing
, mas essas funções provavelmente estão desaprovadas).IO Integer
estaria mais perto de 'subprograma que, quando executado, fornece número inteiro'? Como a) nomain = c >> c
valor retornado por primeiroc
pode ser diferente e depois por segundo,c
enquantoa
terá 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).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:
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:
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.)
Existem algumas coisas que esse método pode fazer:
in2
como resultado.Na verdade, Haskell também tem essas três opções. Mas o C # também oferece as opções adicionais:
in2
antes de devolvê-lo. (Haskell não possui modificação no local.)A reflexão é um martelo particularmente grande; usando a reflexão, posso construir um novo
TY
objeto 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
foobar
funçã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 -> Int
Pode 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!)fonte
fubar :: a -> b
, não? (Sim, eu estou ciente deunsafeCoerce
que 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)foobar :: x
é bastante unimplementable ...x -> y -> y
é perfeitamente implementável. O tipo(x -> y) -> y
não é. O tipox -> y -> y
pega duas entradas e retorna a segunda. O tipo(x -> y) -> y
tem uma função que opera emx
, e de alguma forma tem que fazer umay
fora dessa ...Uma questão SO relacionada .
Não, você realmente não pode - pelo menos não da mesma maneira que Java. Em Java, esse tipo de coisa acontece:
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.
null
faz parte do sistema de tipos (asNothing
), 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:
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.
fonte
interface
s 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 deinterface
s, onde doisList<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.Object
.any
tipo 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 reinventarnull
em um contexto aninhado. Nenhuma língua pode.Isso ocorre principalmente com pequenos programas. Haskell impede que você cometa erros fáceis em outros idiomas (por exemplo, comparar an
Int32
e aWord32
e 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.
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.
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,
null
sem 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!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