Heterogeneous polymorphism in Haskell (correct way)
Asked Answered
S

3

3

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!

Signature answered 2/12, 2012 at 18:43 Comment(2)
Mmmm... I'm reading haskell.org/haskellwiki/Existential_type , then, is data Shape the correct way?Signature
How about adding an instance to class Area like class Surface a => Area a where area = surface?Selfesteem
C
5

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.

Courtmartial answered 2/12, 2012 at 22:16 Comment(1)
Wow! DataTypesALaCarte.pdf is exactly I looking for, thank you again!Signature
L
8

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 Shapes, and apply them right in place! This reversal exchanges existential quantification for universal quantification, which is rather more widely accepted.

Leitmotiv answered 2/12, 2012 at 18:56 Comment(3)
No, sum [ area c1, area c2 ] is not a valid response, polymorphism is not encapsulates into all process (sum...). For that is data Shape. I ask for a direct way (if exists) to perform data Shape. And yes, it could be possible, Haskell compiler could create a virtual data Shape when detect [c1, r1] array (I think it isn't about statically languages). Thank you anyway!Signature
How is data Shape = ... not a direct way? "could create a virtual data Shape" That would mess up pretty much all of Haskell's type inference mechanisms. You would need to give every single object an explicit type, the language would then look about as ugly as C++03.Leitmotiv
@Signature You can do it the idiomatic Haskell way, in which case you'll need to change your approach as suggested. Or you can keep your approach, and accept the awkwardness that comes with going against the language's natural grain.Gusher
C
5

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.

Courtmartial answered 2/12, 2012 at 22:16 Comment(1)
Wow! DataTypesALaCarte.pdf is exactly I looking for, thank you again!Signature
C
5

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]
Circlet answered 2/12, 2012 at 23:8 Comment(1)
thank you, but you solve my example, not my problem (expose abstract behavior "a priori"). jberryman explain it. Thank's anyway!Signature

© 2022 - 2024 — McMap. All rights reserved.