I've just read this paper ("Type classes: an exploration of the design space" by Peyton Jones & Jones), which explains some challenges with the early typeclass system of Haskell, and how to improve it.
Many of the issues that they raise are related to context reduction which is a way to reduce the set of constraints over instance and function declarations by following the "reverse entailment" relationship.
e.g. if you have somewhere instance (Ord a, Ord b) => Ord (a, b) ...
then within contexts, Ord (a, b)
gets reduced to {Ord a, Ord b}
(reduction does not always shrink the number of constrains).
I did not understand from the paper why this reduction was necessary.
Well, I gathered it was used to perform some form of type checking. When you have your reduced set of constraint, you can check that there exist some instance that can satisfy them, otherwise it's an error. I'm not too sure what the added value of that is, since you would notice the problem at the use site, but okay.
But even if you have to do that check, why use the result of reduction inside inferred types? The paper points out it leads to unintuitive inferred types.
The paper is quite ancient (1997) but as far as I can tell, context reduction is still an ongoing concern. The Haskell 2010 spec does mention the inference behaviour I explain above (link).
So, why do it this way?
Context reduced is the language of the classroom in which there are fewer non-verbal cues and the language is more abstract.
If we choose the right contexts, the learner's brain will learn to recognize the trigger conditions for the ability, and the elements that can change without affecting the requirement to execute. This also includes situations that suggest how to adapt the skill to different situations where it's still relevant.
English in Context means that the teacher learns to empathise with the needs of his/her students. English text is fairly easy for native speakers to understand, but not for ESL students = Use photos/drawings/videos to illustrate words and ideas so that ESL students get the gist.
Context-embedded learning involves exploration of socially relevant issues. It supports the achievement of core learning outcomes within national curricula while also enabling young people to engage in, develop understanding of, and act on issues that are impacting their families, schools and communities.
It’s important that context tells you, the receiver, what importance to place on something, what assumptions to draw about what is being communicated, and most importantly, it puts meaning into the message. What is the content context? The medium contained in the work that’s available for audience is referred to as content.
The bigger the difference between the first task you are working on and the next task you switch to, the larger the context switch between the two tasks is. Managing and improving context switches, especially around pull requests, can do more to increase your team's productivity than almost anything else.
From a teaching perspective, the value of a strong context cannot be underestimated when presenting new language to students. Contextualising early on in a lesson, through the use of situations, topics, images and talking points, creates a frame of reference for students to refer to when any new content comes at them.
Whenever we use language, whether we are speaking, listening, reading or writing, we do it in some kind of context. The situation we are in, the tone we want to express and the ways that others respond to us all affect the nature of the language choices that we make.
I don't know if this is The Reason, necessarily, but it might be considered A Reason: in early Haskell, type signatures were only permitted to have "simple" constraints, namely, a type class name applied to a type variable. Thus, for example, all of these were okay:
Ord a => a -> a -> Bool
Eq a => a -> a -> Bool
Graph gr => gr n e -> [n]
But none of these:
Ord (Tree a) => Tree a -> Tree a -> Bool
Eq (a -> b) => (a -> b) -> (a -> b) -> Bool
Graph Gr => Gr n e -> [n]
I think there was a feeling then -- and still today, as well -- that allowing the compiler to infer a type which one couldn't write manually would be a bit unfortunate. Context reduction was a way of turning the above signatures either into ones that could be written by hand as well or an informative error. For example, since one might reasonably have
instance Ord a => Ord (Tree a)
in scope, we could turn the illegal signature Ord (Tree a) => ...
into the legal signature Ord a => ...
. On the other hand, if we don't have any instance of Eq
for functions in scope, one would report an error about the type which was inferred to require Eq (a -> b)
in its context.
This has a couple of other benefits:
(Eq a, Eq a, Ord a)
into just Ord a
-- a transformation one definitely would want to do for readability.Eq (Integer -> Integer) => Bool
which can't be satisfied in a law-abiding way, one can report an error like Perhaps you did not apply a function to enough arguments?
. Much friendlier!Eq (Tree (Grizwump a, [Flagle (Gr n e) (Gr n' e') c]))
and complaining that the context is not satisfiable, it instead is forced to reduce this to the constituent constraints; it will instead complain that we couldn't determine Eq (Grizwump a)
from the existing context -- a much more precise and actionable error.I think this is indeed desirable in a dictionary passing implementation. In such an implementation, a "dictionary", that is, a tuple or record of functions is passed as implicit argument for every type class constraint in the type of the applied function.
Now, the question is simply when and how those dictionaries are created. Observe that for simple types like Int
by necessity all dictionaries for whatever type class Int
is an instance of will be a constant.
Not so in the case of parameterized types like lists, Maybe
or tuples. It is clear that to show a tuple, for instance, the Show
instances of the actual tuple elements need to be known. Hence such a polymorphic dictionary cannot be a constant.
It appears that the principle guiding the dictionary passing is such that only dictionaries for types that appear as type variables in the type of the applied function are passed. Or, to put it differently: no redundant information is replicated.
Consider this function:
f :: (Show a, Show b) => (a,b) -> Int
f ab = length (show ab)
The information that a tuple of show-able components is also showable, thus a constraint like Show (a,b)
needs not to appear when we already know (Show a, Show b)
.
An alternative implementation would be possible, though, where the caller .would be responsible to create and pass dictionaries. This could work without context reduction, such that the type of f
would look like:
f :: Show (a,b) => (a,b) -> Int
But this would mean that the code to create the tuple dictionary would have to be repeated on every call site. And it is easy to come up with examples where the number of necessary constraints actually increases, like in:
g :: (Show (a,a), Show(b,b), Show (a,b), Show (b, a)) => a -> b -> Int
g a b = maximum (map length [show (a,a), show (a,b), show (b,a), show(b,b)])
It is instructive to implement a type class/instance system with actual records that are explicitly passed. For example:
data Show' a = Show' { show' :: a -> String }
showInt :: Show' Int
showInt = Show' { show' = intshow } where
intshow :: Int -> String
intshow = show
Once you do this you will probably easily recognize the need for "context reduction".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With