opciones - Haskell: ¿Por qué usar Proxy?
imprimir en haskell (2)
Dos ejemplos, uno donde Proxy
es necesario y otro donde Proxy
no cambia fundamentalmente los tipos, pero tiendo a usarlo de todos modos.
Proxy
necesario
Proxy
o algún truco equivalente es necesario cuando hay algún tipo intermedio, no expuesto en la firma de tipo normal, que desea que el consumidor pueda especificar. Quizás el tipo intermedio cambie la semántica, como read . show :: String -> String
read . show :: String -> String
. Con ScopedTypeVariables
habilitado, escribiría
f :: forall proxy a. (Read a, Show a) => proxy a -> String -> String
f _ = (show :: a -> String) . read
> f (Proxy :: Proxy Int) "3"
"3"
> f (Proxy :: Proxy Bool) "3"
"*** Exception: Prelude.read: no parse
El parámetro proxy me permite exponer a
como un parámetro de tipo. show . read
show . read
es una especie de ejemplo estúpido. Una mejor situación puede ser cuando algún algoritmo utiliza una colección genérica internamente, donde el tipo de colección seleccionado tiene algunas características de rendimiento que usted desea que el consumidor pueda controlar sin requerir (o permitir) que proporcione o reciba el valor intermedio.
Algo como esto, usando tipos de fgl
, donde no queremos exponer el tipo de Data
interno. (¿Quizás alguien puede sugerir un algoritmo apropiado para este ejemplo?)
f :: Input -> Output
f = g . h
where
h :: Gr graph Data => Input -> graph Data
g :: Gr graph Data => graph Data -> Output
La exposición de un argumento proxy le permitiría al usuario seleccionar entre un árbol Patricia o una implementación de árbol normal.
Proxy
como API o conveniencia de implementación
A veces uso Proxy
como una herramienta para elegir una instancia de clase de tipo, especialmente en instancias de clases recursivas o inductivas. Considere la clase MightBeA
que escribí en esta respuesta sobre el uso de Anidados s :
class MightBeA t a where
isA :: proxy t -> a -> Maybe t
fromA :: t -> a
instance MightBeA t t where
isA _ = Just
fromA = id
instance MightBeA t (Either t b) where
isA _ (Left i) = Just i
isA _ _ = Nothing
fromA = Left
instance MightBeA t b => MightBeA t (Either a b) where
isA p (Right xs) = isA p xs
isA _ _ = Nothing
fromA = Right . fromA
La idea es extraer un Maybe Int
de, digamos, Either String (Either Bool Int)
. El tipo de isA
es básicamente a -> Maybe t
. Hay dos razones para usar un proxy aquí:
Primero, elimina las firmas de tipo para el consumidor. Puede llamar a isA
como isA (Proxy :: Proxy Int)
lugar de isA :: MightBeA Int a => a -> Maybe Int
.
En segundo lugar, es más fácil para mí pensar en el caso inductivo simplemente pasando el proxy. Con ScopedTypeVariables
, la clase se puede reescribir sin un argumento proxy; el caso inductivo se implementaría como
instance MightBeA'' t b => MightBeA'' t (Either a b) where
-- no proxy argument
isA'' (Right xs) = (isA'' :: b -> Maybe t) xs
isA'' _ = Nothing
fromA'' = Right . fromA''
Esto no es realmente un gran cambio en este caso; si la firma de tipo de isA
fuera considerablemente más compleja, usar el proxy sería una gran mejora.
Cuando el uso es exclusivamente para facilitar la implementación, normalmente exportaría una función de contenedor para que el usuario no necesite proporcionar el proxy.
Proxy
vs. Tagged
En todos mis ejemplos, el parámetro tipo a
no agrega nada útil al tipo de salida en sí. (En los primeros dos ejemplos, no está relacionado con el tipo de salida; en el último ejemplo, es redundante del tipo de salida). Si devolviera un Tagged ax
, el consumidor invariablemente lo eliminaría inmediatamente. Además, el usuario tendrá que escribir el tipo de x
en su totalidad, lo que a veces es muy inconveniente porque es un tipo intermedio complicado. (Tal vez algún día podamos usar _
en firmas de tipo ... )
(Estoy interesado en escuchar otras respuestas sobre esta sub-pregunta, literalmente nunca he escrito nada usando Tagged
(sin reescribirlo en poco tiempo usando Proxy
) y me pregunto si me falta algo).
En Haskell, un Proxy es un tipo de valor testigo que hace que sea fácil pasar algunos tipos alrededor
data Proxy a = Proxy
Un ejemplo de uso está aquí en json-schema :
class JSONSchema a where
schema :: Proxy a -> Schema
por lo que podría hacer un schema (Proxy :: Proxy (Int,Char))
para obtener lo que sería la representación JSON para un Int-Char-Tuple (probablemente una matriz).
¿Por qué la gente usa proxies? Me parece que lo mismo podría lograrse
class JSONSchema a where
schema :: Schema a
similar a cómo funciona la clase de tipo Bounded
. Primero pensé que podría ser más fácil obtener el esquema de algún valor dado al usar proxies, pero eso no parece ser cierto:
{-# LANGUAGE ScopedTypeVariables #-}
schemaOf :: JSONSchema a => a -> Schema a
schemaOf (v :: x) = schema (Proxy :: Proxy x) -- With proxy
schemaOf (v :: x) = schema :: Schema x -- With `:: a`
schemaOf _ = schema -- Even simpler with `:: a`
Además, uno podría preocuparse si los valores Proxy
realmente se erradican en tiempo de ejecución, que es un problema de optimización que no existe cuando se utiliza el enfoque :: a
.
Si el enfoque :: a
adoptado por Bounded
logra el mismo resultado con un código más corto y menos preocupaciones sobre la optimización, ¿por qué las personas usan proxies? ¿Cuáles son los beneficios de los proxies?
EDITAR: Algunas respuestas y comentaristas señalaron con acierto que el enfoque :: a
contamina los data Schema = ...
escribe con un parámetro de tipo "inútil", al menos desde la perspectiva de la propia estructura de datos, que no usa nunca el a
( ver aquí ).
La sugerencia es usar el tipo fantasma Tagged sb
, que permite separar las dos preocupaciones ( Tagged a Schema
combina el tipo de esquema no paramétrico con una variable de tipo a
), que es estrictamente mejor que el :: a
approach.
Entonces mi pregunta debería ser mejor ¿Cuáles son los beneficios de los proxies versus el enfoque etiquetado?
En última instancia, realizarán la misma funcionalidad y los verá en cualquier estilo. A veces es apropiado etiquetar sus valores fantasma, a veces le gustaría considerarlos como sin tipo.
La otra alternativa es usar Data.Tagged
.
class JSONSchema a where
schema :: Tagged a Schema
Aquí tenemos algo de lo mejor de ambos mundos ya que un Schema
Tagged
tiene información de tipo fantasma necesaria para resolver la instancia, pero podemos pasar por alto trivialmente esa información usando unTagged :: Tagged sb -> b
.
Diría que la pregunta de conducción, redactada en términos de este ejemplo, debería ser "¿Deseo considerar operaciones mecanografiadas en Schema
s?". Si la respuesta es "no", entonces estará predispuesto hacia los enfoques de Proxy
o Tagged
. Si la respuesta es "sí", entonces Schema a
es una gran solución.
Como nota final, puede usar el enfoque de Proxy
(algo hackily) sin ninguna importación. Ves esto a veces en el estilo
class JSONSchema a where
schema :: proxy a -> Schema
Ahora que Proxy
ha convertido en una variable tipo sugestiva, solo podemos hacer algo como lo siguiente
foo :: Schema
foo = schema ([] :: [X])
y nunca tiene que importar Proxy
en absoluto. Personalmente creo que este es un trabajo de hack completo, que probablemente terminará por confundir a los lectores.