¿Hay un equivalente de Haskell clases abstractas de programación orientada a objetos, utilizando tipos de datos algebraicos o polimorfismo?
Pregunta
En Haskell, es posible escribir una función con una firma que puede aceptar dos tipos de datos diferentes (aunque similar), y funcionan de manera diferente dependiendo de qué tipo se pasa en?
Un ejemplo podría hacer mi pregunta clara. Si tengo una función llamada myFunction
, y dos tipos con nombre MyTypeA
y MyTypeB
, puedo definir myFunction
de modo que sólo puede aceptar datos de MyTypeA
tipo o MyTypeB
como primer parámetro?
type MyTypeA = (Int, Int, Char, Char)
type MyTypeB = ([Int], [Char])
myFunction :: MyTypeA_or_MyTypeB -> Char
myFunction constrainedToTypeA = something
myFunction constrainedToTypeB = somethingElse
En un lenguaje de programación orientada a objetos, se podría escribir lo que estoy tratando de lograr de esta manera:
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();
}
}
He estado leyendo acerca de los tipos de datos algebraicos y yo creo que tenga que definir una Haskell type , pero no estoy seguro de cómo hacer para definirlo para que pueda almacenar un tipo o otra, y también cómo lo uso en mis propias funciones.
Solución
Sí, estás en lo correcto, que busca los tipos de datos algebraicos. Hay un gran tutorial sobre ellos en Aprender Eres una Haskell .
Para el registro, el concepto de una clase abstracta de la programación orientada a objetos en realidad tiene tres diferentes traducciones en Haskell, y los ADT son sólo uno. Aquí está una descripción rápida de las técnicas.
Tipos algebraica de datos
tipos de datos algebraicos codificar el patrón de una clase abstracta cuya subclases son conocidos, y donde las funciones de comprobar qué instancia particular el objeto es un miembro de por abajo de la fundición.
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 traduce en:
data IntBox = Empty | Full Int
get :: IntBox -> Int
get Empty = 0
get (Full x) = x
Registro de funciones
Este estilo no permite bajar a presión, por lo que la función de Get
anterior no sería expresable en este estilo. Así que aquí es 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"); }
}
Su traducción en Haskell no asignar tipos de un tipo determinado. Animal
es el único tipo, y Dog
y Cat
se aplastan de distancia en sus funciones constructoras:
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" }
Hay algunas permutaciones diferentes de este concepto básico. El invariante es que el tipo abstracto es un tipo de registro, donde los métodos son los campos del registro.
EDIT:. Hay una buena discusión en los comentarios sobre algunas de las sutilezas de este enfoque, incluyendo un error en el código anterior
clases de tipos
Esta es mi menos favorito de codificación de ideas OO. Es cómodo para los programadores orientados a objetos, ya que utiliza palabras familiares y los mapas de tipos de tipos. Pero el registro de las funciones de enfoque anterior tiende a ser más fácil de trabajar cuando las cosas se complican.
Voy a codificar de nuevo el ejemplo de los animales:
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"
Esto se ve bien, no es cierto? La dificultad viene cuando se da cuenta de que a pesar de que parece OO, que en realidad no funciona como OO. Es posible que desee tener una lista de los animales, pero lo mejor que puede hacer en este momento es Animal a => [a]
, una lista de animales homogéneos, por ejemplo. una lista de sólo los gatos o perros solamente. Luego hay que hacer este tipo de envoltorio:
{-# LANGUAGE ExistentialQuantification #-}
data AnyAnimal = forall a. Animal a => AnyAnimal a
instance Animal AnyAnimal where
catchPhrase (AnyAnimal a) = catchPhrase a
speak (AnyAnimal a) = speak a
Y [AnyAnimal]
a continuación es lo que quiere para su lista de animales. Sin embargo, resulta que expone AnyAnimal
exactamente la misma información sobre sí mismo como el registro Animal
en el segundo ejemplo, se acaba de ir sobre él de una manera indirecta. Por lo tanto ¿por qué no considero clases de tipos a ser una muy buena codificación de OO.
Y así concluye la edición de esta semana de demasiada información!
Otros consejos
Parece que es posible que desee leer sobre clases de tipos .
Considere este ejemplo utilizando clases de tipos .
Se define un c ++ - como MVC
"clase abstracta" basado en tres tipos (nota): MultiParamTypeClasses
tState
tAction
tReaction
con el fin de
definir una función clave tState -> tAction -> (tState, tReaction)
(cuando se aplica una acción para el estado, se obtiene un nuevo estado y una reacción.
La clase de tipos tiene
tres funciones "C ++ abstracto", y un poco más definido en los "abstractos". Las funciones "abstractos" se definirán cuándo y instance MVC
que se necesita.
{-# 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