Haskell: Typeclass vs pasar una función

16

Para mí, parece que siempre puedes pasar argumentos de función en lugar de usar una clase de tipos. Por ejemplo, en lugar de definir la clase de tipo de igualdad:

class Eq a where 
  (==)                  :: a -> a -> Bool

Y usarlo en otras funciones para indicar el argumento de tipo debe ser una instancia de Eq:

elem                    :: (Eq a) => a -> [a] -> Bool

¿No podemos simplemente definir nuestra elemfunción sin usar una clase de tipos y en su lugar pasar un argumento de función que haga el trabajo?

mahdix
fuente
2
eso se llama pasar diccionario. Puede pensar las restricciones de la clase de tipos como argumentos implícitos.
Poscat
2
Podría hacerlo, pero obviamente es mucho más conveniente no tener que pasar una función y simplemente hacer que use una "estándar" según el tipo.
Robin Zigmond
2
Podrías decirlo así, sí. Pero diría que hay al menos otra ventaja importante: la capacidad de escribir funciones polimórficas que funcionen en cualquier tipo que implemente una "interfaz" particular o un conjunto de características. Creo que las restricciones de la clase de tipos lo expresan muy claramente de una manera en que pasar argumentos de funciones adicionales no lo hace. En particular debido a las "leyes" (tristemente implícitas) que muchas clases de tipos tienen que cumplir. Una Monad mrestricción me dice más que pasar argumentos de función adicionales de tipos a -> m ay m a -> (a -> m b) -> m b.
Robin Zigmond
1
Ver también ¿ son esenciales las clases de tipos?
luqui
1
La TypeApplicationsextensión le permite hacer explícito el argumento implícito. (==) @Int 3 5compara 3y 5específicamente como Intvalores. Puede pensar @Intcomo una clave en el diccionario de funciones de igualdad específicas de tipo, en lugar de la Intfunción de comparación específica en sí.
chepner

Respuestas:

19

Si. Esto se llama "estilo de aprobación de diccionario". A veces, cuando estoy haciendo algunas cosas especialmente difíciles, necesito desechar una clase de texto y convertirla en un diccionario, porque la aprobación del diccionario es más potente 1 , pero a menudo bastante engorrosa, lo que hace que el código conceptual simple parezca bastante complicado. A veces uso el estilo de aprobación de diccionario en idiomas que no son Haskell para simular clases de tipos (pero he aprendido que, por lo general, no es una idea tan buena como parece).

Por supuesto, cada vez que hay una diferencia en el poder expresivo, hay una compensación. Si bien puede usar una API determinada de más maneras si está escrita con DPS, la API obtiene más información si no puede. Una forma en que esto aparece en la práctica es en Data.Set, que se basa en el hecho de que solo hay un Orddiccionario por tipo. Las Settiendas de sus elementos ordenados según Ord, y si se construye un conjunto con un diccionario, y luego inserta un elemento utilizando una diferente, ya que sería posible con DPS, se podría romper Set's invariante y hacer que se caiga. Este problema de singularidad puede mitigarse utilizando un fantasma existencialescriba para marcar el diccionario, pero, una vez más, a costa de una complejidad bastante molesta en la API. Esto también se muestra más o menos de la misma manera en la TypeableAPI.

El bit de singularidad no aparece muy a menudo. En qué clases de tipos es excelente escribir código para usted. Por ejemplo,

catProcs :: (i -> Maybe String) -> (i -> Maybe String) -> (i -> Maybe String)
catProcs f g = f <> g

que toma dos "procesadores" que toman una entrada y pueden dar una salida, y los concatena, aplanándolos Nothing, tendría que escribirse en DPS algo como esto:

catProcs f g = (<>) (funcSemi (maybeSemi listSemi)) f g

Esencialmente tuvimos que deletrear el tipo en el que lo estamos usando nuevamente, a pesar de que ya lo deletreamos en la firma de tipo, e incluso eso era redundante porque el compilador ya conoce todos los tipos. Debido a que solo hay una forma de construir un determinado Semigroupen un tipo, el compilador puede hacerlo por usted. Esto tiene un efecto de tipo "interés compuesto" cuando comienzas a definir muchas instancias paramétricas y a usar la estructura de tus tipos para calcular por ti, como en los Data.Functor.*combinadores, y esto se usa para un gran efecto con el deriving viaque esencialmente puedes obtener todos los estructura algebraica "estándar" de su tipo escrita para usted.

Y ni siquiera me hagas comenzar con MPTC y fundeps, que retroalimentan la información en la verificación de tipos e inferencia. Nunca he intentado convertir tal cosa a DPS; sospecho que implicaría pasar muchas pruebas de igualdad de tipos, pero en cualquier caso, estoy seguro de que sería mucho más trabajo para mi cerebro de lo que estaría cómodo con.

-

1 U alvo que utilizar reflectionen cuyo caso se convierten en equivalente en potencia - pero reflectiontambién puede ser complicado de usar.

luqui
fuente
Estoy muy interesado en fundeps expresados ​​a través de DPS. ¿Conoces algunos recursos recomendables sobre este tema? De todos modos, explicación muy comprensible.
bob
@bob, no fuera de lugar, pero sería una exploración interesante. Tal vez hacer una nueva pregunta al respecto?
luqui
5

Si. Eso (llamado pasar de diccionario) es básicamente lo que hace el compilador para escribir clases de todos modos. Para esa función, hecha literalmente, se vería un poco así:

elemBy :: (a -> a -> Bool) -> a -> [a] -> Bool
elemBy _ _ [] = False
elemBy eq x (y:ys) = eq x y || elemBy eq x ys

Llamar elemBy (==) x xsahora es equivalente a elem x xs. Y en este caso específico, puede ir un paso más allá: eqtiene el mismo primer argumento cada vez, por lo que puede hacer que la responsabilidad de la persona que llama aplique eso y termine con esto:

elemBy2 :: (a -> Bool) -> [a] -> Bool
elemBy2 _ [] = False
elemBy2 eqx (y:ys) = eqx y || elemBy2 eqx ys

Llamar elemBy2 (x ==) xsahora es equivalente a elem x xs.

...Oh espera. Eso es justo any. (Y, de hecho, en la biblioteca estándar,.elem = any . (==) )

Joseph Sible-Reinstate a Monica
fuente
La aprobación del diccionario AFAIU es el enfoque de Scala para codificar clases de tipos. Esos argumentos adicionales se pueden declarar como implicity el compilador los inyectaría desde el alcance.
michid