haskell polymorphism and lists

后端 未结 6 1002
温柔的废话
温柔的废话 2021-02-02 13:45

Suppose I have the following:

class Shape a where
    draw a :: a -> IO ()

data Rectangle = Rectangle Int Int

instance Shape Rectangle where
    draw (Recta         


        
相关标签:
6条回答
  • 2021-02-02 14:27

    One way to do it would be with vtables:

    data Shape = Shape {
      draw :: IO (),
      etc :: ...
    }
    
    rectangle :: Int -> Int -> Shape
    rectangle len width = Shape {
      draw = ...,
      etc = ...
    }
    
    circle :: Int -> Int -> Shape
    circle center radius = Shape {
      draw = ...,
      etc = ...
    }
    
    0 讨论(0)
  • 2021-02-02 14:29

    If you really do need to do this, then use an existential:

    {-# LANGUAGE GADTs #-}
    
    
    class IsShape a where
        draw :: a -> IO ()
    
    data Rectangle = Rectangle Int Int
    
    instance IsShape Rectangle where
        draw (Rectangle length width) = ...
    
    data Circle = Circle Int Int
    
    instance IsShape Circle where
        draw (Circle center radius) = ...
    
    data Shape where
        Shape :: IsShape a => a -> Shape
    
    shapes = [Shape (Circle 5 10), Shape (Circle 20 30), Shape (Rectangle 10 15)]
    

    (I renamed your class as there would be a name clash with the datatype otherwise, and having the naming this way round seems more natural).

    The advantage of this solution over the other answer involving a single datatype with different constructors is that it is open; you can define new instances of IsShape wherever you like. The advantage of the other answer is that it's more "functional", and also that the closedness may in some cases be an advantage as it means that clients know exactly what to expect.

    0 讨论(0)
  • 2021-02-02 14:29

    How to deal with a heterogeneous list of shapes in Haskell — Abstract polymorphism with type classes: http://pastebin.com/hL9ME7qP via @pastebin

    CODE:

    {-# LANGUAGE GADTs #-}
    
    class Shape s where
     area :: s -> Double
     perimeter :: s -> Double
    
    data Rectangle = Rectangle {
     width :: Double,
     height :: Double
    } deriving Show
    
    instance Shape Rectangle where
     area rectangle = (width rectangle) * (height rectangle)
     perimeter rectangle = 2 * ((width rectangle) + (height rectangle))
    
    data Circle = Circle {
     radius :: Double
    } deriving Show
    
    instance Shape Circle where
     area circle = pi * (radius circle) * (radius circle)
     perimeter circle = 2.0 * pi * (radius circle)
    
    r=Rectangle 10.0 3.0 
    c=Circle 10.0
    list=[WrapShape r,WrapShape c]
    
    data ShapeWrapper where
     WrapShape :: Shape s => s -> ShapeWrapper
    
    getArea :: ShapeWrapper -> Double
    getArea (WrapShape s) = area s
    
    getPerimeter :: ShapeWrapper -> Double
    getPerimeter (WrapShape s) = perimeter s
    
    areas = map getArea list
    perimeters = map getPerimeter list
    
    0 讨论(0)
  • 2021-02-02 14:42

    As Ganesh said, you could indeed use GADTs to have more type safety. But if you don't want (or need) to, here's my take on this:

    As you already know, all elements of a list need to be of the same type. It isn't very useful to have a list of elements of different types, because then your throwing away your type information.

    In this case however, since you want throw away type information (you're just interested in the drawable part of the value), you would suggest to change the type of your values to something that is just drawable.

    type Drawable = IO ()
    
    shapes :: [Drawable]
    shapes = [draw (Circle 5 10), draw (Circle 20 30), draw (Rectangle 10 15)]
    

    Presumably, your actual Drawable will be something more interesting than just IO () (maybe something like: MaxWidth -> IO ()).

    And also, due to lazy evaluation, the actual value won't be drawn until you force the list with something like sequence_. So you don't have to worry about side effects (but you probably already saw that from the type of shapes).


    Just to be complete (and incorporate my comment into this answer): This is a more general implementation, useful if Shape has more functions:

    type MaxWith = Int
    
    class Shape a where
        draw :: a -> MaxWidth -> IO ()
        size :: a -> Int
    
    type ShapeResult = (MaxWidth -> IO (), Int)
    
    shape :: (Shape a) => a -> ShapeResult
    shape x = (draw x, size x)
    
    shapes :: [ShapeResult]
    shapes = [shape (Circle 5 10), shape (Circle 20 30), shape (Rectangle 10 15)]
    

    Here, the shape function transforms a Shape a value into a ShapeResult value, by simply calling all the functions in the Shape class. Due to laziness, none of the values are actually computed until you need them.

    To be honest, I don't think I would actually use a construct like this. I would either use the Drawable-method from above, or if a more general solution is needed, use GADTs. That being said, this is a fun exercise.

    0 讨论(0)
  • 2021-02-02 14:44

    Consider using a single type instead of separate types and a typeclass.

    data Shape = Rectangle Int Int
               | Circle Int Int
    
    draw (Rectangle length width) = ...
    draw (Circle center radius)   = ...
    
    shapes = [Circle 5 10, Circle 20 30, Rectangle 10 15]
    
    0 讨论(0)
  • 2021-02-02 14:46

    A variant of Ganesh's solution using the existential quantification syntax instead.

    {-# LANGUAGE ExistentialQuantification #-}
    class IsShape a where
        draw :: a -> String
    
    data Rectangle = Rectangle Int Int
    
    instance IsShape Rectangle where
        draw (Rectangle length width) = ""
    
    data Circle = Circle Int Int
    
    instance IsShape Circle where
        draw (Circle center radius) = ""
    
    data Shape = forall a. (IsShape a) => Shape a
    
    shapes = [Shape (Circle 5 10), Shape (Circle 20 30), Shape (Rectangle 10 15)]
    
    0 讨论(0)
提交回复
热议问题