Объектно-ориентированное программирование в Haskell
Я пытаюсь понять объектно-ориентированное программирование стиля в 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
s, как будто у меня может быть массив (умных) указателей на Animal
s в C++.
4 ответов
вы просто не хотите этого делать, даже не начинайте. ОО, конечно, имеет свои достоинства, но "классические примеры", такие как ваш c++, почти всегда являются надуманными структурами, предназначенными для забивания парадигмы в мозги студентов, чтобы они не начали жаловаться на то, насколько глупы языки, которые они должны использовать†.
идея кажется в основном моделированием" реальных объектов " объектами на вашем языке программирования. Что может быть хорошим подходом для фактические проблемы программирования, но это имеет смысл только в том случае, если вы можете провести аналогию между тем, как вы используете реальный объект и как объекты OO обрабатываются внутри программы.
что просто смешно для таких примеров животных. Во всяком случае, методы должны быть такими, как "корм", "молоко", "убой"... но "транспорт" - неправильное название, я бы взял это на самом деле движение животное, которое предпочло бы быть методом окружающей среды животное живет, и в основном имеет смысл только как часть шаблона посетителя.
describe
, type
и то, что вы называете transport
С другой стороны, гораздо проще. Это в основном зависящие от типа константы или простые чистые функции. Только ОО паранойя‡ ратифицирует, делая их методами класса.
любая вещь вдоль линий этого животного материала, где есть в основном только данные, становится намного проще, если вы не пытаетесь заставить его что-то вроде OO, но просто оставайтесь с (с пользой набранным) сведения в Haskell.
так как этот пример, очевидно, не приводит нас дальше, давайте рассмотрим что-то, где ООП тут смысла. Виджеты toolkits приходят на ум. Что-то вроде
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 { ... };
почему ОО имеет смысл здесь? Во-первых, он легко позволяет нам хранить гетерогенную коллекцию виджетов. Это на самом деле нелегко достичь в Haskell, но прежде чем попробовать, вы можете спросить себя, если тебе это действительно нужно. Для некоторых контейнеров это, возможно, не так желательно, чтобы позволить это, в конце концов. В Хаскелле,параметрический полиморфизм очень приятно использовать. Для любого заданного типа виджета мы наблюдаем функциональность Container
в значительной степени сводится к простому списку. Так почему бы просто не использовать список, где вам требуется Container
?
конечно, в этом примере, вы, вероятно, найдете вы do нужны гетерогенные контейнеры; самые прямые способ их получения -{-# LANGUAGE ExistentialQuantification #-}
:
data GenericWidget = GenericWidget { forall w . Widget w => getGenericWidget :: w }
в этом случае Widget
будет классом типа (может быть довольно буквальный перевод абстрактного класса Widget
). В Хаскелле это скорее последнее средство, но может быть прямо здесь.
Paned
это скорее интерфейс. Мы могли бы использовать здесь другой тип класса, в основном, Транслитерация на C++:
class Paned c where
childBoundaries :: c -> Int -> Maybe Rectangle
ReEquipable
сложнее, потому что его методы фактически мутируют контейнер. Это очевидно проблематично в Haskell. Но опять же вы можете обнаружить, что это не нужно: если вы заменили Container
класс по простым спискам, вы можете сделать обновления как чисто функциональные обновления.
хотя, возможно, это было бы слишком неэффективно для задачи под рукой. Полное обсуждение способов эффективного выполнения изменяемых обновлений было бы слишком много для области этого ответа, но такие способы существуют, например, использование lenses
.
резюме
OO не слишком хорошо переводит на Haskell. Существует не один простой общий изоморфизм, только несколько приближений, среди которых для выбора требуется опыт. Как можно чаще вы должны избегать подхода к проблеме под углом OO и думать о данных, функциях, слоях монады. Оказывается, это очень далеко заводит вас в Хаскелле. Только в нескольких приложениях OO настолько естественно, что это стоит вдавливая его в язык.
†Извините, эта тема всегда приводит меня в режим сильного мнения...
‡
в Haskell нет хорошего метода для создания" деревьев " наследования. Вместо этого мы обычно делаем что-то вроде
data Animal = Animal ...
data Mammal = Mammal Animal ...
data Bat = Bat Mammal ...
таким образом, мы инкапсулируем общую информацию. Что не так уж редко встречается в ООП, "предпочтение композиции над наследством". Затем мы создаем эти интерфейсы, называемые Type classes
class Named a where
name :: a -> String
тогда мы бы сделали Animal
, Mammal
и Bat
экземпляров Named
однако это имело смысл для каждого из них.
С тех пор, мы просто напишите функции в соответствующую комбинацию классов типов, нам все равно, что Bat
есть Animal
похоронен внутри него с именем. Мы просто говорим
prettyPrint :: Named a => a -> String
prettyPrint a = "I love " ++ name a ++ "!"
и пусть базовые typeclasses беспокоятся о том, как обрабатывать конкретные данные. Это позволяет нам писать более безопасный код во многих отношениях, например
foo :: Top -> Top
bar :: Topped a => a -> a
С foo
, мы понятия не имеем что подтип Top
возвращается, мы должны сделать уродливые, основанные на времени выполнения кастинг, чтобы выяснить это. С bar
, мы статически гарантируем, что мы придерживаемся нашего интерфейса, но что базовая реализация согласована по всей функции. Это значительно упрощает безопасное создание функций, которые работают на разных интерфейсах для одного и того же типа.
TLDR; в Haskell мы составляем данные более композиционно, а затем полагаемся на ограниченный параметрический полиморфизм для обеспечения безопасной абстракции по конкретным типам без ущерба для типа информация.
есть много способов успешно реализовать это в Haskell, но мало что будет "чувствовать", как Java. Вот один пример: мы будем моделировать каждый тип независимо, но предоставлять операции "cast", которые позволяют нам обрабатывать подтипы 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
s без проблем. Иногда людям нравится реализовывать это через ExistentialTypes
и typeclasses
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)
чтобы мы могли бы просто пойти с первым решением.
многие другие ответы уже намекают на то, как классы типа может быть вам интересно. Однако я хочу отметить, что, по моему опыту, много раз, когда вы думаете, что класс-это решение проблемы, на самом деле это не так. Я считаю, что это особенно верно для людей с фоном ООП.
на самом деле есть очень популярная статья в блоге об этом, 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
значения и, например, печать описания каждого животного.