所以我看到了一些问题,问你如何在 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
这使您可以灵活地处理不同类型的对象列表,或者使用相同功能且绝对没有语言扩展的仅单一类型的列表。
请记住,这仍在尝试以严格的函数式语言实现一种对象模型,这并不完全理想,但考虑到这一点,您可以拥有
totalArea :: IsShape s => [s] -> Float
) Shape
使用智能构造函数,则密封方法并向其添加更多方法,然后使用 area
给它们起别名和 location
可能还有其他一些 OOP 范例,所有代码都比 Java 或 C# 中的代码少,唯一的区别是代码没有全部组合在一起。这有其优点和缺点,例如能够更自由地定义新的实例和数据类型,但使代码更难以导航。
关于oop - Haskell 中的面向对象多态性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27005419/