izotop
izotop

Reputation: 51

Typeclass method which returns another typeclass in Haskell

I have this task in which I have to define 2 typeclasses in Haskell. One is simple - it implements Ord and has one method which converts given type to an int. Something like this:

class (Ord a) => Id a where
  toInt :: a -> Int

But then I have another typeclass HasId which has a method getId. This method should return Id typeclass. So I wrote something like this:

class HasId a where
  getId :: a -> Id a 

And I get an error I don't know how to solve.

• Expected kind ‘* -> Constraint’, but ‘Int’ has kind ‘*’
• In the class declaration for ‘HasId’
 |
 | class (Int a) => HasId a where     |        ^^^^^

• Expected a type, but ‘Id a’ has kind ‘Constraint’
• In the type signature: getId :: a -> Id a
  In the class declaration for ‘HasId’
 |
 |  getId :: a -> Id a     |             

Can somebody tell me how can I return typeclass in another's typeclass method? Should I implement some instance of Id typeclass at first?

Upvotes: 0

Views: 575

Answers (2)

leftaroundabout
leftaroundabout

Reputation: 120711

It's important to understand that a Haskell class is very different from a class in OO languages. It's a type class, i.e. it “groups together a collection of types”, whereas an OO class groups together a collection of values. IOW, an OO class is a type (containing values), but a Haskell class is rather something like a type of types, as it were.

Now, a function/method always takes values as argument and gives back values as results. But it can't give you back a type, nor a “value of a typeclass” because there's no such thing.

Instead, the way to use type classes is this: you write a polymorphic function, i.e. a function that accepts or yields some value of unspecified type. That's what type variables are there for. Normally, a polymorphic signature means that the function can deal with values of any type, like with

length :: [a] -> Int

which takes a list and doesn't care what type its elements have. But then you say that you do care a bit what types are used, namely, you require that they are in the class. That's a constraint, and it's written with the => notation. toInt actually has the signature (automatically generated by the class declaration)

toInt :: Id a => a -> Int

Frankly, I doubt that this is really what you want. If all you can do with an Id a is converting it to an Int, then the result effectively is an Int (just with a type-annotation which indicates what kind of object this Id belongs to), regardless of the a parameter. So instead of the Id class, you should consider having an Id type

newtype Id' a = Id {toInt :: Int}  -- The prime ' symbol has no particular meaning,
                                   -- I just use it for disambiguation.

Then, you have the more sensible type

toInt :: Id' a -> Int

without needing any constraints.

newtype is much more similar to a OO class, in that it actually defines a concrete type which has values you can pass around.

The getId method is also a constrained-polymorphic function, that can take something which “has an Id” and gives you back that Id. In this case, a class makes sense (because you can have different data structure that may store their Ids in different ways). This would now actually be

class HasId a where
  getId :: a -> Id' a

As per Welperooni's answer, it is indeed also conveivable to define HasId using your original Id class, just it doesn't really make much sense. Id is the class of all types which can be used as identifyers. You could express that getId can yield an Id of any such type:

class HasId a where
  getId :: Id b => a -> b

Note that the result-Id type is now completely independent of the type of object whose Id you want to calculate. b can always be any Id-type. As I said this doesn't make sense: why would you need a whole lot of different types that represent Ids, but require all the HasId types to support all of them?

In fact this cannot be implemented as it stands, because there's no mechanism that allows you to generate an Id of arbitrary Id type. This would require an additional method

class Id a where
  toInt :: a -> Int
  fromInt :: Int -> a

and now you could do

class HasId a where
  getId :: Id b => a -> b

data SomeObj { objName :: String
             , objId :: Int }

instance HasId SomeObj where
  getId (SomeObj _ i) = fromInt i

However, if you require that any Id type is convertible both from and to Int, then these types must be isomorphic to Int itself. Therefore, the newtype Id' a approach is almost certainly better.

Upvotes: 7

Welperooni
Welperooni

Reputation: 643

You declared the return value to be a HKT Id a, but what you wanted is another type, constrained by the typeclass Id.

What you need is not getId :: a -> Id a, but getId :: Id b => a -> b

Upvotes: 1

Related Questions