問題描述
我正在嘗試了解 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.
這篇關于Haskell 面向對象編程的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網!