面向对象的Haskell多态性 [英] Object Oriented Polymorphism in Haskell

查看:137
本文介绍了面向对象的Haskell多态性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

所以我看到了一些问题,问你如何在Haskell中进行面向对象的编程,比如这个例如。答案是沿着类型类似接口但不完全的方式。特别是一个类的类不允许所有这些类型的列表。例如。我们不能做 map show [1,1.4,hello] ,尽管这有一个逻辑结果。



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

 模块形状(
Shape(..)
,Point
,Circle(..)
,Triangle(。 。)
,Square(..)
,位置
,area
)其中

数据点=点{
xcoord :: Float
,ycoord :: Float
}派生(读取,显示)

data Shape = CircleT Circle | PolygonT Polygon派生(读取,显示)

data Circle = Circle {
cLocation :: Point
,cRadius :: Float
}派生(读取,显示)

数据多边形= SquareT Square | TriangleT Triangle(读取,显示)

数据Square = Square {
sLocation :: Point
,sLength :: Float
}派生(读取,显示)

- 只有直角三角形才能实现!
data Triangle = Triangle {
tLocation :: Point
,tSide1 :: Float
,tSide2 :: Float
}派生(读取,显示)

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

实例ShapeIf Shape其中
位置(CircleT a)=位置a
位置(PolygonT a)=位置a
区域(CircleT a)=区域a
区域(PolygonT a)=区域a

实例ShapeIf多边形其中
位置(SquareT a)=位置a
位置(TriangleT a)=位置a
区域(SquareT a)=区域a
区域(TriangleT a)=区域a

实例ShapeIf Square其中
位置= sLocation
区域a =(sLength a )^ 2

实例ShapeIf Circle其中
位置= c位置
区域a = pi *(cRadius a)^ 2

实例ShapeIf三角形其中
位置= t位置
面积a = 0.5 *(tSide1 a)*(tSide2 a)



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

那么我哪里错了?这怎么做得更好?说不要用对象来思考是很好的,但这似乎有几个应用程序(例如角色扮演游戏中的角色列表......具有一些共享属性但功能不同的角色,或GUI对象往往倾向于为了达到目的)。

解决方案

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

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

data形状= Shape
{shapeLocation :: Point
,shapeArea :: Float
}派生(Eq,Show)

这可能是您需要的唯一两种类型,具体取决于您的应用程序,因为您可以编写函数

  circle :: Point  - >浮动 - > Shape 
circle loc radius = Shape loc $ pi * r * r

square :: Point - >浮动 - > Shape
square loc sLength = Shape loc $ sLength * sLength

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

但是也许你想要保留这些论点。在这种情况下,为每个数据类型写一个数据类型

  data Circle = Circle 
{cLocation :: Point
,cRadius :: Float
}派生(Eq,Show)

数据Square = Square
{sLocation :: Point
,sLength :: Float
)派生(Eq,Show)

data三角形=三角形
{tLocation :: Point
,tBase :: Float
,tHeight :: Float
}派生(Eq,Show)

然后为了方便起见,我在这里使用一个类型类来定义 toShape

  class IsShape s其中
toShape :: s - >形状

实例IsShape Shape其中
toShape = id

实例IsShape Circle其中
toShape(圆形位置半径)=形状位置$ pi * radius *半径

实例IsShape Square其中
toShape(Square loc sideLength)=形状loc $ sideLength * sideLength

实例IsShape三角形其中
toShape(三角形loc基地高度)=形状LOC $ 0.5 *基地*高度

但现在有问题,你必须将每种类型转换为 Shape 以便以更通用的方式获取其区域或位置,除了您可以添加函数

  location :: IsShape s => s  - >点
位置= shapeLocation。 toShape

area :: IsShape s => s - >浮动
区域= shapeArea。 toShape

我会把这些放在 IsShape 类,以便它们不能被重新实现,这类似于对所有 Monad replicateM 之类的函数> s,但不是 Monad typeclass的一部分。现在您可以编写代码,如

  twiceArea :: IsShape s => s  - >浮动
twiceArea =(2 *)。区域

当你只使用单个形状参数时,这很好。如果你想操作它们的集合:

  totalArea :: IsShape s => [s]  - >浮动
totalArea =总和。地图区域

因此,您不必依靠存在来构建它们的集合,您可以取而代之的是

 >让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 使用智能构造函数并添加更多方法然后使用区域和位置

  • 未密封方法将它们别名化if您只允许这些由智能构造函数设置

  • public和private由模块导出设置



以及其他一些OOP范例,所有代码都比使用Java或C#的代码少得多,唯一的区别是代码并不是全部组合在一起。这有好处和坏处,例如能够更自由地定义新的实例和数据类型,但使代码更难以浏览。


So I have seen questions that ask how do you do Object Oriented Programming in Haskell, like this for example. To which the answer is along the lines of "type classes are like interfaces but not quite". In particular a type class doesn't allow a list to be built of all those types. E.g. we can't do map show [1, 1.4, "hello"] despite that having a logical result.

Given some time I wondered if it wasn't possible to do better. So I had an attempt at coding polymorphism for a simple Shape class, which can be found below (if you like sanity probably better to stop reading now, and apologies for it being so long).

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)

Despite all the madness this ends up having some quite nice properties: I can have a list of shapes and I can map functions over them that make sense (like location and area). But also if I have a particular Shape (say a Triangle) then I can also call area just on that. But it is horrendous. I don't like the code at all (indeed I'm sure it would be much shorter in any object oriented programming language).

So where have I gone wrong? How can this be made nicer? Saying "don't think in terms of objects" is nice, but this seems to have several applications (e.g. a list of characters in a role playing game ... who have some shared attributes but different abilities, or GUI programming where objects tend to make sense).

解决方案

You can use simple data types for this purpose without resorting to typeclasses. If you do want to use typeclasses, it's better to use it to describe a conversion to your base type rather than having it include all the implementation details:

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

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

This might be the only two types you need, depending on your application, since you could write functions

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

But maybe you want to preserve those arguments. In which case, write a data type for each

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)

Then for convenience, I'd use a typeclass here to define 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

But now there's the problem that you have to convert each type to Shape in order to get its area or location in a more generic way, except you can just add the functions

location :: IsShape s => s -> Point
location = shapeLocation . toShape

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

I would keep these out of the IsShape class so that they can't be re-implemented, this is similar to functions like replicateM that work on all Monads, but aren't part of the Monad typeclass. Now you can write code like

twiceArea :: IsShape s => s -> Float
twiceArea = (2 *) . area

And this is fine when you're only operating on a single shape argument. If you want to operate on a collection of them:

totalArea :: IsShape s => [s] -> Float
totalArea = sum . map area

So that you don't have to rely on existentials to build a collection of them you can instead have

> 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

This gives you the flexibility to work on a list of objects of different types, or on a list of just a single type using the same function and absolutely no language extensions.

Bear in mind that this is still trying to implement a sort of object model in a strictly functional language, something that isn't going to be completely ideal, but considering this allows you to have

  • multiple "interfaces" (conversions to different types)
  • generics (totalArea :: IsShape s => [s] -> Float)
  • sealed methods if you were to use a smart constructor for Shape and add more methods to it then alias them like with area and location
  • unsealed methods if you just allowed those to be set by the smart constructor
  • public and private are set by module exports

and probably some other OOP paradigms, all with really less code than it would take in Java or C#, the only difference is that the code isn't all grouped together. This has it's benefits and disadvantages, such as being able to define new instances and data types more freely, but making the code somewhat more difficult to navigate.

这篇关于面向对象的Haskell多态性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆