Haskell 面向对象编程
我正在尝试了解 Haskell 中面向对象风格的编程,因为我知道由于缺乏可变性,情况会有所不同.我玩过类型类,但我对它们的理解仅限于它们作为接口.所以我编写了一个 C++ 示例,它是具有纯基础和虚拟继承的标准菱形.Bat
继承了Flying
和Mammal
,Flying
和Mammal
都继承了动物
.
#include 类动物{上市:虚拟 std::string 传输()const = 0;虚拟 std::string type() const = 0;std::string describe() const;};std::string Animal::describe() const{ return "I am a " + this->transport() + " " + this->type();}飞行类:虚拟公共动物{上市:虚拟 std::string 传输()const;};std::string Flying::transport() const { return "Flying";}类哺乳动物:虚拟公共动物{上市:虚拟 std::string type() const;};std::string Mammal::type() const { return "哺乳动物";}类蝙蝠:公共飞行,公共哺乳动物{};int main() {蝙蝠 b;std::cout <<b.描述()<
基本上我对如何将这样的结构翻译成 Haskell 感兴趣,基本上这将允许我有一个 Animal
的列表,就像我可以有一个(智能)指针数组Animal
s in C++.
你只是不想那样做,甚至不要开始.OO 固然有它的优点,但是经典的例子"就像你的 C++ 一样,它几乎总是人为的结构,旨在将范式打入本科生的大脑,这样他们就不会开始抱怨他们应该使用的语言是多么愚蠢†.>
这个想法似乎基本上是对真实世界的对象"进行建模;通过您的编程语言中的对象.对于实际的编程问题,这可能是一种很好的方法,但只有当您实际上可以在如何使用现实世界的对象和如何在程序内部处理 OO 对象之间进行类比时,这才有意义.
对于这样的动物例子来说,这简直是荒谬的.如果有的话,这些方法必须是像饲料"、牛奶"、屠宰"……但是运输"之类的东西.用词不当,我认为实际上是移动动物,这更像是动物生活环境的一种方法,并且基本上只作为访客模式的一部分才有意义.另一方面,
describe
、type
和您所说的transport
要简单得多.这些基本上是依赖于类型的常量或简单的纯函数.只有 OO 偏执狂‡ 批准将它们设为类方法.
任何与动物相关的东西,基本上只有数据,如果你不尝试强迫它变成类似面向对象的东西,而是保持(有用的输入) 数据在Haskell中.
所以这个例子显然没有给我们带来任何进一步的帮助,让我们考虑一下 OOP 确实有意义的事情.小部件工具包浮现在脑海中.类似的东西
class Widget;类容器:公共小部件{std::vector<std::unique_ptr<Widget>>孩子们;上市://吸气剂 ...};类窗格:公共容器{公共:矩形 childBoundaries(int) const;};类 ReEquipable :公共容器 { 公共:void pushNewChild(std::unique_ptr<Widget>&&);void popChild(int);};类 HJuxtaposition: public Paned, public ReEquipable { ... };
为什么 OO 在这里有意义?首先,它很容易让我们存储一个异构的小部件集合.这在 Haskell 中实际上并不容易实现,但在尝试之前,您可能会问自己是否真的需要它.毕竟,对于某些容器,允许这样做可能不是那么可取.在 Haskell 中,参数多态非常好用.对于任何给定类型的小部件,我们观察到 Container
的功能几乎简化为一个简单的列表.那么为什么不直接使用列表,无论您在何处需要 Container
?
当然,在这个例子中,你可能会发现你确实需要异构容器;最直接的获取方式是{-# LANGUAGE ExistentialQuantification #-}
:
data GenericWidget = GenericWidget { forall w .小部件 w =>getGenericWidget :: w }
在这种情况下,Widget
将是一个类型类(可能是抽象类 Widget
的字面翻译).在 Haskell 中,这是最后的手段,但可能就在这里.
Paned
更像是一个界面.我们可能会在这里使用另一种类型类,基本上是音译 C++ 类:
class Paned c wherechildBoundaries :: c ->内部 ->也许矩形
ReEquipable
更难,因为它的方法实际上改变了容器.这在 Haskell 中显然是有问题的.但是您可能会再次发现这没有必要:如果您用普通列表替换了 Container
类,您可能能够将更新作为纯功能更新进行.
尽管如此,这对于手头的任务来说效率太低了.充分讨论有效地进行可变更新的方法对于本答案的范围来说太过分了,但是存在这样的方法,例如使用 lenses
.
总结
OO 不能很好地转换为 Haskell.没有一种简单的通用同构,只有在其中选择的多个近似值需要经验.你应该尽可能避免从面向对象的角度来解决问题,而是考虑数据、函数、monad 层.事实证明,这会让你在 Haskell 中走得更远.只有在少数应用程序中,OO 非常自然,值得将其压入语言中.
<小时>†抱歉,这个话题总是让我陷入强烈的观点咆哮模式......
‡这些偏执的部分原因是可变性的问题,这在 Haskell 中没有出现.
I'm trying to get an understanding of object oriented style programming in Haskell, knowing that things are going to be a bit different due to lack of mutability. I've played around with type classes, but my understanding of them is limited to them as interfaces. So I've coded up a C++ example, which is the standard diamond with a pure base and virtual inheritance. Bat
inherits Flying
and Mammal
, and both Flying
and Mammal
inherit 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;
}
Basically I'm interested in how to translate such a structure into Haskell, basically that would allow me to have a list of Animal
s, like I could have an array of (smart) pointers to Animal
s in C++.
You just don't want to do that, don't even start. OO sure has its merits, but “classic examples” like your C++ one are almost always contrived structures designed to hammer the paradigm into undergraduate students' brains so they won't start complaining about how stupid the languages are they're supposed to use†.
The idea seems basically modelling “real-world objects” by objects in your programming language. Which can be a good approach for actual programming problems, but it only makes sense if you can in fact draw an analogy between how you'd use the real-world object and how the OO objects are handled inside the program.
Which is just ridiculous for such animal examples. If anything, the methods would have to be stuff like “feed”, “milk”, “slaughter”... but “transport” is a misnomer, I'd take that to actually move the animal, which would rather be a method of the environment the animal lives in, and basically makes only sense as part of a visitor pattern.
describe
, type
and what you call transport
are, on the other hand, much simpler. These are basically type-dependent constants or simple pure functions. Only OO paranoia‡ ratifies making them class methods.
Any thing along the lines of this animal stuff, where there's basically only data, becomes way simpler if you don't try do force it into something OO-like but just stay with (usefully typed) data in Haskell.
So as this example obviously doesn't bring us any further let's consider something where OOP does make sense. Widget toolkits come to the mind. Something like
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 { ... };
Why OO makes sense here? First, it readily allows us to store a heterogeneous collection of widgets. That's actually not easy to achieve in Haskell, but before trying it, you might ask yourself if you really need it. For certain containers, it's perhaps not so desirable to allow this, after all. In Haskell, parametric polymorphism is very nice to use. For any given type of widget, we observe the functionality of Container
pretty much reduces to a simple list. So why not just use a list, wherever you require a Container
?
Of course, in this example, you'll probably find you do need heterogeneous containers; the most direct way to obtain them is {-# LANGUAGE ExistentialQuantification #-}
:
data GenericWidget = GenericWidget { forall w . Widget w => getGenericWidget :: w }
In this case Widget
would be a type class (might be a rather literal translation of the abstract class Widget
). In Haskell this is rather a last-resort thing to do, but might be right here.
Paned
is more of an interface. We might use another type class here, basically transliterating the C++ one:
class Paned c where
childBoundaries :: c -> Int -> Maybe Rectangle
ReEquipable
is more difficult, because its methods actually mutate the container. That is obviously problematic in Haskell. But again you might find it's not necessary: if you've substituted the Container
class by plain lists, you might be able to do the updates as pure-functional updates.
Probably though, this would be too inefficient for the task at hand. Fully discussing ways to do mutable updates efficiently would be too much for the scope of this answer, but such ways exists, e.g. using lenses
.
Summary
OO doesn't translate too well to Haskell. There isn't one simple generic isomorphism, only multiple approximations amongst which to choose requires experience. As often as possible, you should avoid approaching the problem from an OO angle alltogether and think about data, functions, monad layers instead. It turns out this gets you very far in Haskell. Only in a few applications, OO is so natural that it's worth pressing it into the language.
†Sorry, this subject always drives me into strong-opinion rant mode...
‡These paranoia are partly motivated by the troubles of mutability, which don't arise in Haskell.
相关文章