
Richard,
I'm not sure that I agree or disagree with you; I think the decision
is above my pay grade.
On Mon, Jul 26, 2010 at 4:49 AM, Richard O'Keefe
On Jul 26, 2010, at 12:35 PM, John Lato wrote:
Incidentally, there seems to be a consensus that this a Bad Idea [1]. Even when you specify a type class context on a data declaration, Haskell still requires you to specify the context on functions that use that data (Address c a).
This has always puzzled me.
Take the obvious data Ord key => BST key val = Empty | Node key val (BST key val) (BST key val)
Why would anyone say this if they didn't *want* the constraint implied on every use? If you want the constraint implied on every use of any constructor, including ones where the constructor is used for pattern matching, what do you do if not this?
Currently you include the constraint manually every time you use the constructor (but you already know that). Another approach (which I wouldn't advocate) is to use existentially-quantified data, which carries it's context automatically. I don't know if any other extensions would help, possibly GADT's?
Good software engineering involves *controlled* use of redundancy. Having it *stated* in one place and *checked* in others is an example. Requiring the same information to be repeated everywhere is not.
What's worse is that you need the class restriction for *all* functions that use an Address,
and if you didn't WANT that, you wouldn't say this.
I think this makes more sense when I think about a class context as a dictionary instead of a type restriction. If I think of a type class as meaning "I want these types to have this relationship", then I want that to be always true for this data. If I think of a type class as meaning "here's an extra set of functions that are available for these types", then I'd prefer not to carry it around unless it's necessary. In any case, even if you want to specify a type relation which is always valid, it's frequently irrelevant to the operation at hand, and can be ignored (left out) in those cases. If the behavior of class contexts on data types were changed to what you think it should mean, i.e. contexts specified in a data declaration are carried around for all uses of that type instead of just the data constructor, I wouldn't mind at all. Whether this is a good idea or would cause other problems, I can't say.
Oh sure, something like is_empty (Empty) = True is_empty (Node _ _ _ _) = Fase doesn't happen to make use of any constrained component. But it is part of a *group* of methods which collectively don't make any sense without it, so there's no real practical advantage to having some functions constrained and some not (unless you count delaying error message as an advantage).
You don't delay an error message though; this is resolved at compile time. This function "is_empty" doesn't need the context, but any function that calls is_empty is likely to have it available anyway. If you write functionWithNoContext x = do_something_with (needsContext x) The compiler complains that "functionWithNoContext" needs the context, exactly where it's required. Would this be easier if "BST key val" carried the context implicitly? Probably so. And I do agree that for many data types it makes sense to have contexts available implicitly. Until that happens, though, I prefer to keep my type signatures as simple as possible.
and don't export the Address data constructor.
This doesn't help _within_ the defining module where you are pattern matching.
No, and it's particularly irksome that the only options are programmer discipline or creating a separate module for the data type and losing pattern matching. One thing on my wish list for Haskell' would be allowing for data constructors to be exported for pattern matching only. That is, you could do this: case x of Foo x -> ... but not let y = Foo x John