Any advantage of using type constructors in class classes?

Take, for example, the Functor class:

 class Functor a instance Functor Maybe 

Here Maybe is a type constructor.

But we can do this in two other ways:

First, using classes with several parameters:

 class MultiFunctor ae instance MultiFunctor (Maybe a) a 

Second, using type families:

 class MonoFunctor a instance MonoFunctor (Maybe a) type family Element type instance Element (Maybe a) a 

Now there is one obvious advantage of the last two methods, namely that it allows us to do such things:

 instance Text Char 

Or:

 instance Text type instance Element Text Char 

Thus, we can work with monomorphic containers.

The second advantage is that we can instantiate types that do not have a type parameter as the final parameter. Suppose we create an Either style type, but use the types incorrectly:

 data Silly t errorT = Silly t errorT instance Functor Silly -- oh no we can't do this without a newtype wrapper 

While

 instance MultiFunctor (Silly t errorT) t 

works great and

 instance MonoFunctor (Silly t errorT) type instance Element (Silly t errorT) t 

also good.

Given these advantages of flexibility only when using full types (not type signing) in class class definitions, is there any reason to use the original style definition if you use GHC and don't mind using extensions? That is, is there anything special you can do to create a type constructor, and not just the full type in the type class, which you cannot do with classes with multiple parameters or types?

+8
haskell typeclass
source share
3 answers

Your suggestions ignore some pretty important information about the existing definition of Functor , because you could not handle the details of writing what would happen to the class member function.

 class MultiFunctor ae where mfmap :: (e -> ??) -> a -> ???? instance MultiFunctor (Maybe a) a where mfmap = ??????? 

An important property of fmap at the moment is that its first argument can change types. fmap show :: (Functor f, Show a) => fa -> f String . You can't just throw it away, or lose most of the fmap value. So really, the MultiFunctor will need to be more like ...

 class MultiFunctor stab | s -> a, t -> b, sb -> t, ta -> s where mfmap :: (a -> b) -> s -> t instance (a ~ c, b ~ d) => MultiFunctor (Maybe a) (Maybe b) cd where mfmap _ Nothing = Nothing mfmap f (Just a) = Just (fa) 

Notice how incredibly difficult it is to try to make a conclusion that is at least as close as possible. All functional dependencies are available for instance selection without annotating types throughout the place. (Maybe I missed a couple of possible functional dependencies!) The instance itself created some crazy type equality constraints to provide a more reliable instance selection. And the worst part is even worse properties for reasoning than fmap .

Suppose my previous instance does not exist, I could write such an instance:

 instance MultiFunctor (Maybe Int) (Maybe Int) Int Int where mfmap _ Nothing = Nothing mfmap f (Just a) = Just (if fa == a then a else fa * 2) 

It’s broken, of course, but it’s broken in a new way, which was not before. An important part of the Functor definition is that the types a and b in fmap do not appear anywhere in the instance definition. Just looking at the class is enough to tell the programmer that the behavior of fmap cannot depend on the types a and b . You get this warranty for free. You do not need to believe that the instances were written correctly.

Since fmap provides you this guarantee for free, you don’t even need to check the Functor laws when specifying an instance. It is enough to check the law fmap id x == x . The second law is distributed free of charge when the first law is proved. But with that broken mfmap that I just provided, mfmap id x == x true, although the second law is not.

As an mfmap developer, you have more work to prove that your implementation is correct. As a user of this, you should trust the implementation more because the type system cannot guarantee as much.

If you have developed more complete examples for other systems, you will find that they have the same problems if you want to support the full fmap functionality. And that is why they are not actually used. They add more complexity just for a small increase in utility.

+12
source share

Well, on the one hand, the traditional class of functors is much simpler. This in itself is a good reason to prefer it, although it is Haskell and not Python . And it also represents a mathematical idea that a functor should be better: mapping objects to objects ( f :: *->* ), with the additional property ( ->Constraint ) that each ( forall (a::*) (b::*) ) morphism ( a->b ) rises to morphism on the corresponding object displayed on ( -> f a->fb ).
None of this can be clearly seen in the version of the class * -> * -> Constraint or its equivalent TypeFamilies.

On a more practical account, yes, there are also things that you can only do with the version (*->*)->Constraint .

In particular, what this restriction guarantees to you right away is that all Haskell types are valid objects that you can put in a functor, whereas for MultiFunctor you need to check each possible content type one by one. Sometimes this is simply not possible (or is it?), For example, when you flip an infinite number of types:

 data Tough fa = Doable (fa) | Tough (f (Tough f (a, a))) instance (Applicative f) = Semigroup (Tough fa) where Doable x <> Doable y = Tough . Doable $ (,)<$>x<*>y Tough xs <> Tough ys = Tough $ xs <> ys -- The following actually violates the semigroup associativity law. Hardly matters here I suppose... xs <> Doable y = xs <> Tough (Doable $ fmap twice y) Doable x <> ys = Tough (Doable $ fmap twice x) <> ys twice x = (x,x) 

Note that this uses the Applicative f instance not only in type a , but also in arbitrary sets. I don’t see how you could express this with an applicative class based on MultiParamTypeClasses - or TypeFamilies . (Perhaps if you make Tough suitable GADT, but without it ... maybe not.)

By the way, this example may not be as useless as it might look - it basically expresses read-only vectors of length 2 n in a monadic state.

+4
source share

The advanced version is really more flexible. It was used, for example. Oleg Kiselev to identify limited monads . Rude, you can have

  class MN2 ma where ret2 :: a -> ma class (MN2 ma, MN2 mb) => MN3 mab where bind2 :: ma -> (a -> mb) -> mb 

allows monad instances to be parameterized by a and b . This is useful because you can restrict these types to members of another class:

 import Data.Set as Set instance MN2 Set.Set a where -- does not require Ord return = Set.singleton instance Prelude.Ord b => MN3 SMPlus ab where -- Set.union requires Ord m >>= f = Set.fold (Set.union . f) Set.empty m 

Note that due to this restriction of Ord we cannot define Monad Set.Set using unlimited monads. Indeed, the monad class requires that the monad be used for all types.

Also see: parameterized (indexed) monad .

+2
source share

All Articles