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ância Num (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!
Numinstâncias e o fromIntegerproblema
Você está surpreso que o compilador aceita 10 :: Coord, ou seja 10 :: (Float, Float). É razoável supor que literais numéricos como 10serão inferidos como tendo tipos "numéricos". Fora da caixa, literais numéricos pode ser interpretado como Int, Integer, Float, ou Double. 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 sobre Complex.
Feliz ou infelizmente, porém, Haskell é uma linguagem muito flexível. O padrão especifica que um literal inteiro like 10será interpretado como fromInteger 10, que tem tipo Num a => a. Portanto, 10pode ser inferido como qualquer tipo que tenha uma Numinstâ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 como Num a => Num (a, a)ou Num (Float, Float). Não existe tal instância no Prelude, então ela deve ter sido definida em outro lugar. Usando :i Num, você percebeu rapidamente de onde veio: o glosspacote.
Digite sinônimos e instâncias órfãs
mas espere um minuto. Você não está usando nenhum glosstipo neste exemplo; por que a instância glossafetou você? A resposta vem em duas etapas.
Primeiro, um sinônimo de tipo introduzido com a palavra-chave typenão cria um novo tipo . Em seu módulo, escrever Coordé simplesmente um atalho para (Float, Float). Da mesma forma em Graphics.Gloss.Data.Point, Pointsignifica (Float, Float). Em outras palavras, seus Coorde gloss's Pointsão, literalmente equivalente.
Portanto, quando os glossmantenedores decidiram escrever instance Num Point where ..., eles também transformaram seu Coordtipo em uma instância de Num. Isso é equivalente a instance Num (Float, Float) where ...ou instance Num Coord where ....
(Por padrão, Haskell não permite que sinônimos de tipo sejam instâncias de classe. Os glossautores tiveram que habilitar um par de extensões de linguagem, TypeSynonymInstancese FlexibleInstances, 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 Aonde Ce Asão definidos em outros módulos. Aqui é particularmente insidiosa porque cada parte envolvida, ou seja Num, (,)e Float, vem do Preludee é provável que seja no escopo em todos os lugares.
Sua expectativa é que Numseja definido em Prelude, e tuplas e Floatsejam definidos em Prelude, então tudo sobre como essas três coisas funcionam é definido em Prelude. 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 glosssubstituí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 o fromIntegerproblema, não, definindo fromInteger = 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 Kmett linear(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 lineare de muitas outras bibliotecas bem comportadas e forneça instâncias órfãs em um módulo separado terminando em .OrphanInstancesou .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 adicionassem transformers. 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 :ino GHCi para verificar.
Defina seus próprios newtypes em vez de typesinô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.