una que programacion poo polimorfico orientada objetos instanciar herencia ejemplos clases clase haskell types typeclass

haskell - que - Cuándo usar una clase de tipo, cuándo usar un tipo



programacion orientada a objetos python pdf (3)

Estaba revisando un fragmento de código que escribí para hacer una búsqueda combinatoria hace unos meses, y noté que había una manera alternativa y más simple de hacer algo que había logrado anteriormente con una clase de tipo.

Específicamente, anteriormente tenía una clase de tipo para el tipo de problemas de búsqueda , que tienen un estado de tipo s , acciones (operaciones en estados) de tipo a , un estado inicial, una forma de obtener una lista de pares (de acción, de estado) y una forma de probar si un estado es una solución o no:

class Problem p s a where initial :: p s a -> s successor :: p s a -> s -> [(a,s)] goaltest :: p s a -> s -> Bool

Esto es algo insatisfactorio, ya que requiere la extensión MultiParameterTypeClass, y generalmente necesita FlexibleInstances y posiblemente TypeSynonymInstances cuando quiera hacer instancias de esta clase. También desordena las firmas de tu función, por ejemplo

pathToSolution :: Problem p => p s a -> [(a,s)]

Me di cuenta hoy de que puedo deshacerme de la clase por completo, y utilizar un tipo en su lugar, a lo largo de las siguientes líneas

data Problem s a { initial :: s, successor :: s -> [(a,s)], goaltest :: s -> Bool }

Esto no requiere ninguna extensión, las firmas de función se ven mejor:

pathToSolution :: Problem s a -> [(a,s)]

y, lo más importante, descubrí que después de refacturar mi código para usar esta abstracción en lugar de una clase de tipo, me quedé con un 15-20% menos de líneas de las que tenía anteriormente.

El mayor logro fue en el código que creaba abstracciones usando la clase de tipo. Anteriormente tuve que crear nuevas estructuras de datos que envolvieran las antiguas de una manera complicada y luego convertirlas en instancias de la clase Problem (que requería más extensiones de lenguaje). muchas líneas de código para hacer algo relativamente simple. Después del refactor, solo tenía un par de funciones que hacían exactamente lo que quería.

Ahora estoy mirando el resto del código, tratando de detectar casos en los que puedo reemplazar las clases de tipos por tipos y obtener más ganancias.

Mi pregunta es: ¿en qué situación no funcionará esta refactorización? ¿En qué casos es realmente mejor usar una clase de tipo en lugar de un tipo de datos, y cómo puede reconocer esas situaciones con anticipación, para que no tenga que pasar por una costosa refactorización?


Considere una situación en la que tanto el tipo como la clase existen en el mismo programa. El tipo puede ser una instancia de la clase, pero eso es bastante trivial. Más interesante es que puede escribir una función desde fromProblemClass :: (CProblem psa) => psa -> TProblem sa .

La refactorización que realizó es más o menos equivalente a la fromProblemClass manual de fromProblemClass cualquier lugar donde construya algo utilizado como instancia de CProblem , y al hacer que cada función que acepta una instancia de TProblem acepte TProblem .

Como las únicas partes interesantes de esta refactorización son la definición de TProblem y la implementación de fromProblemClass , si puede escribir un tipo y función similar para cualquier otra clase, también puede refactorizarla para eliminar completamente la clase.

¿Cuándo funciona esto?

Piense en la implementación de fromProblemClass . Básicamente, se aplicará parcialmente cada función de la clase a un valor del tipo de instancia, y en el proceso se eliminará cualquier referencia al parámetro p (que es lo que reemplaza el tipo).

Cualquier situación en la que la refactorización de una clase de tipo sea sencilla va a seguir un patrón similar.

¿Cuándo es esto contraproducente?

Imagine una versión simplificada de Show , con solo la función show definida. Esto permite la misma refactorización, aplicando show y reemplazando cada instancia con ... una String . Claramente, hemos perdido algo aquí, es decir, la capacidad de trabajar con los tipos originales y convertirlos en una String en varios puntos. El valor de Show es que está definido en una amplia variedad de tipos no relacionados.

Como regla general, si hay muchas funciones específicas para los tipos que son instancias de la clase, y estas se usan a menudo en el mismo código que las funciones de clase, es útil posponer la conversión. Si hay una línea divisoria nítida entre el código que trata los tipos individualmente y el código que usa la clase, las funciones de conversión pueden ser más apropiadas, con una clase de tipo que es una conveniencia sintáctica menor. Si los tipos se utilizan casi exclusivamente a través de las funciones de clase, la clase de tipo probablemente sea completamente superflua.

¿Cuándo es esto imposible?

Por cierto, la refactorización aquí es similar a la diferencia entre una clase y una interfaz en lenguajes OO; de forma similar, las clases de tipos en las que esta refactorización es imposible son aquellas que no se pueden expresar directamente en muchos lenguajes OO.

Más al punto, algunos ejemplos de cosas que no se pueden traducir fácilmente, en todo caso, de esta manera:

  • El parámetro de tipo de clase que aparece solo en la posición covariante , como el tipo de resultado de una función o como un valor sin función. mempty notables aquí son mempty para mempty y return para Monad .

  • El parámetro de tipo de la clase que aparece más de una vez en el tipo de una función puede no hacer esto realmente imposible, pero complica las cosas bastante severamente. Ofensores notables aquí incluyen Eq , Ord , y básicamente cada clase numérica.

  • Uso no trivial de tipos superiores , los detalles de los cuales no estoy seguro de cómo precisar, pero (>>=) para Monad es un delincuente notable aquí. Por otro lado, el parámetro p en su clase no es un problema.

  • El uso no trivial de las clases de tipo multiparámetro , que tampoco estoy seguro de cómo precisar y se vuelve tremendamente complicado en la práctica de todos modos, es comparable a la distribución múltiple en los lenguajes OO. Nuevamente, tu clase no tiene un problema aquí.

Tenga en cuenta que, dado lo anterior, esta refactorización ni siquiera es posible para muchas de las clases de tipos estándar, y sería contraproducente para las pocas excepciones. Esto no es una coincidencia. :]

¿A qué te rindes aplicando esta refactorización?

Usted renuncia a la capacidad de distinguir entre los tipos originales. Esto parece obvio, pero es potencialmente significativo: si hay situaciones en las que realmente necesita controlar cuál de los tipos de instancias de clases originales se utilizó, la aplicación de esta refactorización pierde un cierto grado de seguridad de tipo, que solo puede recuperar saltando a través del el mismo tipo de aros utilizados en otros lugares para garantizar invariantes en tiempo de ejecución.

Por el contrario, si hay situaciones en las que realmente necesitas hacer que los distintos tipos de instancias sean intercambiables -la complicada envoltura que mencionaste es un síntoma clásico de esto- obtienes una gran cantidad al tirar los tipos originales. Este suele ser el caso en el que realmente no le importan mucho los datos originales, sino más bien cómo le permite operar con otros datos; por lo tanto, el uso de registros de funciones directamente es más natural que una capa adicional de indirección.

Como se señaló anteriormente, esto se relaciona estrechamente con OOP y el tipo de problemas para los que es más adecuado, así como representa el "otro lado" del problema de expresión de lo que es típico en los lenguajes de estilo ML.


Si eres del backeground de OOP. Puedes pensar en clases de tipos como interfaces en java. Generalmente se usan cuando se desea proporcionar la misma interfaz a diferentes tipos de datos, generalmente con implementaciones específicas del tipo de datos para cada uno.

En su caso, no tiene sentido usar una clase de letra, solo complicará su código. Para obtener más información, siempre puede consultar haskellwiki para una mejor comprensión. http://www.haskell.org/haskellwiki/OOP_vs_type_classes

La regla general es: si tiene dudas sobre si necesita clases de tipo o no, entonces probablemente no las necesite.


Su refactorización está estrechamente relacionada con esta entrada del blog de Luke Palmer: "Haskell Antipattern: Hayntial Typeclass" .

Creo que podemos probar que su refactorización siempre funcionará. ¿Por qué? Intuitivamente, porque si algún tipo Foo contiene suficiente información para que podamos convertirlo en una instancia de su clase de Problem , siempre podemos escribir una función Foo -> Problem que "proyecte" la información relevante de Foo en un Problem que contiene exactamente el información necesaria.

Un poco más formalmente, podemos esbozar una prueba de que su refactorización siempre funciona. Primero, para establecer el escenario, el siguiente código define una traducción de una instancia de clase Problem en un tipo concreto de CanonicalProblem :

{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances #-} class Problem p s a where initial :: p s a -> s successor :: p s a -> s -> [(a,s)] goaltest :: p s a -> s -> Bool data CanonicalProblem s a = CanonicalProblem { initial'' :: s, successor'' :: s -> [(a,s)], goaltest'' :: s -> Bool } instance Problem CanonicalProblem s a where initial = initial'' successor = successor'' goaltest = goaltest'' canonicalize :: Problem p s a => p s a -> CanonicalProblem s a canonicalize p = CanonicalProblem { initial'' = initial p, successor'' = successor p, goaltest'' = goaltest p }

Ahora queremos probar lo siguiente:

  1. Para cualquier tipo Foo tal como la instance Problem Foo sa , es posible escribir una canonicalizeFoo :: Foo sa -> CanonicalProblem sa que produce el mismo resultado que canonicalize cuando se aplica a cualquier Foo sa .
  2. Es posible reescribir cualquier función que use la clase Problem en una función equivalente que use CanonicalProblem lugar. Por ejemplo, si tiene solve :: Problem psa => psa -> r , puede escribir un canonicalSolve :: CanonicalProblem sa -> r que sea equivalente a solve . canonicalize solve . canonicalize

Voy a dibujar pruebas. En el caso de (1), suponga que tiene un tipo Foo con esta instancia de Problem :

instance Problem Foo s a where initial = initialFoo successor = successorFoo goaltest = goaltestFoo

Luego, dado x :: Foo sa , puedes probar trivialmente lo siguiente por sustitución:

-- definition of canonicalize canonicalize :: Problem p s a => p s a -> CanonicalProblem s a canonicalize x = CanonicalProblem { initial'' = initial x, successor'' = successor x, goaltest'' = goaltest x } -- specialize to the Problem instance for Foo s a canonicalize :: Foo s a -> CanonicalProblem s a canonicalize x = CanonicalProblem { initial'' = initialFoo x, successor'' = successorFoo x, goaltest'' = goaltestFoo x }

Y el último se puede usar directamente para definir nuestra función canonicalizeFoo deseada.

En el caso de (2), para cualquier función, solve :: Problem psa => psa -> r (o tipos similares que involucren restricciones del Problem ), y para cualquier tipo Foo tal que instance Problem Foo sa :

  • Defina canonicalSolve :: CanonicalProblem sa -> r'' tomando la definición de solve y sustituyendo todas las instancias de los métodos del Problem con sus definiciones de instancia CanonicalProblem .
  • Demuestre que para cualquier x :: Foo sa , solve x es equivalente a canonicalSolve (canonicalize x) .

Las pruebas concretas de (2) requieren definiciones concretas de solve o funciones relacionadas. Una prueba general podría ser cualquiera de estas dos formas:

  • Inducción sobre todos los tipos que tienen restricciones Problem psa .
  • Demuestre que todas las funciones del Problem se pueden escribir en términos de un pequeño subconjunto de las funciones, compruebe que este subconjunto tiene equivalentes de CanonicalProblem , y que las diversas formas de usarlas juntas conservan la equivalencia.