Haskell中面向对象的多态性

时间:2014-11-18 22:33:26

标签: oop haskell polymorphism

所以我看到了一些问题,询问你如何在Haskell中进行面向对象编程,比如this。答案是"类型类的答案就像接口但不完全"。特别是类型类不允许为所有这些类型构建列表。例如。尽管有合理的结果,我们无法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)

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

那我哪里出错了?如何做得更好?说"不要在对象方面思考"很好,但这似乎有几个应用程序(例如角色扮演游戏中的角色列表......谁有一些共享属性但不同的能力,或者对象往往有意义的GUI编程)。

4 个答案:

答案 0 :(得分:2)

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

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的函数,但是不属于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

这使您可以灵活地处理不同类型的对象列表,或者使用相同的函数只使用一个类型的列表,而且绝对没有语言扩展。

请记住,这仍然是试图用严格的函数式语言实现某种对象模型,这种方式并不完全理想,但考虑到这一点可以让你拥有

  • 多个“接口”(转换为不同类型)
  • generics(totalArea :: IsShape s => [s] -> Float
  • 如果您要为Shape使用智能构造函数并为其添加更多方法,请使用密封方法,然后将其替换为arealocation
  • 如果您只允许智能构造函数设置
  • ,则启用未密封的方法
  • 公共和私人由模块导出设置

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

答案 1 :(得分:1)

您可以将existential quantification用于此目的:

{-# LANGUAGE ExistentialQuantification #-}

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

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

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

data Triangle = Triangle {
          tLocation :: Point
        , tSide1 :: Float
        , tSide2 :: Float
} deriving (Read, Show)

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

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)

data Shape = forall a. ShapeIf a => Shape a

instance ShapeIf Shape where
    location (Shape s) = location s
    area     (Shape s) = area s

p = Point 0 0

shlist :: [Shape]        
shlist = [Shape (Square p 0), Shape (Circle p 1), Shape (Triangle p 2 3)]

main = print $ map area shlist

但请注意,Haskell中没有向下转换,因此它不是Java风格子类型的直接类比。另请查看this

答案 2 :(得分:1)

在被指向this博客帖子关于存在量化是一种反模式(我以一种略显笨拙的方式重新发明)后,我尝试重写并提出:

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

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

data Shape = Shape {
      location :: Point
    , shape :: ShapeT
}

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

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

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

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

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

square :: Point -> Float -> Shape
square p l = Shape p (PolygonT $ SquareT (Square l))

circle :: Point -> Float -> Shape
circle p r = Shape p (CircleT (Circle r))

triangle :: Point -> Float -> Float -> Shape
triangle p s1 s2 = Shape p (PolygonT $ TriangleT (Triangle s1 s2))

area :: Shape -> Float
area = area' . shape

area' (PolygonT (SquareT (a))) = (sLength a) ^ 2
area' (CircleT (a)) = pi * (cRadius a) ^ 2
area' (PolygonT (TriangleT (a))) = 0.5 * (tSide1 a) * (tSide2 a)

答案 3 :(得分:0)

你可以得到茜草。

用Haskell术语分析,​​声明一个Java风格的类有很多东西:

  1. 声明存在一组共享公共接口的类型,要求该组的所有成员也是所有基类的成员。相关类型集
  2. 声明具体记录类型
  3. 声明新数据类型是新类型集的成员,以及所有基类'相关集合
  4. 声明一种存在类型,能够容纳任何具体类型的新类型
  5. 呼。接口,最终类等功能,如果您不需要/想要整个捆绑包,基本上可以跳过该列表的一部分。最重要的是,Java风格的类提供了一个模块系统,我根本不会解决这个问题。

    通过这种方式,如果您使用" OO设计模式" 可以在Haskell中获得以上所有内容自己实现每一个。但是在像Java这样的语言中,语言提供了很多很多的帮助,如果它存在的话,它会在Haskell中显示为合理的默认值和语法糖。一个例子是继承,它基本上是自动包含子类记录中的超类记录,并从子类实现自动委托到超类实现。 Haskell将不会给你任何帮助,所以一切都必须是明确的,并且" OO设计模式"出现了令人难以置信的冗长。

    第1部分很容易看到;共享通用接口的一组类型是类型类 。 Haskell允许我们将超类约束放在新类型上。完成。

    第2部分也很简单;只声明一个包含所有成员变量的新数据类型。请注意,如果您打算能够继承"继承"来自这个" class"并使用相同的评估器来获取成员变量,您希望将它们作为类型类的一部分,而不是仅使用Haskell的记录语法为您声明它们。如果你继续"继承"来自其他" OO模式"类,您希望将其数据类型包含为新数据类型的成员。

    第3部分是语言缺乏帮助开始变得单调乏味的地方。您需要为OO继承层次结构隐含的每个类型类实现实例,一直向上(即不仅仅是直接基础)。如果你没有覆盖"方法"那么这将是非常机械和乏味的,因为你可以委派所有"继承"包含基类成员数据的方法(如果您遵循该模式,它应该已经具有所有需要的实例)。这是手动实现OO继承的默认设置。

    第4部分是doozy。 OO程序员是存在量化类型的主人,他们只是不知道它。 Haskell支持存在量化类型,但只能通过扩展,并且有点笨拙。语言,习语和图书馆并不是真的希望你能够大量使用存在类型,所以你会开始使用它们来经历大量的摩擦;主要是以烦人的类型错误的形式,当你设法明确地写出正确的类型时会消失,偶尔你需要明确地eta扩展(即将f = foo变成f x = foo x ,高阶函数的逻辑应该说没有区别。)

    您可能认为我们不应该需要存在类型,因为类型类约束类型变量应该足以允许代码处理类型类的任何成员。麻烦的是,类型类约束的类型变量必须在每次调用类型类中的任何 one 类型时实例化(并且由调用者进行选择,而不是由到达的任何数据)在运行时)。

    这就是为什么类型类不允许你使用异类列表;虽然类型Shape a => [a]可以保存实现Shape的任何类型的对象,但列表的所有元素只有一个单一的类型变量,所以它们都必须相同&#34 ;任何实现Shape"的类型。存在类型是一个包装器,它包含带有类型变量的数据,但包装器本身不具有自己类型的类型变量。这样,您就可以获得[Shape]的列表,其中Shape内部包含ShapeI a => a

    我觉得我已经筋疲力尽了,如果没有示例代码我可以解释这个问题,所以这里就是这样。警告,这很难看:

    {-# LANGUAGE ExistentialQuantification, GADTs, RankNTypes #-}
    
    
    newtype Point = Point (Double, Double)
      deriving (Show, Eq)
    
    
    -- The Shape common interface
    -- Shape is just an interface, so no member data type
    class ShapeI a
      where area :: a -> Double
    
    -- The Shape existential reference
    data Shape 
      where Shape :: ShapeI a => a -> Shape 
    
    
    -- The Polygon common interface: 'subtype' of Shape
    -- Polygon is just an interface, so no member data type
    class ShapeI a => PolygonI a
      where vertexes :: a -> [Point]
    
    -- The Polygon existential reference
    data Polygon
      where Polygon :: PolygonI a => a -> Polygon
    
    
    -- The Circle common interface
    class ShapeI a => CircleI a
      where centre :: a -> Point 
            radius :: a -> Double
    
    -- The Circle existential reference
    data Circle
      where Circle :: CircleI a => a -> Circle
    
    -- The Circle member data type
    data CircleM = CircleM Point Double
      deriving (Show, Eq)
    
    -- Circles are Shapes
    instance ShapeI CircleM
      where area (CircleM _ r) = pi * r * r 
    
    -- Circles are Circles
    instance CircleI CircleM
      where centre (CircleM c _) = c
            radius (CircleM _ r) = r
    
    
    data Colour = Med | Blue
      deriving (Show, Eq)
    
    -- The ColouredCircle member data type
    -- ColouredCircle is final, so not bothering with a type class or existential reference
    data CircleColouredM = CircleColouredM CircleM Colour
      deriving (Show, Eq)
    
    
    -- ColouredCircles are Shapes
    instance ShapeI CircleColouredM
      where area (CircleColouredM circle _) = area circle
    
    -- ColouredCircles are Circles
    -- Note there is no actual implementation logic here, ColouredCircleM implements
    -- the Circle methods purely by using Circle's implementations
    instance CircleI CircleColouredM
      where centre (CircleColouredM circle _) = centre circle
            radius (CircleColouredM circle _) = radius circle
    
    
    -- The Triangle member data type
    -- Triangle is final, so not bothering with a type class or existential refernce
    data TriangleM = TriangleM Point Point Point
      deriving (Show, Eq)
    
    instance ShapeI TriangleM
      where area = const 7 -- In this hypothetical universe, all triangles have area 7
    
    instance PolygonI TriangleM
      where vertexes (TriangleM a b c) = [a, b, c]
    

    鉴于这一切:

    -- Heterogenous list of different types of objects which are all Circles
    circles :: [Circle]
    circles = [Circle (CircleM (Point (3, 7)) 2), Circle (CircleColouredM (CircleM (Point (8, 1)) 1) Blue)]
    
    
    -- Casts a Circle existential to a Shape existential
    -- Note that the object *indside* the existential reference is the same; we're
    -- just treating it as a Shape now
    circleToShape :: Circle -> Shape
    circleToShape (Circle c) = Shape c
    
    
    -- Heterogenous list of different types of objects which are all Shapes
    -- Note we are able to uniformly cast the circles list to shapes in order store
    -- them in this list, even though they're not all the same type already
    shapes :: [Shape]
    shapes = [Shape (TriangleM (Point (0, 0)) (Point (10, 0)) (Point (0, 10)))] ++ map circleToShape circles
    
    
    -- Helper function; can apply any function that is polymorphic in ShapeI to a
    -- Shape existential; the explicit type is necessary, because it's a rank 2 type
    apply :: (forall a. ShapeI a => a -> b) -> Shape -> b
    apply f (Shape x) = f x
    
    
    areas = map (apply area) shapes
    

    所以你看到我们确实得到异类列表(或者一般来说,类型可以独立地拥有"类"并且允许访问该类'公共接口),OO样式继承层次结构(尽管使用手动样板继承方法不变),甚至是向上转换。

    您可能遇到的另一个问题是Haskell对类型规则的严格要求。你无法垂头丧气;实际上,除了Shape界面隐含的内容之外,您无法引用ShapeI存在主义的任何属性;所有关于它特别包含的知识都消失了。

    这也意味着shapes列表几乎无用;我们可以用它做的唯一有意义的事情是map (apply area) shapes,所以我们可能已经废除了大量的样板,并且首先创建了一个Double列表。此外,OO语言中的根类往往提供了惊人的功能;你可以toString Java中的任意对象,比较它们是否相等等等。你在这里得到 none 。一旦某些内容成为存在的引用,您就可以访问 nothing ,但其约束条件可以说明。没有Show约束,没有show方法(即使我在这里使用的所有类型执行实现Show)。同样,没有Eq约束,没有==函数;这可能不会像你在这里那样工作,因为(作为惯用的Haskell函数并且不期望处理模仿OO类heirarchies的存在性)==只适用于保证为两个值的两个值。相同的类型,存在参考放弃了所有关于任何特定类型的知识,所以你永远不能保证。

    我确定你可以改进上面的模式以使它更有用,甚至可以自动化它的一部分(我们可以写一个通用的upcast函数吗?TemplateHaskell可以为我们生成样板吗?)。如果你把Typeable之类的约束放入混合中,你甚至应该能够获得运行时检查的向下转换(如果你真的想要的话),并且可能能够实现一个有效的相等运算符(返回False为不同的具体类型并在类型匹配时委托给==。但我个人并不是非常倾向于试图进一步充实这一点。

    TLDR:OO样式类(忽略突变)基本上是类型类的特定组合,类型包含成员数据,存在类型,具有大量默认机制,使其易于工作而不是一个巨大的痛苦。鉴于Haskell为每个部分提供了正交最小概念,我发现分别更好地了解和理解这些概念并单独应用它们或者在需要时将它们应用于,而不是采用OO&采用瑞士军刀方法,并试图强制每个程序适合该结构提供的设施。