Ao tentar depurar um problema em meu programa (2 círculos com raio igual estão sendo desenhados em tamanhos diferentes usando o Gloss *
), me deparei com uma situação estranha. Em meu arquivo que manipula objetos, tenho a seguinte definição para um Player
:
type Coord = (Float,Float)
data Obj = Player { oPos :: Coord, oDims :: Coord }
e em meu arquivo principal, que importa Objects.hs, tenho a seguinte definição:
startPlayer :: Obj
startPlayer = Player (0,0) 10
Isso aconteceu porque eu adicionei e alterei campos para o jogador e esqueci de atualizar startPlayer
depois (suas dimensões foram determinadas por um único número para representar um raio, mas eu mudei para um Coord
para representar (largura, altura); caso eu algum dia faça o jogador objetar um não-círculo).
O incrível é que o código acima é compilado e executado, apesar do segundo campo ser do tipo errado.
A princípio pensei que talvez houvesse versões diferentes dos arquivos abertos, mas qualquer alteração em qualquer arquivo se refletia no programa compilado.
Em seguida, pensei que talvez startPlayer
não estivesse sendo usado por algum motivo. startPlayer
Porém, comentar produz um erro do compilador, e ainda mais estranho, alterar o 10
in startPlayer
causa uma resposta apropriada (altera o tamanho inicial do Player
); novamente, apesar de ser do tipo errado. Para ter certeza de que ele está lendo a definição de dados corretamente, inseri um erro de digitação no arquivo e ocorreu um erro; então estou olhando para o arquivo correto.
Tentei colar as 2 trechos acima em seu próprio arquivo, e ele cuspiu o erro esperado que o segundo campo de Player
em startPlayer
está incorreto.
O que poderia permitir que isso acontecesse? Você pensaria que isso é exatamente o que o verificador de tipo de Haskell deve evitar.
*
A resposta ao meu problema original, dois círculos de raio supostamente igual sendo desenhados em tamanhos diferentes, era que um dos raios era na verdade negativo.
Point
anewtype
ou use outros nomes de operador alalinear
)Respostas:
A única maneira de compilar isso é se houver uma
Num (Float,Float)
instância. Isso não é fornecido pela biblioteca padrão, embora seja possível que uma das bibliotecas que você está usando o tenha adicionado por algum motivo insano. Tente carregar seu projeto no ghci e ver se10 :: (Float,Float)
funciona, depois tente:i Num
descobrir de onde a instância está vindo e grite com quem a definiu.Adendo: não há como desativar as instâncias. Não há nem uma maneira de não exportá-los de um módulo. Se isso fosse possível, levaria a um código ainda mais confuso. A única solução real aqui é não definir instâncias como essa.
fonte
10 :: (Float, Float)
produz(10.0,10.0)
e:i Num
contém a linhainstance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’
(Point
é o alias de Gloss para Coord). Seriamente? Obrigado. Isso me salvou de uma noite sem dormir.Num
onde faz sentido, como umAngle
tipo de dados que restringeDouble
entre-pi
epi
, ou se alguém quisesse escrever um tipo de dados representando quatérnions ou algum outro tipo numérico mais complexo, esse recurso é muito conveniente. Ele também segue as mesmas regras queString
/Text
/ByteString
, permitir essas instâncias faz sentido do ponto de vista de facilidade de uso, mas pode ser mal utilizado como neste caso.newtype
declaração para emCoord
vez de atype
.Num (Float, Float)
ou mesmo(Floating a) => Num (a,a)
não exigiria a extensão, mas resultaria no mesmo comportamento.O verificador de tipo de Haskell está sendo razoável. O problema é que os autores de uma biblioteca que você está usando fizeram algo ... menos razoável.
A resposta breve é: Sim,
10 :: (Float, Float)
é perfeitamente válido se houver uma instânciaNum (Float, Float)
. Não há nada de "muito errado" nisso da perspectiva do compilador ou da linguagem. Simplesmente não se enquadra em nossa intuição sobre o que os literais numéricos fazem. Como você está acostumado com o sistema de tipos que detecta o tipo de erro que você cometeu, você está justificadamente surpreso e desapontado!Num
instâncias e ofromInteger
problemaVocê está surpreso que o compilador aceita
10 :: Coord
, ou seja10 :: (Float, Float)
. É razoável supor que literais numéricos como10
serão inferidos como tendo tipos "numéricos". Fora da caixa, literais numéricos pode ser interpretado comoInt
,Integer
,Float
, ouDouble
. Uma tupla de números, sem nenhum outro contexto, não parece um número da mesma forma que esses quatro tipos são números. Não estamos falando sobreComplex
.Feliz ou infelizmente, porém, Haskell é uma linguagem muito flexível. O padrão especifica que um literal inteiro like
10
será interpretado comofromInteger 10
, que tem tipoNum a => a
. Portanto,10
pode ser inferido como qualquer tipo que tenha umaNum
instância escrita para ele. Eu explico isso um pouco mais detalhadamente em outra resposta .Então, quando você postou sua pergunta, um Haskeller experiente percebeu imediatamente que, para
10 :: (Float, Float)
ser aceita, deve haver uma instância comoNum a => Num (a, a)
ouNum (Float, Float)
. Não existe tal instância noPrelude
, então ela deve ter sido definida em outro lugar. Usando:i Num
, você percebeu rapidamente de onde veio: ogloss
pacote.Digite sinônimos e instâncias órfãs
mas espere um minuto. Você não está usando nenhum
gloss
tipo neste exemplo; por que a instânciagloss
afetou você? A resposta vem em duas etapas.Primeiro, um sinônimo de tipo introduzido com a palavra-chave
type
não cria um novo tipo . Em seu módulo, escreverCoord
é simplesmente um atalho para(Float, Float)
. Da mesma forma emGraphics.Gloss.Data.Point
,Point
significa(Float, Float)
. Em outras palavras, seusCoord
egloss
'sPoint
são, literalmente equivalente.Portanto, quando os
gloss
mantenedores decidiram escreverinstance Num Point where ...
, eles também transformaram seuCoord
tipo em uma instância deNum
. Isso é equivalente ainstance Num (Float, Float) where ...
ouinstance Num Coord where ...
.(Por padrão, Haskell não permite que sinônimos de tipo sejam instâncias de classe. Os
gloss
autores tiveram que habilitar um par de extensões de linguagem,TypeSynonymInstances
eFlexibleInstances
, para escrever a instância.)Em segundo lugar, isso é surpreendente porque é uma instância órfã , ou seja, uma declaração de instância
instance C A
ondeC
eA
são definidos em outros módulos. Aqui é particularmente insidiosa porque cada parte envolvida, ou sejaNum
,(,)
eFloat
, vem doPrelude
e é provável que seja no escopo em todos os lugares.Sua expectativa é que
Num
seja definido emPrelude
, e tuplas eFloat
sejam definidos emPrelude
, então tudo sobre como essas três coisas funcionam é definido emPrelude
. Por que importar um módulo completamente diferente mudaria alguma coisa? Idealmente, não seria, mas instâncias órfãs quebram essa intuição.(Observe que o GHC avisa sobre instâncias órfãs - os autores de
gloss
substituíram especificamente esse aviso. Isso deveria ter levantado uma bandeira vermelha e gerado pelo menos um aviso na documentação.)As instâncias de classe são globais e não podem ser ocultadas
Além disso, as instâncias de classe são globais : qualquer instância definida em qualquer módulo que é importado transitivamente de seu módulo estará no contexto e disponível para o inspetor de tipos ao fazer a resolução da instância. Isso torna o raciocínio global conveniente, porque podemos (geralmente) assumir que uma função de classe como
(+)
será sempre a mesma para um determinado tipo. No entanto, também significa que as decisões locais têm efeitos globais; definir uma instância de classe muda irrevogavelmente o contexto do código downstream, sem nenhuma maneira de mascará-lo ou ocultá-lo atrás dos limites do módulo.Você não pode usar listas de importação para evitar a importação de instâncias . Da mesma forma, você não pode evitar a exportação de instâncias de módulos que você define.
Esta é uma área problemática e muito discutida do design da linguagem Haskell. Há uma discussão fascinante de questões relacionadas neste tópico do reddit . Veja, por exemplo, o comentário de Edward Kmett sobre permitir o controle de visibilidade para instâncias: "Você basicamente descarta a exatidão de quase todo o código que escrevi."
(A propósito, como esta resposta demonstrou , você pode quebrar a suposição de instância global em alguns aspectos usando instâncias órfãs!)
O que fazer - para implementadores de biblioteca
Pense duas vezes antes de implementar
Num
. Você não pode contornar ofromInteger
problema, não, definindofromInteger = error "not implemented"
que não torná-lo melhor. Seus usuários ficarão confusos ou surpresos - ou pior, nunca perceberão - se seus literais inteiros forem acidentalmente inferidos como tendo o tipo que você está instanciando? Fornecer(*)
e(+)
isso é crítico - especialmente se você tiver que hackear?Considere o uso de operadores aritméticos alternativos definidos em uma biblioteca como Conal Elliott
vector-space
(para tipos de tipo*
) ou Edward Kmettlinear
(para tipos de tipo* -> *
). Isso é o que eu mesmo costumo fazer.Use
-Wall
. Não implemente instâncias órfãs e não desative o aviso de instância órfã.Como alternativa, siga o exemplo de
linear
e de muitas outras bibliotecas bem comportadas e forneça instâncias órfãs em um módulo separado terminando em.OrphanInstances
ou.Instances
. E não importe esse módulo de qualquer outro módulo . Então, os usuários podem importar os órfãos explicitamente, se quiserem.Se você se descobrir definindo órfãos, considere pedir aos mantenedores originais para implementá-los, se possível e apropriado. Eu costumava escrever frequentemente a instância órfã
Show a => Show (Identity a)
, até que a adicionassemtransformers
. Posso até ter levantado um relatório de bug sobre isso; Não me lembro.O que fazer - para consumidores de bibliotecas
Você não tem muitas opções. Alcance - educada e construtivamente! - os mantenedores da biblioteca. Mostre-lhes esta questão. Eles podem ter tido algum motivo especial para escrever sobre o órfão problemático, ou podem simplesmente não perceber.
De forma mais ampla: esteja ciente dessa possibilidade. Esta é uma das poucas áreas de Haskell onde existem verdadeiros efeitos globais; você teria que verificar se cada módulo importado e cada módulo importado por esses módulos não implementa instâncias órfãs. As anotações de tipo podem, às vezes, alertá-lo sobre problemas e, claro, você pode usar
:i
no GHCi para verificar.Defina seus próprios
newtype
s em vez detype
sinônimos se for importante o suficiente. Você pode ter certeza de que ninguém vai mexer com eles.Se você está tendo problemas frequentes derivados de uma biblioteca de código aberto, é claro que pode fazer sua própria versão da biblioteca, mas a manutenção pode rapidamente se tornar uma dor de cabeça.
fonte