Babra Cunningham
Babra Cunningham

Reputation: 2967

Functor declaration with multiple types?

I've got the following data type:

data Users id height weight = User id height weight

instance Functor Users where
fmap f (User id height weight) = User(f id height weight)

Yet this won't compile?

It works fine when I use a type with a single parameter, such as:

data Users id = User id
instance Functor Users where
fmap f (User id) = User (f id)

Why isn't my first example working?

Upvotes: 1

Views: 1852

Answers (2)

pangiole
pangiole

Reputation: 991

Instances of the Functor class can be defined for types of first-order kind only, which are those of kind * -> *

If you try querying GHCI as follows

ghci> :kind Users
Users :: * -> * -> * -> *

you will understand that you custom type is of much higher kindness, hence the compiler error.

If you still wish to provide an instance of the Functor class for it, despite how useful that could be, do as follows

ghci> :{
    | -- define custom type having a too high kind
    | data User a b c = User a b c deriving Show
    |
    | -- provide functor instance by reducing its type kindness 
    | instance Functor (User a b) where
    |   fmap f (User x y z) = User x y (f z)
    | :}

The trick is to partially apply the User type constructor so to produce a type having kindness reduced to the expected one. Then, define your fmap by pattern matching the whole User value constructor and apply the function f to the last input value.

Example of usage is:

ghci> u1 = User 1 168 58.9
ghci> fmap (+5) u1
User 1 168 63.9

That's roughly how the Either, tuple (,) and function (->) types, which are also higher-order kinded types in Haskell prelude, are able to provide instances of the Functor class too.

Upvotes: 2

chepner
chepner

Reputation: 532053

Each type and type constructor has a kind. Something simple like Int has kind *. Your single-argument type constructor Users has kind * -> *; it takes one type and returns a new one. Your first example of Users has kind * -> * -> * -> *; it takes three types and returns a new one.

Functor only works with type constructors of kind * -> *. This allows you to define a Functor instance for your second Users type constructor, but not your first one.

Think about your first try: the data constructor Users takes three arguments, but your definition of fmap attempts to call it with just one, the return value of f. You could make Users a functor as long as you are willing to make all three fields the same type:

data Users a = Users a a a

instance Functor Users where
    fmap f (Users a b c) = Users (f a) (f b) (f c)

In theory, you could define a class Trifunctor (there is a Bifunctor class available):

class Trifunctor f where
    trimap :: (a1 -> b1) -> (a2 -> b2) -> (a3 -> b3) -> f a1 a2 a3 -> f b1 b2 b3

data Users a b c = Users a b c

instance Trifunctor Users where
    trimap f g h (Users a b c) = Users (f a) (g b) (h c)

but it's debatable how useful this would be. Functor is useful for general purpose containers, because they have a wide range of uses. Users, on the other hand, seems pretty specific. You generally don't need the flexibility you are defining it with; data User = User Int Int Int seems like it would work fine, and there is no need to map a function over this: how often to you need the same function to modify height, weight, and age in the same way?

Upvotes: 7

Related Questions