Y at-il un équivalent Haskell des classes abstraites de POO, en utilisant des types de données algébriques ou polymorphisme?
Question
Dans Haskell, est-il possible d'écrire une fonction avec une signature qui peut accepter deux différents (bien que similaires) types de données, et fonctionnent différemment selon le type est passé dans?
exemple pourrait prendre une ma question plus claire. Si j'ai une fonction nommée myFunction
, et deux types nommés MyTypeA
et MyTypeB
, puis-je définir myFunction
de sorte qu'il ne peut accepter que des données de type MyTypeA
ou MyTypeB
comme premier paramètre?
type MyTypeA = (Int, Int, Char, Char)
type MyTypeB = ([Int], [Char])
myFunction :: MyTypeA_or_MyTypeB -> Char
myFunction constrainedToTypeA = something
myFunction constrainedToTypeB = somethingElse
Dans un langage OOP, vous pouvez écrire ce que je suis en train de réaliser comme ceci:
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();
}
}
J'ai lu sur les types de données algébriques et je pense que je dois définir une Haskell type , mais je ne suis pas sûr de savoir comment s'y prendre pour définir afin qu'il puisse stocker un type ou une autre, et aussi comment je l'utilise dans mes fonctions.
La solution
Oui, vous avez raison, vous êtes à la recherche des types de données algébriques. Il y a un grand tutoriel sur les Vous apprenez Haskell .
Pour mémoire, le concept d'une classe abstraite de POO a en fait trois traductions différentes en Haskell et ADTs sont juste un. Voici un aperçu rapide des techniques.
Types de données Algebraic
types de données algébriques coder le modèle d'une classe abstraite dont les sous-classes sont connues, et où les fonctions de vérifier quelle instance particulière l'objet est un élément de terre-par coulée.
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");
}
se traduit:
data IntBox = Empty | Full Int
get :: IntBox -> Int
get Empty = 0
get (Full x) = x
Enregistrement des fonctions
Ce style ne permet pas vers le bas de coulée, de sorte que la fonction Get
ci-dessus ne serait pas exprimable dans ce style. Voici donc quelque chose de complètement différent.
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"); }
}
La traduction en Haskell ne mappe pas les types en types. Animal
est le seul type et Dog
et Cat
sont écrasées loin dans leurs fonctions constructeur:
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" }
Il y a quelques permutations différentes de ce concept de base. L'invariant est que le type abstrait est un type d'enregistrement où les méthodes sont les champs de l'enregistrement.
EDIT:. Il y a une bonne discussion dans les commentaires sur quelques-unes des subtilités de cette approche, y compris un bogue dans le code ci-dessus
classes de types
Ceci est mon préféré moins le codage des idées OO. Il est à l'aise pour les programmeurs OO, car il utilise des mots familiers et des cartes types de types. Mais le bilan des fonctions approche ci-dessus tend à être plus facile de travailler quand les choses se compliquent.
Je vais encode à nouveau l'exemple des animaux:
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"
Cette apparence agréable, non? La difficulté vient quand on se rend compte que même si elle ressemble à OO, il n'a vraiment pas le travail comme OO. Vous voudrez peut-être d'avoir une liste des animaux, mais le mieux que vous pouvez faire est en ce moment Animal a => [a]
, une liste d'animaux homogènes, par exemple. une liste de seulement chats ou chiens seulement. Ensuite, vous devez faire ce type d'emballage:
{-# LANGUAGE ExistentialQuantification #-}
data AnyAnimal = forall a. Animal a => AnyAnimal a
instance Animal AnyAnimal where
catchPhrase (AnyAnimal a) = catchPhrase a
speak (AnyAnimal a) = speak a
Et [AnyAnimal]
alors est ce que vous voulez pour votre liste d'animaux. Cependant, il se avère que AnyAnimal
expose de exactement les mêmes informations sur lui-même que le dossier de Animal
dans le second exemple, nous venons de faire à ce sujet d'une manière détournée. Ainsi pourquoi je ne considère pas comme une classes de types très bon encodage OO.
Et conclut ainsi l'édition de cette semaine de Way Too Much Information!
Autres conseils
On dirait que vous pouvez lire sur rel="nofollow"> classes de types.
Considérez cet exemple en utilisant classes de types .
Nous définissons un c ++ - comme "classe abstraite" MVC
basée sur trois types (note MultiParamTypeClasses
): tState
tAction
tReaction
pour
définir une fonction clé tState -> tAction -> (tState, tReaction)
(lorsqu'une action est appliquée à l'état, vous obtenez un nouvel état et une réaction.
La classe de types a
trois fonctions « c ++ abstrait », et un peu plus défini sur les « abstraits ». Les fonctions « abstraites » seront définis quand et instance MVC
est nécessaire.
{-# 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