Существует ли эквивалент 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