【问题标题】:Object oriented programming in HaskellHaskell 中的面向对象编程
【发布时间】:2013-12-09 15:51:06
【问题描述】:

我试图了解 Haskell 中的面向对象风格编程,因为我知道由于缺乏可变性,事情会有些不同。我玩过类型类,但我对它们的理解仅限于它们作为接口。所以我编写了一个 C++ 示例,它是具有纯基和虚拟继承的标准菱形。 Bat 继承 FlyingMammalFlyingMammal 都继承 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;
}

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

【问题讨论】:

  • 您熟悉类型类吗? en.wikipedia.org/wiki/Type_class
  • 正如问题所说,是的,但是我不确定如何将它们用作层次结构中的部分实现类(而不是基本上 Java 接口)。我认为示例代码是最快的学习方式。
  • @Clinton 你的用例是什么?我认为当你开始尝试像面向对象一样编写 Haskell 代码时会遇到问题。
  • @bheklilr:这只是学习 C++ 概念如何(或不)转换为 Haskell 的示例。
  • 该示例需要一个真实世界的上下文,其中 OOP 是有意义的,以便正确翻译成 Haskell。编写一个简单的程序,其中 OOP 结构很重要。

标签: c++ oop haskell functional-programming virtual-functions


【解决方案1】:

你只是不想这样做,甚至不要开始。 OO 确实有其优点,但是像 C++ 这样的“经典示例”几乎总是人为设计的结构,旨在将范式锤入本科生的大脑,这样他们就不会开始抱怨他们应该使用的语言有多愚蠢†.

这个想法似乎基本上是通过您的编程语言中的对象来建模“现实世界的对象”。对于实际的编程问题,这可能是一种很好的方法,但只有当您实际上可以在您如何使用现实世界的对象和如何在程序中处理 OO 对象之间进行类比时,它才有意义。

对于这样的动物例子来说,这简直是荒谬的。如果有的话,这些方法必须是诸如“饲料”、“牛奶”、“屠宰”之类的东西……但“运输”是一个用词不当,我会用它来真正移动动物,这宁愿是动物生活环境的一种方法,并且基本上仅作为访客模式的一部分才有意义。

另一方面,describetype 和你所谓的transport 则要简单得多。这些基本上是依赖于类型的常量或简单的纯函数。只有 OO 偏执狂&ddagger; 批准将它们设为类方法。

任何类似于这种动物的东西,基本上只有数据,如果你不尝试强制它变成类似 OO 的东西,而只是留下来(有用的类型) 数据 在 Haskell 中。

所以这个例子显然没有给我们带来任何进一步的帮助,让我们考虑一下 OOP 确实 有意义的事情。小部件工具包浮现在脑海中。类似的东西

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 { ... };

为什么 OO 在这里有意义?首先,它很容易让我们存储异构的小部件集合。这实际上在 Haskell 中并不容易实现,但在尝试之前,您可能会问自己是否真的需要它。毕竟,对于某些容器,允许这样做可能不是那么可取。在 Haskell 中,parametric polymorphism 非常好用。对于任何给定类型的小部件,我们观察到Container 的功能几乎可以简化为一个简单的列表。那么为什么不在需要Container 的地方使用列表呢?

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

data GenericWidget = GenericWidget { forall w . Widget w => getGenericWidget :: w }

在这种情况下,Widget 将是一个类型类(可能是抽象类Widget 的字面翻译)。在 Haskell 中,这是不得已而为之的事情,但可能就在这里。

Paned 更像是一个接口。我们可能在这里使用另一个类型类,基本上是对 C++ 的音译:

class Paned c where
  childBoundaries :: c -> Int -> Maybe Rectangle

ReEquipable 更难,因为它的方法实际上改变了容器。这在 Haskell 中显然有问题。但同样,您可能会发现没有必要:如果您已将 Container 类替换为普通列表,您可能可以将更新作为纯功能更新。

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

总结

OO 不能很好地转换为 Haskell。没有一个简单的通用同构,只有多个近似值可供选择,需要经验。您应该尽可能避免从 OO 的角度来处理问题,而是考虑数据、函数、monad 层。事实证明,这会让你在 Haskell 中走得很远。只有在少数应用程序中,OO 是如此自然,值得将其压入语言中。


对不起,这个话题总是让我陷入强烈的观点咆哮模式......

&ddagger;这些偏执的部分原因是可变性的麻烦,而这在 Haskell 中不会出现。

【讨论】:

    【解决方案2】:

    在 Haskell 中,没有一个很好的方法来制作继承的“树”。相反,我们通常会这样做

    data Animal = Animal ...
    data Mammal = Mammal Animal ...
    data Bat    = Bat Mammal ...
    

    所以我们封装了公共信息。这在 OOP 中并不少见,“喜欢组合而不是继承”。接下来我们创建这些接口,称为类型类

    class Named a where
      name :: a -> String
    

    然后我们会创建NamedAnimalMammalBat 实例,但这对他们每个人来说都是有意义的。

    从那时起,我们只需将函数写入类型类的适当组合,我们并不真正关心 Bat 有一个 Animal 埋在其中并带有名称。我们只是说

    prettyPrint :: Named a => a -> String
    prettyPrint a = "I love " ++ name a ++ "!"
    

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

    foo :: Top -> Top
    bar :: Topped a => a -> a
    

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

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

    【讨论】:

      【解决方案3】:

      有很多方法可以在 Haskell 中成功实现这一点,但很少有“感觉”像 Java。这是一个例子:我们将独立地对每种类型进行建模,但提供“转换”操作,允许我们将Animal 的子类型视为Animal

      data Animal = Animal String String String
      data Flying = Flying String String
      data Mammal = Mammal String String
      
      castMA :: Mammal -> Animal
      castMA (Mammal transport description) = Animal transport "Mammal" description
      
      castFA :: Flying -> Animal
      castFA (Flying type description) = Animal "Flying" type description
      

      然后您显然可以毫无问题地列出Animals。有时人们喜欢通过ExistentialTypes 和类型类来实现这一点

      class IsAnimal a where
        transport :: a -> String
        type :: a -> String
        description :: a -> String
      
      instance IsAnimal Animal where
        transport (Animal tr _ _) = tr
        type (Animal _ t _) = t
        description (Animal _ _ d) = d
      
      instance IsAnimal Flying where ...
      instance IsAnimal Mammal where ...
      
      data AnyAnimal = forall t. IsAnimal t => AnyAnimal t
      

      这让我们可以将FlyingMammal 一起直接注入到一个列表中

      animals :: [AnyAnimal]
      animals = [AnyAnimal flyingType, AnyAnimal mammalType]
      

      但这实际上并不比原来的例子好多少,因为我们已经丢弃了关于列表中每个元素的所有信息,除了它有一个 IsAnimal 实例,仔细看,它完全等同于说它是只是一个Animal

      projectAnimal :: IsAnimal a => a -> Animal
      projectAnimal a = Animal (transport a) (type a) (description a)
      

      所以我们还是选择了第一个解决方案。

      【讨论】:

      • "我们已经丢弃了列表中每个元素的所有信息,除了它有一个 IsAnimal 实例" --> 严格来说,即使在 OO 语言中,使用向下转换也是“错误的” .只是 OO 语言几乎没有提供以“正确”方式完成工作的工具。
      • 操作员正在寻找如何制作动物名单,所以不管怎样都有一个沮丧的人。
      • 你也可以在 Haskell 中使用 Typeable。不过,这很少是个好主意。
      • 同意——这是一个我真的不想潜入的兔子洞。我一直在寻找的唯一稍微好的用途是 Typing Dynamic Typing 的东西,我仍在努力思考这是否值得。
      【解决方案4】:

      许多其他答案已经暗示您可能对type classes 感兴趣。但是,我想指出,根据我的经验,很多时候当你认为类型类是问题的解决方案时,实际上并非如此。我相信对于具有 OOP 背景的人来说尤其如此。

      实际上有一篇非常受欢迎的博客文章,Haskell Antipattern: Existential Typeclass,你可能会喜欢它!

      解决问题的一种更简单的方法可能是将接口建模为纯代数数据类型,例如

      data Animal = Animal {
          animalTransport :: String,
          animalType :: String
      }
      

      这样你的bat 就变成了一个普通的值:

      flyingTransport :: String
      flyingTransport = "Flying"
      
      mammalType :: String
      mammalType = "Mammal"
      
      bat :: Animal
      bat = Animal flyingTransport mammalType
      

      有了这个,您可以定义一个描述任何动物的程序,就像您的程序一样:

      describe :: Animal -> String
      describe a = "I am a " ++ animalTransport a ++ " " ++ animalType a
      
      main :: IO ()
      main = putStrLn (describe bat)
      

      这使得拥有Animal 值的列表变得容易,例如打印每种动物的描述。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-09-18
        • 1970-01-01
        • 2011-04-16
        • 1970-01-01
        • 1970-01-01
        • 2011-02-24
        • 2011-07-09
        • 1970-01-01
        相关资源
        最近更新 更多