The type checker is allowing a very wrong type replacement, and the program still compiles
The only way this could possibly compile is if there exists a Num (Float,Float)
instance. This isn't provided by the standard library, although it is possible that one of the libraries you're using added it for some insane reason. Try loading up your project in ghci and see if 10 :: (Float,Float)
works, then try :i Num
to find out where the instance is coming from, and then yell at whoever defined it.
Addendum: There is no way to turn off instances. There isn't even a way to not export them from a module. If this were possible, it would lead to even more confusing code. The only real solution here is to not define instances like that.
Haskell's type checker is being reasonable. The problem is that the authors of a library you're using have done something... less reasonable.
The brief answer is: Yes, 10 :: (Float, Float)
is perfectly valid if there's an instance Num (Float, Float)
. There's nothing "very wrong" about it from the compiler's or the language's perspective. It just doesn't square with our intuition about what numeric literals do. Since you're used to the type system catching the sort of error you made, you're justifiably surprised and disappointed!
Num
instances and the fromInteger
problem
You're surprised that the compiler accepts 10 :: Coord
, i.e. 10 :: (Float, Float)
. It's reasonable to assume that numeric literals like 10
will be inferred to have "numeric" types. Out of the box, numeric literals can be interpreted as Int
, Integer
, Float
, or Double
. A tuple of numbers, with no other context, doesn't seem like a number in the way those four types are numbers. We're not talking about Complex
.
Fortunately or unfortunately, however, Haskell is a very flexible language. The standard specifies that an integer literal like 10
will be interpreted as fromInteger 10
, which has type Num a => a
. So 10
could be inferred as any type that's had a Num
instance written for it. I explain this in a bit more detail in another answer.
So when you posted your question, an experienced Haskeller immediately spotted that for 10 :: (Float, Float)
to be accepted, there must be an instance like Num a => Num (a, a)
or Num (Float, Float)
. There's no such instance in the Prelude
, so it must have been defined somewhere else. Using :i Num
, you quickly spotted where it came from: the gloss
package.
Type synonyms and orphan instances
But hold on a minute. You're not using any gloss
types in this example; why did the instance in gloss
affect you? The answer comes in two steps.
First, a type synonym introduced with the keyword type
does not create a new type. In your module, writing Coord
is simply shorthand for (Float, Float)
. Likewise in Graphics.Gloss.Data.Point
, Point
means (Float, Float)
. In other words, your Coord
and gloss
's Point
are literally equivalent.
So when the gloss
maintainers chose to write instance Num Point where ...
, they also made your Coord
type an instance of Num
. That's equivalent to instance Num (Float, Float) where ...
or instance Num Coord where ...
.
(By default, Haskell doesn't allow type synonyms to be class instances. The gloss
authors had to enable a pair of language extensions, TypeSynonymInstances
and FlexibleInstances
, to write the instance.)
Second, this is surprising because it's an orphan instance, i.e. an instance declaration instance C A
where both C
and A
are defined in other modules. Here it's particularly insidious because each part involved, i.e. Num
, (,)
, and Float
, comes from the Prelude
and is likely to be in scope everywhere.
Your expectation is that Num
is defined in Prelude
, and tuples and Float
are defined in Prelude
, so everything about how those three things work is defined in Prelude
. Why would importing a completely different module change anything? Ideally it wouldn't, but orphan instances break that intuition.
(Note that GHC warns about orphan instances—the authors of gloss
specifically overrode that warning. That should have raised a red flag and prompted at least a warning in the documentation.)
Class instances are global and cannot be hidden
Furthermore, class instances are global: any instance defined in any module that is transitively imported from your module will be in context and available to the typechecker when doing instance resolution. This makes global reasoning convenient, because we can (usually) assume that a class function like (+)
will always be the same for a given type. However, it also means that local decisions have global effects; defining a class instance irrevocably changes the context of downstream code, with no way to mask or conceal it behind module boundaries.
You cannot use import lists to avoid importing instances. Similarly, you cannot avoid exporting instances from modules you define.
This is a problematic and much-discussed area of the Haskell language design. There's a fascinating discussion of related issues in this reddit thread. See, for instance, Edward Kmett's comment on allowing visibility control for instances: "You basically throw out the correctness of almost all of the code I have written."
(By the way, as this answer demonstrated, you can break the global-instance assumption in some regards by using orphan instances!)
What to do—for library implementers
Think twice before implementing Num
. You cannot work around the fromInteger
problem—no, defining fromInteger = error "not implemented"
does not make it better. Will your users be confused or surprised—or worse, never notice—if their integer literals are accidentally inferred to have the type you're instantiating? Is providing (*)
and (+)
that critical—particularly if you have to hack it?
Consider using alternative arithmetical operators defined in a library like Conal Elliott's vector-space
(for types of kind *
) or Edward Kmett's linear
(for types of kind * -> *
). This is what I tend to do myself.
Use -Wall
. Do not implement orphan instances, and do not disable the orphan instance warning.
Alternately, follow the lead of linear
and many other well-behaved libraries, and provide orphan instances in a separate module ending in .OrphanInstances
or .Instances
. And do not import that module from any other module. Then users can import the orphans explicitly if they would like.
If you find yourself defining orphans, consider asking upstream maintainers to implement them instead, if possible and appropriate. I used to frequently write the orphan instance Show a => Show (Identity a)
, until they added it to transformers
. I may even have raised a bug report about it; I don't remember.
What to do—for library consumers
You don't have many options. Reach out—politely and constructively!—to the library maintainers. Point them to this question. They may have had some special reason to write the problematic orphan, or they may just not realize.
More broadly: Be aware of this possibility. This is one of the few areas of Haskell where there are true global effects; you'd have to check that every module you import, and every module those modules import, doesn't implement orphan instances. Type annotations may sometimes alert you to problems, and of course you can use :i
in GHCi to check.
Define your own newtype
s instead of type
synonyms if it's important enough. You can be pretty sure nobody will mess with them.
If you're having frequent problems deriving from an open-source library, you can of course make your own version of the library, but maintenance can quickly become a headache.