haskell - limite - ¿Las clases de tipos son esenciales?
max hashtags instagram 2018 (6)
Una vez hice una pregunta sobre los principiantes de Haskell , ya sea para usar data / newtype o una type class. En mi caso particular resultó que no se requería una clase de tipos. Además, Tom Ellis me dio un brillante consejo sobre qué hacer en caso de duda:
La forma más sencilla de responder a esto, que en su mayoría es correcta, es:
usar datos
Sé que las clases de texto pueden hacer algunas cosas un poco más bonitas, pero no mucho AFIK. También me sorprende que las clases de tipos se utilizan principalmente para las cosas de tronco cerebral, mientras que en las cosas más nuevas, las nuevas clases de tipos casi nunca se introducen y todo se hace con datos / tipo nuevo.
Ahora me pregunto si hay casos en los que las clases de tipos son absolutamente necesarias y las cosas no se pueden expresar con datos / tipo nuevo.
Respondiendo una pregunta similar en StackOverflow Gabriel Gonzales dijo
Usa clases de tipo si:
Solo hay un comportamiento correcto por tipo dado
La clase de tipo tiene ecuaciones asociadas (es decir, "leyes") que todas las instancias deben satisfacer
Hmm ...
¿O son las clases de tipos y los conceptos de data / newtype que compiten entre sí por razones históricas?
Sé que las clases de tipos pueden hacer que algunas cosas sean un poco más bonitas, pero no mucho AFIK.
Un poco más bonita? ¡No! ¡Mucho más bonita! (como otros ya han señalado)
Sin embargo, la respuesta a esto realmente depende mucho de dónde viene esta pregunta.
- Si Haskell es su herramienta preferida para la ingeniería de software seria, las clases de tipos son poderosas y esenciales.
- Si usted es un principiante que usa Haskell para aprender programación (funcional), la complejidad y la dificultad de las clases de tipo pueden superar las ventajas, sin duda al comienzo de sus estudios.
Aquí hay un par de ejemplos que comparan ghc con gofer (predecesor de abrazos, predecesor de haskell moderno):
Gofer
? 1 ++ [2,3,4]
ERROR: Type error in application
*** expression :: 1 ++ [2,3,4]
*** term :: 1
*** type :: Int
*** does not match :: [Int]
Ahora compare con ghc:
Prelude> 1 ++ [2,3,4]
:2:1:
No instance for (Num [a0]) arising from the literal `1''
Possible fix: add an instance declaration for (Num [a0])
In the first argument of `(++)'', namely `1''
In the expression: 1 ++ [2, 3, 4]
In an equation for `it'': it = 1 ++ [2, 3, 4]
:2:7:
No instance for (Num a0) arising from the literal `2''
The type variable `a0'' is ambiguous
Possible fix: add a type signature that fixes these type variable(s)
Note: there are several potential instances:
instance Num Double -- Defined in `GHC.Float''
instance Num Float -- Defined in `GHC.Float''
instance Integral a => Num (GHC.Real.Ratio a)
-- Defined in `GHC.Real''
...plus three others
In the expression: 2
In the second argument of `(++)'', namely `[2, 3, 4]''
In the expression: 1 ++ [2, 3, 4]
Esto debería sugerir que, en lo que respecta al mensaje de error, no solo las clases de tipos no son más bonitas, ¡pueden ser más feas!
Uno puede llegar hasta el final (en gofer) y usar el ''preludio simple'' que no usa ninguna clase de tipos. Esto lo hace bastante poco realista para la programación seria, pero realmente genial para envolver a Hindley-Milner:
Preludio estándar
? :t (==)
(==) :: Eq a => a -> a -> Bool
? :t (+)
(+) :: Num a => a -> a -> a
Preludio simple
? :t (==)
(==) :: a -> a -> Bool
? :t (+)
(+) :: Int -> Int -> Int
Las clases de tipos son, en la mayoría de los casos, no esenciales. Cualquier código de clase se puede convertir mecánicamente en dictionary-passing estilo de dictionary-passing . Proporcionan principalmente conveniencia, a veces una cantidad esencial de conveniencia (véase la respuesta de kmett).
A veces, la propiedad de instancia única de typeclasses se usa para imponer invariantes. Por ejemplo, no podría convertir Data.Set
en un estilo de paso de diccionario de forma segura, porque si inserta dos veces con dos diccionarios Ord
diferentes, podría romper la estructura de datos invariante. Por supuesto, aún puede convertir cualquier código que funcione al código de trabajo en el estilo de paso de diccionario, pero no podrá prohibir tanto código roto.
Las leyes son otro aspecto cultural importante para las clases de texto. El compilador no hace cumplir las leyes, pero los programadores de Haskell esperan que las clases de tipos tengan leyes que satisfagan todas las instancias. Esto se puede leveraged para proporcionar garantías más firmes sobre algunas funciones. Esta ventaja proviene solo de las convenciones de la comunidad y no es una propiedad formal de un idioma.
Para ampliar un poco mi comentario, sugeriría comenzar siempre con el paso de datos y diccionarios. Si el pliego de condiciones y la plomería de instancia manual se vuelven demasiado pesadas, considere la posibilidad de introducir una clase de tipo. Sospecho que este enfoque generalmente conduce a un diseño más limpio.
Para responder esa parte de la pregunta:
"Typeclasses and data / newtype conceptos algo competitivos"
No. Las clases de tipos son una extensión del sistema de tipos, que le permite hacer restricciones en los argumentos polimórficos. Como la mayoría de las cosas en programación, son, por supuesto, azúcar sintáctico [por lo que no son esenciales en el sentido de que su uso no puede ser reemplazado por otra cosa]. Eso no significa que sean superfluos. Solo significa que podría expresar cosas similares utilizando otros recursos lingüísticos, pero perdería algo de claridad mientras lo hace. El paso de diccionario se puede usar para la mayoría de las mismas cosas, pero en última instancia es menos estricto en el sistema de tipos porque permite cambiar el comportamiento en el tiempo de ejecución (que también es un excelente ejemplo de dónde usaría el paso de diccionarios en lugar de clases de tipos).
Los datos y el nuevo tipo todavía significan exactamente lo mismo si tiene clases de tipos o no: introduzca un nuevo tipo, en el caso de data
como nuevo tipo de estructura de datos, y en el caso de newtype
como una variante de type
seguro.
Solo quiero hacer un punto realmente mundano sobre la sintaxis.
La gente tiende a subestimar la conveniencia que ofrecen las clases de tipo, probablemente porque nunca han probado Haskell sin usar ninguna. Este es un tipo de fenómeno de "la hierba es más verde del otro lado de la valla".
while :: Monad m -> m Bool -> m a -> m ()
while m p body = (>>=) m p $ /x ->
if x
then (>>) m body (while m p body)
else return m ()
average :: Floating a -> a -> a -> a -> a
average f a b c = (/) f ((+) (floatingToNum f) a ((+) (floatingToNum f) b c))
(fromInteger (floatingToNum f) 3)
Esta es la motivación histórica para las clases de tipos y sigue siendo válida hoy. Si no tuviéramos clases de tipos, ciertamente necesitaríamos algún tipo de reemplazo para evitar escribir monstruosidades como estas. (Tal vez algo como un juego de palabras o el "abierto" de Agda.)
Yo diría que las clases de tipos son una parte esencial de Haskell.
Son la parte de Haskell que hace que sea el lenguaje más fácil que conozco para refactorizar, y son una gran ventaja para que puedas razonar sobre la exactitud del código.
Entonces, vamos a hablar sobre el paso del diccionario.
Ahora, cualquier tipo de aprobación de diccionario es una gran mejora en el estado de las cosas en los lenguajes orientados a objetos tradicionales. Sabemos cómo hacer OOP con vtables en C ++. Sin embargo, el vtable es ''parte del objeto'' en lenguajes OOP. La fusión del vtable con el objeto fuerza a su código a tener una disciplina rígida sobre quién puede ampliar los tipos principales con nuevas funciones, es realmente el autor original de la clase que tiene que incorporar todas las cosas que otros quieren cocer. su tipo. Esto lleva al "código de flujo de lava" y todo tipo de antipatrones de diseño, etc.
Los lenguajes como C # te dan la capacidad de hackear los métodos de extensión para falsificar cosas nuevas, y los "rasgos" en idiomas como scala y herencia múltiple en otros idiomas te permiten delegar parte del trabajo también, pero son soluciones parciales.
Cuando separas el vtable de los objetos que manipulan, obtienes una gran corriente de poder. Ahora puede pasarlos a donde quiera, pero luego, por supuesto, debe nombrarlos y hablar sobre ellos. La disciplina ML en torno a los módulos / funtores y el estilo explícito de aprobación del diccionario adoptan este enfoque.
Las clases de tipos toman una dirección ligeramente diferente. Confiamos en la singularidad de una instancia de clase de tipo para un tipo determinado y es en gran parte que esta opción nos permite salirse con la suya con tipos de datos básicos tan simples.
¿Por qué?
Debido a que podemos mover el uso de los diccionarios a los sitios de uso, y no tenemos que llevarlos con los tipos de datos y podemos confiar en el hecho de que cuando lo hacemos, nada ha cambiado sobre el comportamiento del código .
La traducción mecánica del código a diccionarios más complejos pasados manualmente pierde la singularidad de dicho diccionario en un tipo dado. Pasar los diccionarios en diferentes puntos de su programa ahora conduce a programas con comportamientos muy diferentes. Puede o no tener que recordar los diccionarios con los que se construyó su tipo de datos, y ay de usted si quiere tener un comportamiento condicional según sus argumentos.
Para ejemplos simples como Set
, puedes salirte con una traducción manual del diccionario. El precio no parece tan alto. Tiene que hornear en el diccionario para, por ejemplo, cómo desea ordenar el Set
cuando crea el objeto y luego insert
/ lookup
, solo conservaría su elección. Esto podría ser un costo que puede soportar. Cuando unes dos Set
s ahora, por supuesto, está en el aire que ordenes conseguir. Tal vez tome el más pequeño e insértelo en el más grande, pero luego la ordenación cambiará de manera involuntaria, así que en su lugar, debe tomar la palabra de la izquierda e insertarla siempre en la derecha, o documentar este comportamiento caótico. Ahora estás siendo forzado a soluciones de rendimiento subóptimas en aras de la "flexibilidad".
Pero Set
es un ejemplo trivial. Allí puede hacer un índice del tipo sobre la instancia que estaba usando, solo hay una clase involucrada. ¿Qué sucede cuando quieres un comportamiento más complejo? Una de las cosas que hacemos con Haskell es trabajar con transformadores de mónada. Ahora que tiene muchas instancias flotando, y no tiene un buen lugar para almacenarlas todas, MonadReader
, MonadWriter
, MonadState
, etc. pueden aplicarse de forma condicional, en función de la mónada subyacente. ¿Qué sucede cuando usted lo levanta y lo cambia y ahora pueden aplicarse o no diferentes cosas?
Llevar un diccionario explícito para esto es mucho trabajo, no hay un buen lugar para almacenarlos y les pides a los usuarios que adopten una transformación de programa global para adoptar esta práctica.
Estas son las cosas que las clases de tipo hacen sin esfuerzo.
¿Creo que deberías usarlos para todo?
Ni por asomo.
Pero no puedo estar de acuerdo con las otras respuestas aquí de que no son esenciales para Haskell.
Haskell es el único lenguaje que los proporciona y son fundamentales para al menos mi capacidad de pensar en este idioma, y son una gran parte de por qué considero a Haskell como mi hogar.
Estoy de acuerdo con algunas cosas aquí, uso las clases de tipos cuando hay leyes y cuando la elección no es ambigua.
Sin embargo, cuestionaría que, si no tiene leyes o si la elección no es inequívoca, es posible que no sepa lo suficiente sobre cómo modelar el dominio del problema, y debería buscar algo para lo que pueda encajar en la clase de tipos. molde, posiblemente incluso en abstracciones existentes, y cuando finalmente encuentre esa solución, encontrará que puede reutilizarla fácilmente.