我正在尝试理解Haskell中面向对象的样式编程,知道由于缺乏可变性,事情会有所不同。我玩过类型类,但我对它们的理解仅限于它们作为接口。所以我编写了一个C ++示例,它是具有纯基础和虚拟继承的标准菱形。 Bat
继承Flying
和Mammal
,Flying
和Mammal
都继承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感兴趣,基本上这将允许我有一个Animal
的列表,就像我可以有一个(智能)指针数组{{1用C ++编写。
答案 0 :(得分:46)
你只是不想这样做,甚至不要开始。 OO肯定有它的优点,但像你的C ++那样的“经典例子”几乎总是设计用于将范式归结为本科生的大脑的设计结构,所以他们不会开始抱怨他们应该使用的语言是多么愚蠢< SUP>†
这个想法似乎基本上是用编程语言中的对象建模“真实世界的对象”。对于实际的编程问题,这可能是一个很好的方法,但只有你可以在你如何使用现实世界对象和如何在程序中处理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 { ... };
为什么OO在这里有意义?首先,它允许我们存储异构的小部件集合。这在Haskell中实际上并不容易实现,但在尝试之前,您可能会问自己是否真的需要它。对于某些容器,毕竟可能不太容易这样做。在Haskell中,参数多态非常好用。对于任何给定类型的小部件,我们观察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非常自然,值得将其压入语言中。
† 对不起,这个主题总是让我陷入强烈舆论模式......
‡ 这些偏执的部分原因是可变性的麻烦,这些麻烦在Haskell中没有出现。
答案 1 :(得分:9)
在Haskell中,没有一种很好的方法来制作继承的“树”。相反,我们通常会做类似
的事情data Animal = Animal ...
data Mammal = Mammal Animal ...
data Bat = Bat Mammal ...
因此我们将公共信息包含在内。这在OOP中并不常见,“赞成合成而不是继承”。接下来,我们创建这些接口,称为类型类
class Named a where
name :: a -> String
然后我们会制作Animal
,Mammal
和Bat
Named
个实例,但这对每个实例都有意义。
从那时起,我们只是将函数写入适当的类型组合,我们并不关心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中,我们更合理地组合处理数据,然后依靠约束参数多态来确保跨具体类型的安全抽象而不牺牲类型信息。
答案 2 :(得分:2)
有许多方法可以在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
然后,你可以毫无困难地列出Animal
的列表。有时人们喜欢通过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
允许我们将Flying
和Mammal
直接注入列表
animals :: [AnyAnimal]
animals = [AnyAnimal flyingType, AnyAnimal mammalType]
但实际上并没有比原始示例好多了,因为我们已经丢弃了列表中每个元素的所有信息,除了它有一个IsAnimal
实例,仔细看,完全等同于说它只是一个Animal
。
projectAnimal :: IsAnimal a => a -> Animal
projectAnimal a = Animal (transport a) (type a) (description a)
所以我们也可以选择第一个解决方案。
答案 3 :(得分:2)
许多其他答案已暗示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
值的列表,例如打印每只动物的描述。