Существует ли эквивалент Haskell абстрактных классов ООП, используя алгебраические типы данных или полиморфизм?

В Haskell можно ли написать функцию с сигнатурой, которая может принимать два разных (хотя похожие) типа данных и работать по-разному в зависимости от того, какой тип передан?

Пример может сделать мой вопрос более ясным. Если у меня есть функция с именем myFunction и два типа с именем MyTypeA и MyTypeB, могу ли я определить myFunction, чтобы он мог принимать только данные типа MyTypeA или MyTypeB в качестве своего первого параметра?

type MyTypeA = (Int, Int, Char, Char)
type MyTypeB = ([Int], [Char])

myFunction :: MyTypeA_or_MyTypeB -> Char
myFunction constrainedToTypeA = something
myFunction constrainedToTypeB = somethingElse

На языке OOP вы можете написать то, что я пытаюсь достичь так:

public abstract class ConstrainedType {
}

public class MyTypeA extends ConstrainedType {
    ...various members...
}

public class MyTypeB extends ConstrainedType {
    ...various members...
}

...

public Char myFunction(ConstrainedType a) {
    if (a TypeOf MyTypeA) {
        return doStuffA();
    }
    else if (a TypeOf MyTypeB) {
        return doStuffB();
    }
}

Я читал об алгебраических типах данных, и я думаю, что мне нужно определить тип Haskell, но я не уверен, как это определить, чтобы он мог хранить тот или иной тип, а также то, как я использую это в моих собственных функциях.

Ответы

Ответ 1

Да, вы правы, вы ищете алгебраические типы данных. На них есть отличный учебник по Learn You a Haskell.

Для записи понятие абстрактного класса из ООП фактически имеет три разных перевода в Haskell, и ADT - это всего лишь одно. Вот краткий обзор методов.

Алгебраические типы данных

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

abstract class IntBox { }

class Empty : IntBox { }

class Full : IntBox {
    int inside;
    Full(int inside) { this.inside = inside; }
}

int Get(IntBox a) {
    if (a is Empty) { return 0; }
    if (a is Full)  { return ((Full)a).inside; }
    error("IntBox not of expected type");
}

Переведено на:

data IntBox = Empty | Full Int

get :: IntBox -> Int
get Empty = 0
get (Full x) = x

Запись функций

Этот стиль не допускает down-casting, поэтому функция Get выше не была бы выражена в этом стиле. Итак, вот что-то совсем другое.

abstract class Animal { 
    abstract string CatchPhrase();
    virtual void Speak() { print(CatchPhrase()); }
}

class Cat : Animal {
    override string CatchPhrase() { return "Meow"; }
}

class Dog : Animal {
    override string CatchPhrase() { return "Woof"; }
    override void Speak() { print("Rowwrlrw"); }
}

Его перевод в Haskell не отображает типы в типы. Animal - единственный тип, а Dog и Cat отбрасываются в их конструкторские функции:

data Animal = Animal {
    catchPhrase :: String,
    speak       :: IO ()
}

protoAnimal :: Animal
protoAnimal = Animal {
    speak = putStrLn (catchPhrase protoAnimal)
}

cat :: Animal
cat = protoAnimal { catchPhrase = "Meow" }

dog :: Animal
dog = protoAnimal { catchPhrase = "Woof", speak = putStrLn "Rowwrlrw" }

Существует несколько различных перестановок этой базовой концепции. Инвариант состоит в том, что абстрактный тип является типом записи, где методы являются полями записи.

РЕДАКТИРОВАТЬ: В комментариях к некоторым из тонкостей этого подхода есть хорошая дискуссия, в том числе ошибка в приведенном выше коде.

Классы типов

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

Я снова закодирую пример Animal:

class Animal a where
    catchPhrase :: a -> String
    speak       :: a -> IO ()

    speak a = putStrLn (catchPhrase a)

data Cat = Cat 
instance Animal Cat where
    catchPhrase Cat = "Meow"

data Dog = Dog
instance Animal Dog where
    catchPhrase Dog = "Woof"
    speak Dog = putStrLn "Rowwrlrw"

Это выглядит хорошо, не так ли? Трудность возникает, когда вы понимаете, что, хотя она выглядит как OO, она не работает как OO. Возможно, вам понадобится список животных, но самое лучшее, что вы можете сделать прямо сейчас, - это Animal a => [a], список однородных животных, например. список только кошек или только собак. Затем вам нужно создать этот тип обертки:

data AnyAnimal = forall a. Animal a => AnyAnimal a
instance Animal AnyAnimal where
    catchPhrase (AnyAnimal a) = catchPhrase a
    speak (AnyAnimal a) = speak a

И тогда [AnyAnimal] - это то, что вы хотите для своего списка животных. Тем не менее, оказывается, что AnyAnimal предоставляет в точности одну и ту же информацию о себе как запись Animal во втором примере, мы просто обходили ее круговым способом. Таким образом, почему я не рассматриваю typeclasses как очень хорошую кодировку OO.

И таким образом заканчивается издание этой недели "Слишком много информации!

Ответ 2

Похоже, вы можете читать typeclasses.

Ответ 3

Рассмотрим этот пример, используя TypeClasses.

Мы определяем С++-подобный "абстрактный класс" MVC на основе трех типов (примечание MultiParamTypeClasses): tState tAction tReaction, чтобы определить ключевую функцию tState -> tAction -> (tState, tReaction) (когда действие применяется к состоянию, вы получаете новое состояние и реакцию.

Класс три абстрактные "С++ абстрактные" функции и некоторые более определенные на "абстрактных". "Абстрактные" функции будут определены, когда и instance MVC необходимо.

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, NoMonomorphismRestriction #-}


-- -------------------------------------------------------------------------------

class MVC tState tAction tReaction | tState -> tAction tReaction where
      changeState :: tState -> tAction -> tState       -- get a new state given the current state and an action ("abstract")
      whatReaction :: tState -> tReaction              -- get the reaction given a new state ("abstract")
      view :: (tState, tReaction) -> IO ()             -- show a state and reaction pair ("abstract")

      -- get a new state and a reaction given an state and an action (defined using previous functions)
      runModel :: tState -> tAction -> (tState, tReaction) 
      runModel s a = let
                                ns = (changeState s a) 
                                r = (whatReaction ns) 
                  in (ns, r)

      -- get a new state given the current state and an action, calling 'view' in the middle (defined using previous functions)
      run :: tState -> tAction -> IO tState
      run s a = do
                        let (s', r) = runModel s a
                        view (s', r)
                        return s'

      -- get a new state given the current state and a function 'getAction' that provides actions from "the user" (defined using previous functions)
      control :: tState -> IO (Maybe tAction) -> IO tState
      control s getAction = do
              ma <- getAction
              case ma of
                   Nothing -> return s
                   Just a -> do
                              ns <- run s a
                              control ns getAction


-- -------------------------------------------------------------------------------

-- concrete instance for MVC, where
-- tState=Int tAction=Char ('u' 'd') tReaction=Char ('z' 'p' 'n')
-- Define here the "abstract" functions
instance MVC Int Char Char where
         changeState i c 
                     | c == 'u' = i+1 -- up: add 1 to state
                     | c == 'd' = i-1 -- down: add -1 to state
                     | otherwise = i -- no change in state

         whatReaction i
                      | i == 0 = 'z' -- reaction is zero if state is 0
                      | i < 0 = 'n' -- reaction is negative if state < 0                     
                      | otherwise = 'p' -- reaction is positive if state > 0

         view (s, r) = do
                  putStrLn $ "view: state=" ++ (show s) ++ " reaction=" ++ (show r) ++ "\n"

--

-- define here the function "asking the user"
getAChar :: IO (Maybe Char) -- return (Just a char) or Nothing when 'x' (exit) is typed
getAChar = do
         putStrLn "?"
         str <- getLine
         putStrLn ""
         let c = str !! 0
         case c of
              'x' -> return Nothing
              _ -> return (Just c)


-- --------------------------------------------------------------------------------------------
-- --------------------------------------------------------------------------------------------

-- call 'control' giving the initial state and the "input from the user" function 
finalState = control 0 getAChar :: IO Int

-- 

main = do
     s <- finalState
     print s