1

我来自 OOP 背景,所以我无法理解这是如何在 Haskell 中完成的。

在 OOP 中,假设我们有Shape -> Circle, Rectangle, Square层次结构。我可以很容易地写出这个伪代码:

Shape[] shapes = [create_circle(), create_rect(), create_square()]
foreach(Shape s: Shapes) draw(s)

它会调用drawCircle、Rectangle、Square 的方法(或 Shape,如果它没有为子类型实现)。

如何在 Haskell 中实现这一点?

4

4 回答 4

4

您可以使用数据类型来封装所有形状,也可以使用存在量化。这两种选择都有优点和缺点。你正面临“表达问题”。选择正确的选项取决于您的架构,因此我将详细说明两者。

我们假设您对形状有类似的定义:

data Circle = Circle { circleCenter :: Point, circleRadius :: Float }
data Rectangle = Rectangle { rectTopLeft :: Point, rectSize :: Size }
data Square = Square { squareTopLeft :: Point, squareSize :: Float }

...以及一些功能drawCircledrawRectangledrawSquare

使用数据类型

data Shape
    = Circle Cirle
    | Rectangle Rectangle
    | Square Square

draw :: Shape -> IO ()
draw (Circle c) = drawCircle c
draw (Rectangle r) = drawRectangle r
draw (Square s) = drawSquare s

这种模式允许您轻松添加新功能(如shapeAreashiftShape等...),但很难添加新形状,尤其是对于您库的用户而言。

使用存在量化

{-# LANGUAGE ExistentialQuantification #-}

-- Instead of using a datatype, we use a typeclass
class Shape s where
    draw :: s -> IO ()

instance Shape Circle where
    draw = drawCircle

instance Shape Rectangle where
    draw = drawRectangle

instance Shape Square where
    draw = drawSquare

-- Can't use newtype with ExistentialQuantification
data Sh = forall s. Shape s => Sh s

instance Shape Sh where
    draw (Sh s) = draw s

使用此解决方案,您或用户将能够轻松添加新形状,但添加新功能可能会稍微复杂一些。

您还可以将类型类“降级”为数据类型,并将类型类成员“降级”为记录字段,如本文所述,正如 leftaroundabout 的答案中提到的那样。

我无法进一步帮助您,因为我不详细了解您的代码。如果您仍然需要帮助选择,请在评论中告诉我:)

于 2017-05-26T13:15:34.770 回答
4

您可以使用类型化的最终样式。例如:

class Circle s where
    circle :: Point -> Radius -> s

class Rectangle s where
    rectangle :: Point -> Point -> s

class Square s where
    square :: Point -> Side -> s

newtype Draw a = Draw { runDraw :: IO a }
    deriving (Functor, Applicative, Monad)

draw :: Draw () -> Draw ()
draw = id

instance Circle (Draw ()) where
    circle = ... -- draw circle here

instance Rectangle (Draw ()) where
    rectangle = ... -- draw rectangle here

instance Square (Draw ()) where
    square = ... -- draw square here

main = runDraw $ forM_ shapes draw
  where
    shapes = [circle ..., rectangle ..., square ...]
于 2017-05-26T13:23:38.777 回答
3

你可能想要

data Circle = Circle { {- e.g. center- and radius fields -} }
data Rectangle = Rectangle { ... }
               | SquareRectangle Square
data Square = Square { ... }

然后

data Shape = CircleShape Circle
           | RectangleShape Rectangle

现在您可以拥有一个列表,Shapes其中可能包含任何圆形、矩形和正方形。

OOP 代码的主要区别在于Shape“类”(它不是 Haskell 类,而是 ADT)是关闭的:如果 sombody 想要添加新的替代形状,他们需要为此定义一个新类型,或者更改实际源旧定义的代码。

对于许多现实世界的应用程序来说,这实际上是一件好事,因为这意味着编译器每次都会看到所有可能的选项,并且可以告诉您是否正在编写不包含某些可能选项的代码。

或者,如果您的意图是让形状抽象并由它们的绘制方式定义,只需使用

type Shape = WhateverTypeYourDrawingCanvasHas
于 2017-05-26T13:01:28.330 回答
1

我认为将构造函数分组会很好CircleRectangle并且Square在相同的类型下调用Shape,甚至我们可以发明一个Shapes类来拥有一些通用方法,如下所示。

class Shapes a where
  area          :: a -> Float
  circumference :: a -> Float
  draw          :: a -> IO ()

data Shape = Circle {pos :: (Float, Float), radius :: Float} |
             Rect   {pos :: (Float, Float), width :: Float, height :: Float} |
             Square {pos :: (Float, Float), width :: Float}

instance Shapes Shape where
  area (Circle _ radius)              = pi * radius ^ 2
  area (Rect _ width height)          = width * height
  area (Square _ width)               = width ^ 2
  circumference (Circle _ radius)     = 2 * pi * radius
  circumference (Rect _ width height) = 2 * (width + height)
  circumference (Square _ width)      = 4 * width
  draw (Circle pos radius)            = putStrLn (" Circle drawn @ " ++ show pos ++ " with radius " ++ show radius)
  draw (Rect pos width height)        = putStrLn (" Rectangle drawn @ " ++ show pos ++ " with (w,h) " ++ show (width,height))
  draw (Square pos width)             = putStrLn (" Square drawn @ " ++ show pos ++ " with width " ++ show width)

instance Show Shape where
  show (Circle pos radius)     = "Circle with radius " ++ show radius ++ " @ " ++ show pos
  show (Rect pos width height) = "Rect (w,h)" ++ show (width, height) ++ " @ " ++ show pos
  show (Square pos width)      = "Square with edge " ++ show width ++ " @ " ++ show pos

*Main> let c1 = Circle (20,20) 5
*Main> draw c1
 Circle drawn @ (20.0,20.0) with radius 5.0
*Main> let c2 = c1 {pos = (10,10)}
*Main> draw c2
 Circle drawn @ (10.0,10.0) with radius 5.0
*Main> draw c1 -- c1 is immutable
 Circle drawn @ (20.0,20.0) with radius 5.0
*Main> area c1
78.53982

但是,如果您想创建一个可扩展的数据类型,那么最好定义一个Shape类并将您的形状定义为从类继承方法的单个数据类型Shape。一个很好的阅读可能是这个SO answer

于 2017-05-26T18:15:07.900 回答