Есть ли в Haskell эквивалент абстрактных классов ООП, использующих алгебраические типы данных или полиморфизм? - PullRequest
32 голосов
/ 27 октября 2010

В 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 ]

64 голосов
/ 27 октября 2010

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

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

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

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

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-down, поэтому 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"); }
}

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

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

3 голосов
/ 27 октября 2010

Звучит так, как будто вы хотите прочитать о классах типов .

1 голос
/ 14 октября 2015

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

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

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