C'è una Haskell equivalente di classi astratte di programmazione orientata agli oggetti, utilizzando i tipi di dati algebrici o polimorfismo?
Domanda
In Haskell, è possibile scrivere una funzione con una firma che può accettare due differenti (anche se simile) tipi di dati, e operano in modo diverso a seconda del tipo viene passato?
Un esempio potrebbe fare la mia domanda più chiara. Se ho una funzione denominata myFunction
, e due tipi di nome MyTypeA
e MyTypeB
, posso definire myFunction
in modo che può accettare solo i dati di tipo MyTypeA
o MyTypeB
come primo parametro?
type MyTypeA = (Int, Int, Char, Char)
type MyTypeB = ([Int], [Char])
myFunction :: MyTypeA_or_MyTypeB -> Char
myFunction constrainedToTypeA = something
myFunction constrainedToTypeB = somethingElse
In un linguaggio OOP, si potrebbe scrivere quello che sto cercando di realizzare in questo modo:
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();
}
}
Ho letto sui tipi di dati algebrici e penso che ho bisogno di definire un Haskell tipo , ma non sono sicuro di come fare per definire in modo che esso può memorizzare un tipo o un'altra, e anche come lo uso nelle mie funzioni.
Soluzione
Sì, lei ha ragione, siete alla ricerca di tipi di dati algebrici. C'è un tutorial su di loro a Imparare È un Haskell .
Per la cronaca, il concetto di una classe astratta da OOP ha in realtà tre diverse traduzioni in Haskell, e ADT sono solo uno. Ecco una rapida panoramica delle tecniche.
Tipi algebrica di dati
tipi di dati algebrici codificare il modello di una classe astratta cui sottoclassi sono noti, e dove le funzioni verificare quale esempio particolare l'oggetto è un membro di by-giù colata.
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");
}
si traduce in:
data IntBox = Empty | Full Int
get :: IntBox -> Int
get Empty = 0
get (Full x) = x
Record di funzioni
Questo stile non consente down-casting, quindi la funzione Get
di cui sopra non sarebbe esprimibile in questo stile. Così qui è qualcosa di completamente diverso.
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 sua traduzione in Haskell non mappa i tipi in tipi. Animal
è l'unico tipo, e Dog
e Cat
sono schiacciate via nelle loro funzioni di costruzione:
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" }
Ci sono un paio di permutazioni diverse di questo concetto di base. L'invariante è che il tipo astratto è un tipo di record in cui i metodi sono i campi del record.
EDIT:. C'è una buona discussione nei commenti su alcune delle sottigliezze di questo approccio, tra cui un bug nel codice di cui sopra
Typeclasses
Questo è il mio preferito almeno la codifica di idee OO. E 'comodo per i programmatori OO perché utilizza parole familiari e mappe tipi di tipi. Ma il record di funzioni approccio di cui sopra tende ad essere più facile lavorare con quando le cose si complicano.
Io codificare di nuovo l'esempio degli animali:
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"
Questo sembra bello, non è vero? La difficoltà arriva quando ti rendi conto che, anche se sembra OO, non funziona davvero come OO. Si potrebbe desiderare di avere una lista di animali, ma il meglio che si può fare in questo momento è Animal a => [a]
, una lista di animali omogenei, ad esempio. un elenco dei soli gatti o cani solo. Allora avete bisogno di fare questo tipo di involucro:
{-# 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 [AnyAnimal]
poi è quello che volete per la vostra lista di animali. Tuttavia, si scopre che espone AnyAnimal
esattamente le stesse informazioni su se stesso come il record Animal
nel secondo esempio, abbiamo appena andato su di esso in modo indiretto. Così perché non ritengo typeclasses di essere un ottimo codifica di OO.
E così conclude edizione di questa settimana di Way Too Much Information!
Altri suggerimenti
Sembra che si potrebbe desiderare di leggere su typeclasses .
Si consideri questo esempio utilizzando TypeClasses .
Si definisce un C ++ - come MVC
"classe astratta" sulla base di tre tipi (nota MultiParamTypeClasses
): tState
tAction
tReaction
al fine di
definire una funzione tState -> tAction -> (tState, tReaction)
chiave (quando viene applicata un'azione allo stato, si ottiene un nuovo Stato e una reazione.
Il typeclass ha
tre funzioni "c ++ astratto", e alcuni più definiti sul "astratte" quelli. Le funzioni "astratti" saranno definiti quando e instance MVC
è necessaria.
{-# 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