poo - ¿Hay un equivalente de Haskell de las clases abstractas de OOP, usando tipos de datos algebraicos o polimorfismo?
ejercicios resueltos de clases abstractas en java (3)
En Haskell, ¿es posible escribir una función con una firma que pueda aceptar dos tipos de datos diferentes (aunque similares), y operar de manera diferente dependiendo de qué tipo se pase?
Un ejemplo podría aclarar mi pregunta. Si tengo una función llamada myFunction
, y dos tipos llamados MyTypeA
y MyTypeB
, ¿puedo definir myFunction
para que solo pueda aceptar datos de tipo MyTypeA
o MyTypeB
como su 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 OOP, podrías escribir lo que intento lograr así:
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 sobre tipos de datos algebraicos y creo que necesito definir un tipo de Haskell, pero no estoy seguro de cómo definirlo para que pueda almacenar un tipo u otro, y también cómo lo uso en mi funciones propias
Considere este ejemplo usando TypeClasses .
Definimos un MVC
"clase abstracta" similar a c ++ basado en tres tipos (note MultiParamTypeClasses
): tState
tAction
tReaction
para definir una función clave tState -> tAction -> (tState, tReaction)
(cuando se aplica una acción al estado , obtienes un nuevo estado y una reacción.
La clase de tipo tiene tres funciones "abstractas c ++" y algunas más definidas en las "abstractas". Las funciones "abstractas" se definirán cuando se necesite la 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
Parece que es posible que desee leer sobre typeclasses de typeclasses .
Sí, tiene razón, está buscando tipos de datos algebraicos. Hay un gran tutorial sobre ellos en Learn You a Haskell .
Para el registro, el concepto de una clase abstracta de OOP en realidad tiene tres traducciones diferentes en Haskell, y los ADT son solo uno. Aquí hay una descripción general rápida de las técnicas.
Tipos de datos algebraicos
Los tipos de datos algebraicos codifican el patrón de una clase abstracta cuyas subclases son conocidas, y donde las funciones comprueban a qué instancia particular pertenece el objeto al realizar un down-casting.
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 como:
data IntBox = Empty | Full Int
get :: IntBox -> Int
get Empty = 0
get (Full x) = x
Registro de funciones
Este estilo no permite el down-casting, por lo que la función Get
anterior no se puede expresar en este estilo. Entonces aquí hay 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 mapea tipos en tipos. Animal
es el único tipo, y Dog
y Cat
son aplastados en sus funciones de constructor:
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. Lo invariante es que el tipo abstracto es un tipo de registro donde los métodos son los campos del registro.
EDITAR: Hay una buena discusión en los comentarios sobre algunas de las sutilezas de este enfoque, incluida una falla en el código anterior.
Tipos de clases
Esta es mi codificación favorita menos de ideas OO. Es cómodo para los programadores OO porque usa tipos de palabras y mapas conocidos para los tipos. Pero el enfoque del registro de funciones arriba es más fácil de usar cuando las cosas se complican.
Volveré a codificar el ejemplo 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"
Esto se ve bien, ¿no? La dificultad surge cuando te das cuenta de que, aunque parezca OO, en realidad no funciona como OO. Es posible que desee tener una lista de Animales, pero lo mejor que puede hacer ahora es Animal a => [a]
, una lista de animales homogéneos, por ej. una lista de solo gatos o solo perros. Entonces necesitas hacer este tipo de envoltura:
{-# 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 luego [AnyAnimal]
es lo que quieres para tu lista de animales. Sin embargo, resulta que AnyAnimal
expone exactamente la misma información sobre sí mismo que el registro de Animal
en el segundo ejemplo, lo hemos seguido de una manera indirecta. Entonces, ¿por qué no considero que las clases de tipos son una muy buena codificación de OO?
¡Y así concluye la edición de esta semana de Way Too Much Information!