Instâncias órfãs em Haskell

86

Ao compilar meu aplicativo Haskell com a -Wallopção, GHC reclama sobre instâncias órfãs, por exemplo:

Publisher.hs:45:9:
    Warning: orphan instance: instance ToSElem Result

A classe de tipo ToSElemnão é minha, ela é definida por HStringTemplate .

Agora eu sei como consertar isso (mover a declaração da instância para o módulo onde o Resultado é declarado) e sei por que o GHC prefere evitar as instâncias órfãs , mas ainda acredito que meu jeito é melhor. Eu não me importo se o compilador é incomodado - ao invés disso do que eu.

O motivo pelo qual desejo declarar minhas ToSEleminstâncias no módulo Publisher é porque é o módulo Publisher que depende de HStringTemplate, não dos outros módulos. Estou tentando manter uma separação de interesses e evitar que todos os módulos dependam do HStringTemplate.

Achei que uma das vantagens das classes de tipo de Haskell, quando comparadas, por exemplo, às interfaces Java, é que elas são abertas em vez de fechadas e, portanto, as instâncias não precisam ser declaradas no mesmo lugar que o tipo de dados. O conselho do GHC parece ser ignorar isso.

Portanto, o que estou procurando é alguma validação de que meu pensamento é correto e que eu teria justificativa para ignorar / suprimir esse aviso, ou um argumento mais convincente contra fazer as coisas do meu jeito.

Dan Dyer
fonte
A discussão nas respostas e comentários ilustra que há uma grande diferença entre definir instâncias órfãs em um executável , como você está fazendo, e em uma biblioteca que é exposta a outros. Esta pergunta tremendamente popular ilustra como as instâncias órfãs podem ser confusas para os usuários finais de uma biblioteca que as define.
Christian Conkle de

Respostas:

94

Eu entendo por que você quer fazer isso, mas, infelizmente, pode ser apenas uma ilusão que as aulas de Haskell pareçam ser "abertas" da maneira que você diz. Muitas pessoas acham que a possibilidade de fazer isso é um bug na especificação Haskell, pelos motivos que explicarei a seguir. De qualquer forma, se realmente não for apropriado para a instância, você precisa ser declarado no módulo onde a classe é declarada ou no módulo onde o tipo é declarado, provavelmente é um sinal de que você deve usar um newtypeou algum outro invólucro em torno do seu tipo.

As razões pelas quais as instâncias órfãs precisam ser evitadas são muito mais profundas do que a conveniência do compilador. Este assunto é bastante controverso, como você pode ver nas outras respostas. Para equilibrar a discussão, vou explicar o ponto de vista de que nunca se deve escrever instâncias órfãs, que eu acho que é a opinião da maioria entre os Haskellers experientes. Minha opinião está em algum lugar no meio, o que explicarei no final.

O problema decorre do fato de que, quando mais de uma declaração de instância existe para a mesma classe e tipo, não há mecanismo no Haskell padrão para especificar qual usar. Em vez disso, o programa é rejeitado pelo compilador.

O efeito mais simples disso é que você poderia ter um programa funcionando perfeitamente que iria parar de compilar repentinamente por causa de uma alteração que outra pessoa faz em alguma dependência remota do seu módulo.

Pior ainda, é possível que um programa em funcionamento comece a falhar no tempo de execução devido a uma mudança distante. Você pode estar usando um método que está assumindo que vem de uma determinada declaração de instância e pode ser substituído silenciosamente por uma instância diferente que é diferente o suficiente para fazer com que seu programa comece a falhar inexplicavelmente.

As pessoas que desejam garantias de que esses problemas nunca acontecerão com elas devem seguir a regra de que se alguém, em qualquer lugar, já declarou uma instância de uma determinada classe para um determinado tipo, nenhuma outra instância deve ser declarada novamente em qualquer programa escrito por qualquer um. Obviamente, existe a solução alternativa de usar um newtypepara declarar uma nova instância, mas isso sempre é pelo menos um pequeno inconveniente e, às vezes, um grande inconveniente. Portanto, nesse sentido, aqueles que escrevem instâncias órfãs intencionalmente estão sendo bastante indelicados.

Então, o que deve ser feito sobre esse problema? O campo de instância anti-órfã diz que o aviso do GHC é um bug, precisa ser um erro que rejeita qualquer tentativa de declarar uma instância órfã. Nesse ínterim, devemos exercer autodisciplina e evitá-los a todo custo.

Como você viu, existem aqueles que não estão tão preocupados com esses problemas potenciais. Na verdade, eles encorajam o uso de instâncias órfãs como uma ferramenta para separação de interesses, como você sugere, e dizem que deve-se apenas ter certeza, caso a caso, de que não há problema. Já fui incomodado várias vezes por casos de órfãos de outras pessoas para estar convencido de que essa atitude é muito arrogante.

Acho que a solução certa seria adicionar uma extensão ao mecanismo de importação de Haskell que controlaria a importação de instâncias. Isso não resolveria os problemas completamente, mas ajudaria a proteger nossos programas contra os danos das instâncias órfãs que já existem no mundo. E então, com o tempo, posso me convencer de que, em certos casos limitados, talvez uma instância órfã não seja tão ruim. (E essa mesma tentação é a razão de alguns no campo da instância anti-órfã se oporem à minha proposta.)

Minha conclusão de tudo isso é que, pelo menos por enquanto, eu recomendo fortemente que você evite declarar quaisquer instâncias órfãs, para ter consideração para com os outros, se não por outro motivo. Use um newtype.

Yitz
fonte
4
Em particular, isso é cada vez mais um problema com o crescimento das bibliotecas. Com mais de 2.200 bibliotecas em Haskell e 10s de milhares de módulos individuais, o risco de pegar instâncias aumenta dramaticamente.
Don Stewart
16
Re: "Acho que a solução certa seria adicionar uma extensão ao mecanismo de importação de Haskell que controlaria a importação de instâncias" Caso essa ideia interesse a alguém, pode valer a pena olhar para a linguagem Scala para um exemplo; ele possui recursos muito semelhantes a este para controlar o escopo de 'implícitos', que podem ser usados ​​de forma muito semelhante a instâncias de typeclass.
Matt de
5
Meu software é um aplicativo e não uma biblioteca, então a possibilidade de causar problemas para outros desenvolvedores é praticamente zero. Você poderia considerar o módulo Publisher o aplicativo e o resto dos módulos como uma biblioteca, mas se eu fosse distribuir a biblioteca, seria sem o Publisher e, portanto, as instâncias órfãs. Mas se eu movesse as instâncias para os outros módulos, a biblioteca seria enviada com uma dependência desnecessária de HStringTemplate. Portanto, neste caso, acho que os órfãos estão bem, mas ouvirei seu conselho se encontrar o mesmo problema em um contexto diferente.
Dan Dyer de
1
Parece uma abordagem razoável. A única coisa a observar é se o autor de um módulo importado adiciona essa instância em uma versão posterior. Se essa instância for igual à sua, você precisará excluir sua própria declaração de instância. Se essa instância for diferente da sua, você precisará colocar um wrapper newtype em torno do seu tipo - o que pode ser uma refatoração significativa do seu código.
Yitz
@Matt: de fato, surpreendentemente Scala acerta esse ponto onde Haskell não acerta! (exceto, é claro, que Scala não tem sintaxe de primeira classe para máquinas de classe de tipo, o que é ainda pior ...)
Erik Kaplun
44

Vá em frente e suprima este aviso!

Você está em boa companhia. Conal faz isso em "TypeCompose". "chp-mtl" e "chp-transformers" fazem isso, "control-monad-exception-mtl" e "control-monad-exception-monadsfd" fazem isso, etc.

btw você provavelmente já sabe disso, mas para aqueles que não sabem e tropeçam na sua pergunta em uma pesquisa:

{-# OPTIONS_GHC -fno-warn-orphans #-}

Editar:

Eu reconheço os problemas que Yitz mencionou em sua resposta como problemas reais. No entanto, não vejo o uso de instâncias órfãs como um problema também, e tento escolher o "menor de todos os males", que é melhor usar instâncias órfãs com prudência.

Usei apenas um ponto de exclamação em minha resposta curta porque sua pergunta mostra que você já está bem ciente dos problemas. Caso contrário, eu teria ficado menos entusiasmado :)

Um pouco de diversão, mas o que eu acredito é a solução perfeita em um mundo perfeito sem concessões:

Eu acredito que os problemas que Yitz menciona (não saber qual instância é escolhida) poderiam ser resolvidos em um sistema de programação "holístico" onde:

  • Você não está editando meros arquivos de texto primitivamente, mas sim auxiliado pelo ambiente (por exemplo, o autocompletar de código apenas sugere coisas de tipos relevantes, etc.)
  • A linguagem de "nível inferior" não tem suporte especial para classes de tipo e, em vez disso, as tabelas de funções são transmitidas explicitamente
  • Mas, o ambiente de programação de "nível superior" exibe o código de maneira semelhante a como Haskell é apresentado agora (você geralmente não verá as tabelas de funções passadas adiante) e escolhe as classes de tipo explícitas para você quando elas são óbvias (para exemplo, todos os casos de Functor têm apenas uma escolha) e quando há vários exemplos (zipando a lista Applicative ou list-monad Applicative, First / Last / lift talvez Monoid), ele permite que você escolha qual instância usar.
  • Em qualquer caso, mesmo quando a instância foi escolhida para você automaticamente, o ambiente permite que você veja facilmente qual instância foi usada, com uma interface fácil (um hiperlink ou interface de foco ou algo assim)

De volta do mundo da fantasia (ou esperançosamente do futuro), agora: Eu recomendo tentar evitar instâncias órfãs enquanto ainda as usa quando você "realmente precisa"

Yairchu
fonte
5
Sim, mas sem dúvida cada uma dessas ocorrências é um erro de alguma ordem. As instâncias ruins em control-monad-exception-mtl e mônadas-fd para Either vêm à mente. Seria menos intrusivo se cada um desses módulos fosse forçado a definir seus próprios tipos ou fornecer invólucros de novo tipo. Quase todas as instâncias órfãs são uma dor de cabeça esperando para acontecer, e se nada mais exigirá sua vigilância constante para garantir que sejam importadas ou não conforme apropriado.
Edward KMETT
2
Obrigado. Acho que vou usá-los nesta situação particular, mas graças a Yitz agora tenho uma melhor avaliação dos problemas que eles podem causar.
Dan Dyer de
37

Instâncias órfãs são um incômodo, mas, em minha opinião, às vezes são necessárias. Costumo combinar bibliotecas onde um tipo vem de uma biblioteca e uma classe vem de outra biblioteca. É claro que não se pode esperar que os autores dessas bibliotecas forneçam instâncias para todas as combinações concebíveis de tipos e classes. Então, eu tenho que provê-los, e eles são órfãos.

A ideia de que você deve envolver o tipo em um novo tipo quando precisa fornecer uma instância é uma ideia com mérito teórico, mas é muito tediosa em muitas circunstâncias; é o tipo de ideia apresentada por pessoas que não escrevem código Haskell para viver. :)

Então vá em frente e forneça instâncias órfãs. Eles são inofensivos.
Se você conseguir travar o ghc com instâncias órfãs, isso é um bug e deve ser relatado como tal. (O bug que o ghc teve / tem sobre não detectar várias instâncias não é tão difícil de corrigir.)

Mas esteja ciente de que em algum momento no futuro outra pessoa poderá adicionar alguma instância como você já fez e você poderá obter um erro (em tempo de compilação).

agosto
fonte
2
Um bom exemplo é (Ord k, Arbitrary k, Arbitrary v) ⇒ Arbitrary (Map k v)ao usar QuickCheck.
Erik Kaplun
17

Nesse caso, acho que o uso de instâncias órfãs está bem. A regra geral para mim é - você pode definir uma instância se "possuir" a typeclass ou se "possuir" o tipo de dados (ou algum componente dele - ou seja, uma instância para Maybe MyData também é adequada, pelo menos às vezes). Dentro dessas restrições, onde você decide colocar a instância é da sua conta.

Há mais uma exceção - se você não possui a typeclass ou o tipo de dados, mas está produzindo um binário e não uma biblioteca, tudo bem também.

sclv
fonte
5

(Eu sei que estou atrasado para a festa, mas isso ainda pode ser útil para outras pessoas)

Você poderia manter as instâncias órfãs em seu próprio módulo; então, se alguém importar esse módulo, é especificamente porque precisa deles e pode evitar importá-los se causar problemas.

Trystan Spangler
fonte
3

Junto com essas linhas, eu entendo a posição das bibliotecas WRT do campo de instância anti-órfã, mas para destinos executáveis, as instâncias órfãs não deveriam servir?

mxc
fonte
3
Em termos de ser indelicado com os outros, você está certo. Mas você está se abrindo para problemas futuros em potencial se a mesma instância for definida no futuro em algum lugar de sua cadeia de dependências. Portanto, neste caso, cabe a você decidir se vale a pena o risco.
Yitz
5
Em quase todos os casos de implementação de uma instância órfã em um executável, é para preencher uma lacuna que você gostaria que já tivesse sido definida para você. Portanto, se a instância aparecer no upstream, o erro de compilação resultante é apenas um sinal útil para dizer que você pode remover sua declaração da instância.
Ben