Есть ли в 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

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

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

3 ответа

Решение

Да, вы правы, вы ищете алгебраические типы данных. На сайте 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

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

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

Я снова закодирую пример 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"

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

{-# LANGUAGE ExistentialQuantification #-}

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.

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

Класс типов имеет три "абстрактные" функции C++, и некоторые более определенные для "абстрактных". "Абстрактные" функции будут определены, когда и 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
Другие вопросы по тегам