I think I may have asked this on Haskell-Cafe at some point, but damned if I can find the answer now... So I'm asking it again here, so hopefully in future I can find the answer!

Haskell is fantastic at dealing with parametric polymorphism. But the trouble is that not everything is parametric. As a trivial example, suppose we want to fetch the first element of data out of a container. For a parametric type, that's trivial:

Now try and write an instance for ByteString. You can't. Its type doesn't mention the element type. You also cannot write an instance for Set, because it requires an Ord constraint - but the class head doesn't mention the element type, so you cannot constrain it.

This doesn't work at all. It says that if we have a function from the element type of f to the element type of f2, then we can turn an f into an f2. So far so good. However, there is apparently no way to demand that f and f2 are the same sort of container!

But we do not have fmap :: (x -> y) -> IO x -> [y]. That is quite impossible. But the class definition above allows it.

Does anybody know how to explain to the type system what I actually meant?

Edit

The above works by defining a way to compute an element type from a container type. What happens if you try to do it the other way around? Define a function to compute a container type from an element type? Does that work out any easier?

2 Answers
2

Well, the problem is that it's not clear what the revised Functor is supposed to mean. For instance, consider ByteString. A ByteString can only be mapped by replacing each Word8 element with an element of the same type. But Functor is for parametric mappable structures. There are really two conflicting notions of mapping here:

Rigid mapping (i.e. transforming each element of a structure without changing its type)

Parametric mapping (i.e. transforming each element of a structure to any type)

So, in this case, you can't explain to the type system what you meant, because it doesn't make much sense. You can, however, change what you mean :)

As far as parametric mapping, there are multiple ways to do it. The simplest way would be to retain the current Functor as-is. Together, these classes covers structures like ByteString, [], Seq, and so on. However, they all fall down on Set and Map, because of the Ord constraint on values. Happily, the constraint kinds extension coming in GHC 7.4 lets us fix this problem:

Here, we're saying that every instance should have an associated typeclass constraint. For instance, the Set instance will have Element Set a = Ord a, to denote that Sets can only be constructed if an Ord instance is available for the type. Anything that can appear on the left hand of => is accepted. We can define our previous instances exactly as they were, but we can also do Set and Map:

However, it's pretty annoying to have to use two separate interfaces for rigid mapping and restricted parametric mapping. In fact, isn't the latter a generalisation of the former? Consider the difference between Set, which can only contain instances of Ord, and ByteString, which can only contain Word8s. Surely we can express that as just another constraint?

We apply the same trick done to HasFirst (i.e. give instances for the whole structure and use a type family to expose the element type), and introduce a new associated constraint family:

The idea here is that Result f a r expresses the constraints it needs on the value type (like Ord a), and also constrains the resulting container type however it needs to; presumably, to ensure it has the type of a same-sort-of-container of as. For instance, Result [a] b r will presumably require that r is [b], and Result ByteString b r will require that b is Word8, and r is ByteString.

Type families already give us what we need to express "is" here: a type equality constraint. We can say (a ~ b) => ... to require that a and b are the same type. We can, of course, use this in constraint family definitions. So, we have everything we need; on to the instances:

Still struggling to comprehend exactly what is going on here... Logically, it's simple. We want a map function that accepts any legal input type, and produces any legal output type. The tricky part is defining which types are "legal" for a given container. (I'm not into this whole "rigid" vs "parametric" distinction.) Can anyone explain what all the "~" characters mean? Or what "Constraint" means?
–
MathematicalOrchidJan 26 '12 at 10:45

I've expanded my answer to hopefully explain things better; I would also recommend reading the blog post I linked for a more thorough explanation of Constraint.
–
ehirdJan 26 '12 at 11:06

So kind "" is a normal type, kind " -> *" is any type constructor, and kind "Constraint" isn't a type at all, it's a type constraint? Is that right?
–
MathematicalOrchidJan 26 '12 at 11:07

Yep, exactly. (Although "type constraint" might be misleading; a single constraint can involve an arbitrary number of types.)
–
ehirdJan 26 '12 at 11:10

So "Result" isn't actually a type at all. "r" is the result type, and "Result" just constrains what it may be?
–
MathematicalOrchidJan 26 '12 at 12:50