Why are classes used instead of regular pattern mappings?

This is a bit of a philosophical question, but I hope to get official documentation or the โ€œword of Godโ€ (read: SPJ). Is there a specific reason why the Haskell committee decided to use explicit interfaces as types rather than a more uniform solution based on pattern matching?

Take Eq as an example:

 class Eq a where (==), (/=) :: a -> a -> Bool x == y = not $ x /= y x /= y = not $ x == y instance Eq Int where (==) = internalIntEq 

Why don't we do something like this (carry pseudo-Haskell):

 (==), (/=) :: a -> a -> Bool default x == y = not $ x /= y -- 1 default x /= y = not $ x == y (Int a) == (Int b) = a `internalIntEq` b -- 2 

That is, if Haskell should allow matching patterns of ordinary data types, then:

  • Programmers could create ad-hoc classes, i.e. instance would be implicit (2)

  • Types can still be inferred and matched statically ( SupportsEqualsEquals a => ... )

  • Default implementations will appear "for free"

  • Classes can be easily extended without breaking anything

There should be a way to specify the default template (1), which, although declared before others, always matches the latter. Can any of these hypothetical functions run into something that is inherent in Haskell? Would it become difficult or impossible to correctly infer types? This powerful feature of this gel seems to blend very well with the rest of Haskell, so I think this is a good reason we are not doing this. Is this mechanism of ad-hoc polymorphism just too ad-hoc?

+7
source share
2 answers

Is this mechanism of ad-hoc polymorphism too ad-hoc?

This question simply begs to be related to a paper by Philip Wadler and Steve Blott 1988, How to make ad-hoc polymorphism less than ad-hoc , where they present the idea of โ€‹โ€‹type classes. Wadler is probably the "word of God" on this.

There are several problems that I see with the proposed "pattern matching for any data type such as Haskell."

The pattern matching method is not sufficient to define polymorphic constants such as mempty :: Monoid a => a .

The pattern matching method still falls for class classes, with the exception of the worst. Type classes classify types (go figure). But the pattern matching method makes it pretty vague. How exactly should you indicate that the functions foo and bar are part of the "same" class? Typeclass restrictions will become completely unreadable if you need to add a new one for each polymorphic function you use.

The pattern matching method introduces new syntax into Haskell, complicating the language specification . The default keyword doesn't look so bad, but matching the pattern by type is new and confusing.

Matching patterns by "regular data types" strikes a dotless style. Instead of (==) = intEq we have (Int a) == (Int b) = intEq ab ; this type of matching artificial pattern prevents eta-contraction .

Finally, it completely changes our understanding of type signatures. a -> a -> Foo is currently a guarantee that the input cannot be verified. Inputs a cannot be accepted, except that the two inputs are of the same type. [a] -> [a] again means that the list items cannot be checked in any meaningful way, providing you with Theorems for free (another Wadler paper).

There may be ways to solve these problems, but my general impression is that type classes already solve this problem in an elegant way, and the proposed template matching technology does not bring any benefits, causing several problems.

+12
source

I do not know the Word of God, but here are a few arguments.

No longer defines a function in the same module. Now you can write

 (==) = internalIntEq (==) = internalFloatEq 

This makes the code less readable. There is a sentence called "TypeBasedNameResolution" that does something similar, but the important fact is that such type branching is performed only for (==) from different modules.

It's a good practice to add a compiler to identifiers. In your case, you automatically create a class of type SupportsEqualsEquals . A new user may ask "where does this come from" and there will be no corresponding source identifying it.

Skipping an instance signature record does not give you as much as you might think. You can get the necessary parameters, for example. :t internalIntEq in ghci. I suppose this would be more convenient, but I would prefer to have a tool that could ask "what is the instance type for Eq , where == is internalIntEq .".

More complex class functions are unclear. Where do you put related types and functional dependencies? This is really important to me!

Your default complicates the assembly of the module. You will not receive free classes for free. Consider

 f :: Supports[==] a => a -> a -> Bool f = (/=) 

As I understand it, it comes down to

 f :: Instance (Supports[==]) a -> a -> a -> Bool f eq_inst xy = not (x eq_inst.== y) 

Now, if I provide a new instance of /= for a specific type a_0 and pass some x :: a_0 to f , then

 fxx = not (x == x) -- computation you really want: fxx = x /= x, using the new /= instance for a_0 

You may ask: "When would it be so foolish to limit f to Supports[==] instead of Supports[/=] ?" But contexts can come from more than function signatures; they can come from functions of a higher order, etc. etc.

Hope this helps.

+4
source

All Articles