Let a module to abstract Area
operations (bad definition)
class Area someShapeType where
area :: someShapeType -> Float
-- module utilities
sumAreas :: Area someShapeType => [someShapeType]
sumAreas = sum . map area
Let a posteriori explicit shape type modules (good or acceptable definition)
data Point = Point Float Float
data Circle = Circle Point Float
instance Surface Circle where
surface (Circle _ r) = 2 * pi * r
data Rectangle = Rectangle Point Point
instance Surface Rectangle where
surface (Rectangle (Point x1 y1) (Point x2 y2)) = abs $ (x2 - x1) * (y2 - y1)
Let some data
c1 = Circle (Point 0 0) 1
r1 = Rectangle (Point 0 0) (Point 1 1)
Then, trying to use
totalArea = sumAreas [c1, r1]
the [c1, r1]
type must be expanded to [Circle]
or [Rectangle]
! (and is not valid)
I can do using forall
and a extra data
type like this
data Shape = forall a . Surface a => Shape a
sumSurfaces :: [Shape] -> Float
sumSurfaces = sum . map (\(Shape x) -> surface x)
then, next code run successfully
sumSurfaces [Shape c1, Shape r1]
but I think, the use of data Shape
and Shape
constructor (on [Shape c1, ...]
and lambda argument) is ugly (my first [and bad] way is pretty).
What is the correct way to do "Heterogeneous polymorphism in Haskell"?
Thank you very much for your time!
Your existential solution is okay. It might be "prettier" to instead use a GADT
, as in:
{-# LANGUAGE GADTs #-}
data Shape where
Shape :: (Surface a) => a -> Shape
...and as leftaraoundabout suggests, you may be able to structure your code differently.
But I think you've basically hit up against the Expression Problem here; or perhaps, more accurately: by trying to structure your code cleverly (separate type for each shape with classes) in anticipation of the EP you've introduced new difficulties for yourself.
Check out the fun Data Types a la Carte by Wouter Swierstra for an elegant solution to what I hope is related to your problem. Maybe someone can comment with good packages on hackage to look at that are inspired by that paper.
Your first (and bad) way is not pretty, it's Lispy. This is just not possible in a statically typed language; even when you do such a thing in e.g. Java you're actually introducing a seperate quantification step by using base class pointers, which is analoguous to the data Shape = forall a. Surface a
.
There is dispute about whether existential quantification is nice, I think most Haskellers don't like it very much. It's certainly not the right thing to use here: sum [ area c1, area c2 ]
is much easier and works just as well. But there sure are more complex problems where it looks differently; when you "need" heterogeneous polymorphism then existentials are the way to go.
Just remember that you can always get around this: since Haskell is lazy, you can just apply all possible operations (in this example it's only area
) "pre-emptively", store all the results in some record, and output a list of these records instead of a list of polymorphic objects. This way you keep all the information.
Or, and that's more idiomatic, don't produce a list of such objects at all. You want to do something with the objects, so why not just pass these actions into the function where you produce different Shape
s, and apply them right in place! This reversal exchanges existential quantification for universal quantification, which is rather more widely accepted.
What you originally did is hit the existential antipattern.
Why use classes here anyways?
data Shape = Shape { area :: Double }
data Point = Point Double Double
circle :: Point -> Double -> Shape
circle p r =
Shape $ 2 * pi * r
rectangle :: Point -> Point -> Shape
rectangle (Point x1 y1) (Point x2 y2) =
Shape $ abs $ (x2 - x1) * (y2 - y1)
And now you easily get what you want (a list of shapes):
*Main> map area [circle (Point 2 0) 5, rectangle (Point 0 0) (Point 2 10)]
[31.41592653589793,20.0]