oop - Haskell 中的面向对象多态性

标签 oop haskell polymorphism

所以我看到了一些问题,问你如何在 Haskell 中进行面向对象编程,比如 this例如。答案是“类型类就像接口(interface)但不完全一样”。特别是类型类不允许构建所有这些类型的列表。例如。我们做不到map show [1, 1.4, "hello"]尽管有一个合乎逻辑的结果。

有一段时间我想知道是否不可能做得更好。所以我尝试为一个简单的 Shape 类编写多态性,可以在下面找到它(如果你喜欢理智可能最好现在停止阅读,并为它这么长而道歉)。

module Shapes (
          Shape(..)
        , Point
        , Circle(..)
        , Triangle(..)
        , Square(..)
        , location
        , area
) where

data Point = Point {
          xcoord :: Float
        , ycoord :: Float
} deriving (Read, Show)

data Shape = CircleT Circle | PolygonT Polygon deriving (Read, Show)

data Circle = Circle {
          cLocation :: Point
        , cRadius :: Float
} deriving (Read, Show)

data Polygon = SquareT Square | TriangleT Triangle deriving (Read, Show)

data Square = Square {
          sLocation :: Point
        , sLength :: Float
} deriving (Read, Show)

-- only right angled triangles for ease of implementation!
data Triangle = Triangle {
          tLocation :: Point
        , tSide1 :: Float
        , tSide2 :: Float
} deriving (Read, Show)

class ShapeIf a where
        location :: a -> Point
        area :: a -> Float

instance ShapeIf Shape where
        location (CircleT a) = location a
        location (PolygonT a) = location a
        area (CircleT a) = area a
        area (PolygonT a) = area a

instance ShapeIf Polygon where
        location (SquareT a) = location a
        location (TriangleT a) = location a
        area (SquareT a) = area a
        area (TriangleT a) = area a

instance ShapeIf Square where
        location = sLocation
        area a = (sLength a) ^ 2

instance ShapeIf Circle where
        location = cLocation
        area a = pi * (cRadius a) ^ 2

instance ShapeIf Triangle where
        location = tLocation
        area a = 0.5 * (tSide1 a) * (tSide2 a)

尽管这很疯狂,但最终还是有一些非常好的属性:我可以有一个形状列表,我可以在它们上映射有意义的函数(比如位置和区域)。但如果我有一个特定的形状(比如三角形),那么我也可以在上面调用 area 。但这太可怕了。我根本不喜欢这段代码(事实上,我确信它在任何面向对象的编程语言中都会短得多)。

那么我哪里出错了?这怎么能变得更好?说“不要考虑对象”很好,但这似乎有几个应用程序(例如角色扮演游戏中的角色列表......具有一些共享属性但不同的能力,或者对象倾向于的 GUI 编程有意义)。

最佳答案

您可以为此目的使用简单的数据类型,而无需求助于类型类。如果您确实想使用类型类,最好使用它来描述对基本类型的转换,而不是让它包含所有实现细节:

data Point = Point
    { xcoord :: Float
    , ycoord :: Float
    } deriving (Eq, Read, Show)

data Shape = Shape
    { shapeLocation :: Point
    , shapeArea :: Float
    } deriving (Eq, Show)

这可能是您唯一需要的两种类型,具体取决于您的应用程序,因为您可以编写函数
circle :: Point -> Float -> Shape
circle loc radius = Shape loc $ pi * r * r

square :: Point -> Float -> Shape
square loc sLength = Shape loc $ sLength * sLength

triangle :: Point -> Float -> Float -> Shape
triangle loc base height = Shape loc $ 0.5 * base * height

但也许你想保留这些论点。在这种情况下,为每个写一个数据类型
data Circle = Circle
    { cLocation :: Point
    , cRadius :: Float
    } deriving (Eq, Show)

data Square = Square
    { sLocation :: Point
    , sLength :: Float
    } deriving (Eq, Show)

data Triangle = Triangle
    { tLocation :: Point
    , tBase :: Float
    , tHeight :: Float
    } deriving (Eq, Show)

然后为方便起见,我将在这里使用类型类来定义 toShape :
class IsShape s where
    toShape :: s -> Shape

instance IsShape Shape where
    toShape = id

instance IsShape Circle where
    toShape (Circle loc radius) = Shape loc $ pi * radius * radius

instance IsShape Square where
    toShape (Square loc sideLength) = Shape loc $ sideLength * sideLength

instance IsShape Triangle where
    toShape (Triangle loc base height) = Shape loc $ 0.5 * base * height

但是现在有一个问题,你必须将每种类型转换为 Shape为了以更通用的方式获取其区域或位置,除了您可以添加功能
location :: IsShape s => s -> Point
location = shapeLocation . toShape

area :: IsShape s => s -> Float
area = shapeArea . toShape

我会将这些排除在 IsShape 之外类,这样它们就不能被重新实现,这类似于 replicateM 之类的函数适用于所有 Monad s,但不属于 Monad类型类。现在您可以编写如下代码
twiceArea :: IsShape s => s -> Float
twiceArea = (2 *) . area

当您只对单个形状参数进行操作时,这很好。如果要对它们的集合进行操作:
totalArea :: IsShape s => [s] -> Float
totalArea = sum . map area

这样您就不必依赖存在主义来构建它们的集合,而是可以拥有
> let p = Point 0 0
> totalArea [toShape $ Circle p 5, toShape $ Square p 10, toShape $ Triangle p 10 20]
278.53983
> totalArea $ map (Square p) [1..10]
385.0

这使您可以灵活地处理不同类型的对象列表,或者使用相同功能且绝对没有语言扩展的仅单一类型的列表。

请记住,这仍在尝试以严格的函数式语言实现一种对象模型,这并不完全理想,但考虑到这一点,您可以拥有
  • 多个“接口(interface)”(转换为不同类型)
  • 泛型 ( totalArea :: IsShape s => [s] -> Float )
  • 如果要对 Shape 使用智能构造函数,则密封方法并向其添加更多方法,然后使用 area 给它们起别名和 location
  • 如果您只允许智能构造函数设置未密封的方法
  • public 和 private 由模块导出
  • 设置

    可能还有其他一些 OOP 范例,所有代码都比 Java 或 C# 中的代码少,唯一的区别是代码没有全部组合在一起。这有其优点和缺点,例如能够更自由地定义新的实例和数据类型,但使代码更难以导航。

    关于oop - Haskell 中的面向对象多态性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27005419/

    相关文章:

    c++ - 增加破坏速度

    Python抽象

    javascript - Haskell WARP/WAI 服务器无法响应来自 Android 嵌入式 WebView 的 AJAX 调用

    java - 从派生类调用构造函数

    haskell - 如何在不阻塞 Haskell 线程的情况下从进程中检索输出

    haskell - 在没有 Monoid 实例的情况下,如何处理 Control.Lens.Indexed 中 at 的 Maybe 结果

    c++ - 具有派生类实例的工厂

    c# - 具有序列化类的层次结构中的多态性 - DataContract 消息

    java - 包含具有相同签名的方法的多个接口(interface)

    c++ - 有没有一种方法可以复制派生类指针的 vector 而不将其强制转换为基类?