面向对象的Haskell多态性

编程入门 行业动态 更新时间:2024-10-27 14:19:28
本文介绍了面向对象的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 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多态性

本文发布于:2023-08-04 00:55:52,感谢您对本站的认可!
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:多态性   面向对象   Haskell

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!