Existe um equivalente a Haskell das classes abstratas da OOP, usando tipos de dados algébricos ou polimorfismo?
Pergunta
Em Haskell, é possível escrever uma função com uma assinatura que possa aceitar dois tipos de dados diferentes (embora semelhantes) e operar de maneira diferente, dependendo de que tipo é passado?
Um exemplo pode deixar minha pergunta mais clara. Se eu tiver uma função chamada myFunction
, e dois tipos nomeados MyTypeA
e MyTypeB
, posso definir myFunction
para que só possa aceitar dados do tipo MyTypeA
ou MyTypeB
Como seu primeiro parâmetro?
type MyTypeA = (Int, Int, Char, Char)
type MyTypeB = ([Int], [Char])
myFunction :: MyTypeA_or_MyTypeB -> Char
myFunction constrainedToTypeA = something
myFunction constrainedToTypeB = somethingElse
Em uma linguagem OOP, você pode escrever o que estou tentando alcançar assim:
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();
}
}
Eu tenho lido sobre tipos de dados algébricos e acho que preciso definir um haskell modelo, mas não tenho certeza de como defini -lo para que ele possa armazenar um tipo ou outro, e também como eu o uso em minhas próprias funções.
Solução
Sim, você está correto, está procurando tipos de dados algébricos. Há um ótimo tutorial sobre eles em Aprender você um haskell.
Para o registro, o conceito de classe abstrata da OOP realmente tem três traduções diferentes em Haskell, e o ADTS é apenas um. Aqui está uma rápida visão geral das técnicas.
Tipos de dados algébricos
Os tipos de dados algébricos codificam o padrão de uma classe abstrata cujas subclasses são conhecidas e onde as funções verificam qual instância específica o objeto é um membro da fundição.
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");
}
Traduz em:
data IntBox = Empty | Full Int
get :: IntBox -> Int
get Empty = 0
get (Full x) = x
Registro de funções
Este estilo não permite a decepagem, então o Get
A função acima não seria expressa nesse estilo. Então, aqui está algo completamente diferente.
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"); }
}
Sua tradução em Haskell não mapeia tipos em tipos. Animal
é o único tipo, e Dog
e Cat
são esmagados em suas funções de construtor:
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" }
Existem algumas permutações diferentes desse conceito básico. O invariante é que o tipo abstrato é um tipo de registro em que os métodos são os campos do registro.
EDIT: Há uma boa discussão nos comentários sobre algumas das sutilezas dessa abordagem, incluindo um bug no código acima.
TypeCelasses
Esta é a minha codificação menos favorita de idéias de OO. É confortável para os programadores OO porque usa tipos familiares de palavras e mapas para tipos. Mas a abordagem do registro de funções acima tende a ser mais fácil de trabalhar quando as coisas ficam complicadas.
Vou codificar o exemplo animal novamente:
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"
Isso parece bom, não é? A dificuldade ocorre quando você percebe que, embora pareça oo, ele realmente não funciona como OO. Você pode querer ter uma lista de animais, mas o melhor que você pode fazer agora é Animal a => [a]
, uma lista de animais homogêneos, por exemplo. uma lista de apenas gatos ou apenas cães. Então você precisa fazer este wrapper tipo:
{-# LANGUAGE ExistentialQuantification #-}
data AnyAnimal = forall a. Animal a => AnyAnimal a
instance Animal AnyAnimal where
catchPhrase (AnyAnimal a) = catchPhrase a
speak (AnyAnimal a) = speak a
E depois [AnyAnimal]
é o que você deseja para a sua lista de animais. No entanto, acontece que AnyAnimal
expõe exatamente a mesma informação sobre si mesma que o Animal
Registre no segundo exemplo, acabamos de fazer isso de uma maneira indireta. Portanto, por que não considero o TypeCelasses uma codificação muito boa do OO.
E assim conclui a edição desta semana de Muita informação!
Outras dicas
Parece que você pode querer ler em TypeCelasses.
Considere este exemplo usando TypeCelasses.
Definimos um C ++-como "classe abstrata" MVC
com base em três tipos (nota MultiParamTypeClasses
): tState
tAction
tReaction
Para definir uma função chave tState -> tAction -> (tState, tReaction)
(Quando uma ação é aplicada ao estado, você obtém um novo estado e uma reação.
O TypeClass possui três funções "C ++ Abstract" e algumas mais definidas nos "abstratos". As funções "abstratas" serão definidas quando e instance MVC
é preciso.
{-# 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