Why typeclasses instead of just pattern matching?

﹥>﹥吖頭↗ 提交于 2019-12-10 13:08:36

问题


This is something of a philosophical question, but one I hope to have answered by official documentation or “word of god” (read: SPJ). Is there a specific reason that the Haskell committee chose to require explicit interfaces in the form of typeclasses rather than a more uniform solution based on pattern-matching?

As an example, take Eq:

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

instance Eq Int where
    (==) = internalIntEq

Why could we not do something like this instead (bear with the 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 were to allow pattern-matching of ordinary data types, then:

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

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

  • Default implementations would come “for free”

  • Classes could readily be extended without breaking anything

There would need to be a way to specify a default pattern (1) that, though declared before others, always matches last. Do any of these hypothetical features clash with something inherent in Haskell? Would it become difficult or impossible to correctly infer types? It seems like a powerful feature that’d gel very well with the rest of Haskell, so I figure there’s a good reason We Don’t Do It That Way™. Is this mechanism of ad-hoc polymorphism simply too ad-hoc?


回答1:


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

This question simply begs to be linked to Philip Wadler and Steve Blott's 1988 paper, How to make ad-hoc polymorphism less ad-hoc, where they present the idea of Type classes. Wadler is probably "word of God" on this one.

There are a few problems I see with the proposed "pattern match on any Haskell data type" technique.

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

The pattern matching technique still falls back onto type classes, except in a worse way. Type classes classify types (go figure). But the pattern matching technique makes it rather vague. How exactly are you supposed to specify that functions foo and bar are part of the "same" class? Typeclass constraints would become utterly unreadable if you have to add a new one for every single polymorphic function you use.

The pattern matching technique introduces new syntax into Haskell, complicating the language specification. The default keyword doesn't look so bad, but pattern matching "on types" is new and confusing.

Pattern matching "on ordinary data types" defeats pointfree style. Instead of (==) = intEq, we have (Int a) == (Int b) = intEq a b; this sort of artificial pattern match prevents eta-reduction.

Finally, it completely changes our understanding of type signatures. a -> a -> Foo is currently a guarantee that the inputs cannot be inspected. Nothing can be assumed about the a inputs, except that the two inputs are of the same type. [a] -> [a] again means the elements of the list cannot be inspected in any meaningful way, giving you Theorems for Free (another Wadler paper).

There may be ways to address these concerns, but my overall impression is that Type classes already solve this problem in an elegant way, and the suggested pattern matching technique adds no benefit, while causing several problems.




回答2:


I don't know the Word of God, but here are a few arguments.

No longer is defining a function in the same module unique. You can now write

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

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

It's bad practice to have the compiler adding in identifiers. In your case, you automatically create type class SupportsEqualsEquals. A new user might ask, "where does this come from", and there'd be no corresponding source defining it.

Skipping writing instance signatures doesn't give you as much as you might think. You can get the necessary parameters via e.g. :t internalIntEq in ghci. I guess it could be more convenient, but I'd rather have a tool I could ask "what's the instance type for Eq where == is internalIntEq.".

More advanced class features are unclear. Where do you put associated types and functional dependencies? These are really important to me!!

Your defaulting makes modular compilation more difficult. You're not going to get extensible classes for free. Consider,

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

As I understand it, this compiles down into

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

Now, if I provide a new /= instance for a particular type a_0, and feed some x :: a_0 into f, then

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

You might ask, "when would one be so dumb to restrict f to Supports[==] instead of Supports[/=]?" But, contexts can come from a more than function signatures; they can come from higher-order functions, etc., etc.

Hope it helps.



来源:https://stackoverflow.com/questions/9106980/why-typeclasses-instead-of-just-pattern-matching

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!