Объектно-ориентированное программирование в 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, в основном это позволит мне иметь список Animals, как будто у меня может быть массив (умных) указателей на Animals в C++.

4 ответов


вы просто не хотите этого делать, даже не начинайте. ОО, конечно, имеет свои достоинства, но "классические примеры", такие как ваш c++, почти всегда являются надуманными структурами, предназначенными для забивания парадигмы в мозги студентов, чтобы они не начали жаловаться на то, насколько глупы языки, которые они должны использовать.

идея кажется в основном моделированием" реальных объектов " объектами на вашем языке программирования. Что может быть хорошим подходом для фактические проблемы программирования, но это имеет смысл только в том случае, если вы можете провести аналогию между тем, как вы используете реальный объект и как объекты OO обрабатываются внутри программы.

что просто смешно для таких примеров животных. Во всяком случае, методы должны быть такими, как "корм", "молоко", "убой"... но "транспорт" - неправильное название, я бы взял это на самом деле движение животное, которое предпочло бы быть методом окружающей среды животное живет, и в основном имеет смысл только как часть шаблона посетителя.

describe, type и то, что вы называете transport С другой стороны, гораздо проще. Это в основном зависящие от типа константы или простые чистые функции. Только ОО паранойя&ddagger; ратифицирует, делая их методами класса.

любая вещь вдоль линий этого животного материала, где есть в основном только данные, становится намного проще, если вы не пытаетесь заставить его что-то вроде 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 настолько естественно, что это стоит вдавливая его в язык.


Извините, эта тема всегда приводит меня в режим сильного мнения...

&ddagger;


в 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

вы можете, очевидно, сделать список Animals без проблем. Иногда людям нравится реализовывать это через 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 значения и, например, печать описания каждого животного.