type - ¿Por qué Haskell no tiene las clases de tipos "obvias"?
deriving show haskell (7)
Como otras respuestas han señalado, Haskell tiende a usar un vocabulario diferente. Sin embargo, no creo que hayan explicado muy bien la razón de la diferencia.
En un lenguaje como Java, las funciones no son "ciudadanos de primera clase"; Es cierto que las funciones anónimas están disponibles en las últimas versiones, pero este estilo de interfaz (Colección, Indexable, Interable, etc.) se diseñó antes de eso.
Esto hace que sea tedioso pasar nuestro código, por lo que preferimos que los datos de otras personas pasen a nuestro código . Por ejemplo:
- Los datos que implementan
Iterable
de Java nos permiten escribirfor (Foo x : anIterable) { ... }
- Los datos que implementan el
ArrayAccess
de PHP nos permiten escribiranArrayAccess[anIndex]
Este estilo también se puede ver en los lenguajes OO que implementan generadores, ya que esa es otra forma de escribir for yieldedElement in aGenerator: ...
Haskell adopta un enfoque diferente con sus clases de tipos: preferimos que nuestro código pase a los datos de otras personas . Algunos ejemplos (simplificados):
-
Functor
''s acepta nuestro código y lo aplica a cualquier elemento que ''contenga'' -
Monad
aceptan nuestro código y lo aplican en algún tipo de ''secuencia'' -
Foldable
aceptan nuestro código y lo utilizan para ''reducir'' sus contenidos
Java solo necesita Iterable
ya que debemos llamar a nuestro código en nuestro bucle for
para asegurarnos de que se llame correctamente. Haskell requiere clases de tipos más específicas ya que el código de otra persona llamará al nuestro, por lo que debemos especificar cómo se debe invocar; ¿es un map
, un fold
, un unfold
, etc.?
Afortunadamente, el sistema de tipos nos ayuda a elegir el método correcto;)
Considere los lenguajes orientados a objetos:
La mayoría de las personas que provienen de un entorno de programación orientado a objetos están familiarizados con las interfaces comunes e intuitivas en varios idiomas que capturan la esencia de las interfaces de Collection
y List
de Java. Collection
refiere a una colección de objetos que no necesariamente tiene un ordenamiento / indexación natural. Una List
es una colección que tiene un ordenamiento / indexación natural. Estas interfaces abstraen muchas estructuras de datos de la biblioteca en Java, al igual que sus interfaces equivalentes en otros lenguajes, y se requiere una comprensión íntima de estas interfaces para funcionar de manera efectiva con la mayoría de las estructuras de datos de la biblioteca.
Transición a Haskell:
Haskell tiene un sistema de clase de tipo que actúa de forma análoga a las interfaces en los objetos. Haskell parece tener una jerarquía de clases de tipos bien diseñada con respecto a Functors, Applicative, Monads, etc. cuando se trata de la funcionalidad de tipo type. Obviamente, quieren clases de tipos correctas y abstractas . Sin embargo, cuando se observan muchos contenedores de Haskell ( List
, Map
, Sequence
, Set
, Vector
) casi todos tienen funciones muy similares (o idénticas), pero no se abstraen a través de clases de tipos.
Algunos ejemplos:
-
null
para probar "vacío" -
length
/size
para el conteo de elementos -
elem
/member
para la inclusión del conjunto -
empty
y / osingleton
para la construcción predeterminada -
union
para establecer la unión -
(//)
/diff
para establecer la diferencia -
(!)
/(!!)
para indexación insegura (función parcial) -
(!?)
/lookup
de indexación segura (función total)
Si quiero usar cualquiera de las funciones anteriores, pero he importado dos o más contenedores, debo comenzar a ocultar las funciones de los módulos importados, o importar explícitamente solo las funciones necesarias de los módulos o calificar los módulos importados. Pero dado que todas las funciones proporcionan la misma funcionalidad lógica, simplemente parece una molestia. Si las funciones se definieron a partir de clases de tipos, y no por separado en cada módulo, la mecánica de inferencia de tipos del compilador podría resolver esto. También haría que cambiar los contenedores subyacentes fuera simple siempre y cuando compartieran las clases de tipos (es decir, solo usemos una Sequence
lugar de una List
para una mejor eficiencia de acceso aleatorio).
¿Por qué Haskell no tiene una Collection
y / o tipos de clase Indexable
para unificar y generalizar algunas de estas funciones?
Dichas clases de tipos existen en Haskell estándar, pero no tienen el mismo nombre que sus equivalentes OO equivalentes. La clase de clase Collection
, por ejemplo, se llama Foldable
en Haskell. Puede usarlo para probar si una estructura está vacía ( foldr (const False) True x
) o para contar el número de elementos ( foldMap (const 1) x
), o para probar la membresía establecida ( foldr (/e'' present -> (e==e'') || present) False x
para algunos e
).
Para operaciones como la búsqueda de elementos, tiene la clase de tipo Array
que podría funcionar para datos secuenciales. Para mayor flexibilidad, puede escribir su propia clase Indexable
, como por ejemplo (cuidado con las lentes):
class Indexable m k a where
at :: k -> Lens'' m (Maybe a)
El elemento nulo y la unión de conjunto pertenecen a la clase de tipo Monoid (donde mappend == union
). En esta luz, la diferencia establecida también podría implementarse en su propia clase de tipos Differentiable
(que estoy seguro ya existe en docenas de bibliotecas Haskell) y tendríamos total compatibilidad con los idiomas imperativos.
Haskell, en virtud de haber sido diseñado por matemáticos y similares, no emplea el mismo vocabulario que la mayoría de los otros idiomas, pero puede estar seguro, eso no significa que no sea un lenguaje práctico además de ser uno impresionante :-)
El paquete de lens
proporciona algo de esto.
Prueba de vacío, creación de contenedores vacíos. Ambos son proporcionados por la
AsEmpty
AsEmpty deControl.Lens.Empty
.Accediendo a los elementos por clave / índice . Las
Ixed
tiposAt
y Ixed deControl.Lens.At
.Comprobando la membresía en contenedores tipo set . La clase de tipos
Contains
deControl.Lens.At
.Anexar y eliminar elementos en contenedores similares a secuencias . Las
Snoc
tiposCons
y Snoc deControl.Lens.Cons
.
Además, el método pure
de la clase de tipo Aplicable a menudo se puede usar para crear contenedores "únicos". Para las cosas que no son functors / applicatives en Haskell, como Set
, quizás se podría usar el point
de Data.Pointed
.
En parte, la razón es que las mónadas y las flechas son características nuevas e innovadoras de Haskell, mientras que las colecciones son relativamente más mundanas. Haskell tiene una larga historia como lenguaje de investigación; preguntas de investigación interesantes (diseño de instancias de mónada y definición de operaciones genéricas para mónadas) obtienen más esfuerzo de desarrollo que el pulido de "resistencia industrial" (definición de API de contenedor).
En parte, la razón es que esos tipos provienen de tres paquetes diferentes (base, contenedores y vector), con tres historias y diseñadores separados. Eso hace que sea más difícil para sus diseñadores coordinar en proporcionar instancias de cualquier clase de tipo único.
En parte, la razón es que definir una clase de tipo único para cubrir los cinco contenedores que mencionaste es realmente difícil. List, Sequence y Vector son relativamente similares, pero Map y Set tienen restricciones completamente diferentes. Para List, Sequence y Vector, desea una clase de constructor simple, pero para Set que no funcionará, ya que Set requiere una instancia de Ord en el tipo de elemento. Lo que es peor, Map puede soportar la mayoría de sus métodos, pero su función singleton necesita dos parámetros donde el resto solo necesita uno.
Haskell tiene algunas clases de tipos para trabajar con colecciones en el paquete base: Functor , Foldable y Traversable puede ser útil para trabajar con colecciones, y las clases de tipos Monoid, Applicative y / o Alternative pueden ser útiles para construir colecciones.
Juntas, estas clases cubren la mayoría de las operaciones mencionadas en la pregunta, pero tal vez sean menos eficientes que una función más específica del contenedor (aunque muchos de estos son métodos de clase, cuyas definiciones predeterminadas pueden anularse si es necesario).
null
para probar "vacío"
Los soportes plegables son null
desde la base 4.8 ( any (const True)
es una alternativa para versiones anteriores).
longitud / tamaño para el conteo de elementos:
La length
soportes plegables desde la base 4.8 ( getSum . foldMap (const 1)
es una alternativa para versiones anteriores).
Elem / miembro para la inclusión del conjunto
Plegable soporta elem
, notElem
y member
.
vacío y / o simple para la construcción predeterminada
Para vacío, hay mempty
de mempty
y está empty
de Alternative. Para singleton, no es pure
de Applicative.
unión para establecer la unión
Hay un mappend
de mappend
y <|>
de Alternative. No implementan necesariamente la unión establecida, pero implementan alguna forma de unión que funciona bien junto con la vacía y generalmente también con singleton y find.
(/) / diff para establecer la diferencia
Este no es compatible, lamentablemente.
(!) / (!!) para indexación insegura (función parcial)
Podría usar fromJust
junto con una función para indexación segura.
(!?) / búsqueda de indexación segura (función total)
Hay find
de Pledable.
Leyes. Una buena clase de tipos tiene leyes. Una gran clase de tipos tiene suficiente parametridad para que sus leyes sean "teoremas gratis". Una clase de tipos sin leyes es solo una sobrecarga ad hoc de nombres.
Además, consulte classy-prelude y Collection .
Tienes clases de tipos para diferentes aspectos de la colección:
composición: Monoid (module Data.Monoid)
control secuencial: aplicable, Monad (módulos Control.Applicative, Control.Monad)
Composición secuencial: alternativa, MonadPlus (módulos Control.Applicative, Control.Monad)
mapeo y reducción no secuencial: Functor (mod.Dato.Functor), Plegable (mod.Data.Foldable)
mapeo y reducción secuencial: Traversable (módulo Data.Traversable)
serialización: Binario (mod. Data.Binary)
comparación: Eq, Ord (mod.Data.Eq, Data.Ord)
textualización: Mostrar, Leer
evaluación profunda (a la forma normal): NFData (mod.Control.DeepSeq)
transitabilidad genérica del tipo de datos: Datos (mod.Data.Data)
Excepto que las colecciones monomórficas (ByteString, IntSet, Text) no pueden implementar Functor y Plegable (requieren tipo arity == 1 (Tipo: * -> *))
Además, ninguno (Set a) implementa Functor .
El paquete mono-traversable redefine algunas clases sin la exclusión de tipos monomórficos.
Actualización . Se intenta poner la mayoría de las funciones en clases de tipos con los paquetes mono-traversable y classy-prelude .