[Haskell-cafe] data type declaration

John Lato jwlato at gmail.com
Mon Jul 26 06:02:30 EDT 2010


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 <ok at cs.otago.ac.nz> wrote:
>
> 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


More information about the Haskell-Cafe mailing list