2

I'm new to Haskell and trying to do something which I'm sure is easy but I'm not seeing the right way to do it.

What I want is a list of values of a particular typeclass, but different types of that typeclass. Eg:

class Shape a where
  area :: a -> Double
  numVertices :: a -> Integer

data Triangle = Triangle {...}
data Square = Square {...}

instance Shape Triangle where ...  
instance Shape Square where ...

x = [Triangle (...), Square (...)]

I'm getting a compiler error because the list has different types. What's the right way to do what I'm trying to do here? The only thing I've been able to come up with is doing something like this:

data WrappedShape = WrappedShape {
    getArea :: () -> Double
  , getNumVertices :: () -> Integer
}

wrap s = WrappedShape {
    getArea = \ () -> area s
  , getNumVertices = \ () -> vertices s
}

x = [wrap (Triangle (...)), wrap (Square (...))]

This works, but it's heavy on boilerplate, since I have to effectively define Shape twice and with differently-named members. What's the standard way to do this sort of thing?

1

2 Answers 2

1

If you just need a few different shapes, you can enumerate each shape as a constructor, here is a example:

data SomeShapes = Triangle {...}
                  | Square {...}

instance Shape SomeShapes where 
    area (Triangle x) = ...
    area (Square x) = ....

now you can put them in a list, because they are same type of SomeShapes

[Triangle {...}, Square {...}]
Sign up to request clarification or add additional context in comments.

Comments

1

Your wrapped type is probably the best idea.

It can be improved by noting that, in a lazy language like Haskell, the type () -> T essentially works like the plain T. You probably want to delay computation and write stuff like let f = \ () -> 1+2 which does not perform addition until the function f is called with argument (). However, let f = 1+2 already does not perform addition until f is really needed by some other expression -- this is laziness.

So, we can simply use

data WrappedShape = WrappedShape {
    getArea :: Double
  , getNumVertices :: Integer
}

wrap s = WrappedShape {
    getArea = area s
  , getNumVertices = vertices s
}

x = [wrap (Triangle (...)), wrap (Square (...))]

and forget about passing () later on: when we will access a list element, the area/vertices will be computed (whatever we need). That is print (getArea (head x)) will compute the area of the triangle.

The \ () -> ... trick is indeed needed in eager languages, but in Haskell it is an anti-pattern. Roughly, in Haskell everything has a \ () -> ... on top, roughly speaking, s o there's no need to add another one.


These is another solution to your problem, which is called an "existential type". However, this sometimes turns into an anti-pattern as well, so I do not recommend to use it lightly.

It would work as follows

data WrappedShape = forall a. Shape a => WrappedShape a

x = [WrappedShape (Triangle ...), WrappedShape (Square ...)]

exampleUsage = case head x of WrappedShape s -> area s

This is more convenient when the type class has a lots of methods, since we do not have to write a lot of fields in the wrapped type.

The main downside of this technique is that it involves more complex type machinery, for no real gain. I mean a basic list [(Double, Integer)] has the same functionality of [WrappedShape] (list of existentials), so why bother with the latter?

Luke Palmer wrote about this anti-pattern. I do not agree with that post completely, but I think he does have some good points.

I do not have a clear-cut line where I would start using existentials over the basic approach, but these factors are what I would consider:

  • How many methods does the type class have?
  • Are there any methods of the type class where the type a (the one related to the class) appears not only as an argument? E.g. a method foo :: a -> (String, a)?

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.