Haskell中面向对象的编程

Cli*_*ton 10 c++ oop haskell functional-programming virtual-functions

我试图了解Haskell中面向对象的样式编程,知道由于缺乏可变性,事情会有所不同.我玩过类型类,但我对它们的理解仅限于它们作为接口.所以我编写了一个C++示例,它是具有纯基础和虚拟继承的标准菱形.Bat继承FlyingMammal,以及FlyingMammal继承Animal.

#include <iostream>

class Animal
{
public:
    virtual std::string transport() const = 0;
    virtual std::string type() const = 0;
    std::string describe() const;
};

std::string Animal::describe() const 
    { return "I am a " + this->transport() + " " + this->type(); }

class Flying : virtual public Animal 
{
public:
    virtual std::string transport() const;
};

std::string Flying::transport() const { return "Flying"; }

class Mammal : virtual public Animal 
{
public:
    virtual std::string type() const;
};

std::string Mammal::type() const { return "Mammal"; }

class Bat : public Flying, public Mammal {};

int main() {
    Bat b;
    std::cout << b.describe() << std::endl;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

基本上我对如何将这样的结构转换为Haskell感兴趣,基本上这将允许我有一个Animals 列表,就像我可以Animal在C++中有一个(智能)指针数组.

lef*_*out 46

你只是不想那样做,甚至不要开始.OO肯定有它的优点,但像你的C++那样的"经典例子"几乎总是设计用于将范式归结为本科学生的大脑的设计结构,所以他们不会开始抱怨他们应该使用的语言愚蠢.

这个想法似乎基本上是用编程语言中的对象建模"真实世界的对象".这对于实际的编程问题的好方法,但它才有意义,如果你其实可以借鉴之间你会如何使用真实世界的对象和OO对象是如何在程序中处理的一个比喻.

对于这样的动物例子来说,这是荒谬的.如果有的话,方法必须是"饲料","牛奶","屠宰"......但"运输"是用词不当,我会采取这种方式来实际移动动物,这更像是一种方法动物所处的环境,基本上只是作为访客模式的一部分.

describe,type和你所说的transport是,在另一方面,要简单得多.这些基本上是类型相关的常量或简单的纯函数.只有OO偏执狂批准让他们成为班级方法.

如果你不尝试将它强制成类似于OO的东西,而只是在Haskell中保留(有用的类型化)数据,那么基本上只有数据的任何东西都会变得更简单.

因此,这个例子显然不会让我们进一步让我们考虑OOP 确实有意义的事情.Widget工具包浮现在脑海中.就像是

class Widget;

class Container : public Widget {
  std::vector<std::unique_ptr<Widget>> children;
 public:
  // getters ...
};
class Paned : public Container { public:
  Rectangle childBoundaries(int) const;
};
class ReEquipable : public Container { public:
  void pushNewChild(std::unique_ptr<Widget>&&);
  void popChild(int);
};
class HJuxtaposition: public Paned, public ReEquipable { ... };
Run Code Online (Sandbox Code Playgroud)

为什么OO在这里有意义?首先,它允许我们存储异构的小部件集合.这在Haskell中实际上并不容易实现,但在尝试之前,您可能会问自己是否真的需要它.对于某些容器来说,毕竟允许它可能并不是那么理想.在Haskell中,参数多态非常好用.对于任何给定类型的小部件,我们观察到的功能Container几乎简化为一个简单的列表.那么为什么不只是使用一个列表,无论你需要Container什么?

当然,在这个例子中,你可能会发现你确实需要异构容器; 获得它们的最直接方法是{-# LANGUAGE ExistentialQuantification #-}:

data GenericWidget = GenericWidget { forall w . Widget w => getGenericWidget :: w }
Run Code Online (Sandbox Code Playgroud)

在这种情况下,Widget它将是一个类型类(可能是抽象类的字面翻译Widget).在Haskell中,这是一个最后的手段,但可能就在这里.

Paned更像是一个界面.我们可能在这里使用另一个类型,基本上是音译C++:

class Paned c where
  childBoundaries :: c -> Int -> Maybe Rectangle
Run Code Online (Sandbox Code Playgroud)

ReEquipable更难,因为它的方法实际上改变了容器.这在Haskell中显然是个问题.但是你可能会发现它没有必要:如果你Container用普通列表替换了类,你可以将更新作为纯函数更新来进行.

但可能这对于手头的任务来说效率太低了.充分讨论有效地进行可变更新的方法对于这个答案的范围来说太多了,但是存在这样的方式,例如使用lenses.

摘要

OO对Haskell的翻译不太好.没有一个简单的通用同构,只有多个近似值可供选择,需要经验.尽可能经常地避免从OO角度解决问题,而是考虑数据,函数,monad层.事实证明,这让你在Haskell中走得很远.仅在少数应用程序中,OO非常自然,值得将其压入语言中.


抱歉,这个主题总是让我陷入强烈舆论咆哮模式......

这些偏执的部分原因是可变性的麻烦,这些麻烦在Haskell中没有出现.


Dan*_*zer 10

在Haskell中,没有一种很好的方法来制作继承的"树".相反,我们通常会做类似的事情

data Animal = Animal ...
data Mammal = Mammal Animal ...
data Bat    = Bat Mammal ...
Run Code Online (Sandbox Code Playgroud)

所以我们包含了共同的信息.这在OOP中并不常见,"赞成合成而不是继承".接下来我们创建这些接口,称为类型类

class Named a where
  name :: a -> String
Run Code Online (Sandbox Code Playgroud)

然后,我们会做Animal,MammalBat的情况下,Named但是,对于他们每个人是有意义的.

从那时起,我们就只写函数类型类的适当组合,我们并不真正关心的是Bat有一个Animal埋在它里面有一个名字.我们只是说

prettyPrint :: Named a => a -> String
prettyPrint a = "I love " ++ name a ++ "!"
Run Code Online (Sandbox Code Playgroud)

让底层的类型组件担心如何处理特定数据.例如,这让我们以多种方式编写更安全的代码

foo :: Top -> Top
bar :: Topped a => a -> a
Run Code Online (Sandbox Code Playgroud)

有了foo,我们不知道返回了什么子类型Top,我们必须做丑陋的,基于运行时的转换来弄清楚它.有了bar,我们静态地保证我们坚持我们的接口,但底层实现在整个功能中是一致的.这使得安全地组合在相同类型的不同接口上工作的函数变得更加容易.

TLDR; 在Haskell中,我们更合理地组合处理数据,然后依靠约束参数多态来确保跨具体类型的安全抽象而不牺牲类型信息.