haskell - hackage
Lista de lo que se puede ver: OOP beats Haskell? (8)
Quiero crear una lista de cosas diferentes que tienen una propiedad en común, es decir, podrían convertirse en cadenas. El enfoque orientado a objetos es sencillo: defina la interfaz Showable
y haga que las clases de interés la implementen. El segundo punto puede ser, en principio, un problema cuando no se pueden modificar las clases, pero pretendemos que este no es el caso. A continuación, crea una lista de Showable
sy la llena con objetos de estas clases sin ningún ruido adicional (por ejemplo, el upcasting generalmente se realiza implícitamente). Prueba de concepto en Java se da aquí .
Mi pregunta es ¿qué opciones tengo para Haskell? A continuación, analizo enfoques que probé y que realmente no me satisfacen.
Enfoque 1 : existenciales. Funciona pero feo
{-# LANGUAGE ExistentialQuantification #-}
data Showable = forall a. Show a => Sh a
aList :: [Showable]
aList = [Sh (1 :: Int), Sh "abc"]
El principal inconveniente para mí aquí es la necesidad de Sh
al completar la lista. Esto se parece mucho a las operaciones upcast que se realizan implícitamente en OO-languages.
De manera más general, el envoltorio ficticio Showable
para lo que ya está en el idioma - Show
clase de tipo - agrega ruido extra en mi código. No es bueno.
Enfoque 2 : impredicatives. Deseado pero no funciona.
El tipo más directo para esa lista para mí y lo que realmente deseo sería:
{-# LANGUAGE ImpredicativeTypes #-}
aList :: [forall a. Show a => a]
aList = [(1 :: Int), "abc"]
Además de eso ( como he oído ) ImpredicativeTypes
es "frágil en el mejor de los casos y roto en el peor", no compila:
Couldn''t match expected type ‘a’ with actual type ‘Int’
‘a’ is a rigid type variable bound by
a type expected by the context: Show a => a
y el mismo error para "abc"
. (Nota firma de tipo para 1: sin ella recibo aún más mensaje extraño: Could not deduce (Num a) arising from the literal ''1''
).
Enfoque 3 : tipos de Rank-N junto con algún tipo de lista funcional (¿listas de diferencias?).
En lugar de ImpredicativeTypes
problemáticos uno probablemente preferiría ImpredicativeTypes
más estables y ampliamente aceptados. Esto básicamente significa: mover el forall a. Show a => a
deseado forall a. Show a => a
forall a. Show a => a
constructor sin tipo (es decir []
) a tipos de funciones simples. En consecuencia, necesitamos alguna representación de listas como funciones simples. Como apenas escuché, hay tales representaciones. Lo único que escuché es listas de diferencias. Pero en el paquete Dlist
, el tipo principal es bueno, por lo que volvemos a los datos impredicativos. No investigué más esta línea ya que sospecho que podría arrojar un código más detallado que en el enfoque 1. Pero si crees que no será así, por favor dame un ejemplo.
En pocas palabras : ¿cómo atacarías esa tarea en Haskell? ¿Podría dar una solución más sucinta que en el lenguaje OO (especialmente en lugar de llenar una lista, ver el comentario del código en el enfoque 1)? ¿Puedes comentar sobre cuán relevantes son los enfoques mencionados anteriormente?
UPD (basado en los primeros comentarios): la pregunta, por supuesto, se simplifica con el fin de facilitar la lectura. El problema real es más acerca de cómo almacenar cosas que comparten la misma clase de tipo, es decir, que se pueden procesar más tarde de varias maneras ( Show
tiene solo un método, pero otras clases pueden tener más de uno). Esto tiene en cuenta las soluciones que sugieren aplicar el método de show
justo al completar una lista.
Dado que la evaluación es floja en Haskell, ¿qué tal si creamos una lista de las cadenas reales?
showables = [ show 1, show "blah", show 3.14 ]
El núcleo del problema es: desea despachar (leer, seleccionar qué función llamar) en tiempo de ejecución, dependiendo de qué es el "tipo" del objeto. En Haskell esto puede lograrse al envolver los datos en un tipo de datos de suma (que se llama aquí ShowableInterface
):
data ShowableInterface = ShowInt Int | ShowApple Apple | ShowBusiness Business
instance Show ShowableInterface where
show (ShowInt i) = show i
show (ShowApple a) = show a
show (ShowBusiness b) = show b
list=[ShowInt 2, ShowApple CrunchyGold, ShowBusiness MoulinRouge]
show list
correspondería a algo como esto en Java:
class Int implements ShowableInterface
{
public show {return Integer.asString(i)};
}
class Apple implements ShowableInterface
{
public show {return this.name};
}
class ShowBusiness implements ShowableInterface
{
public show {return this.fancyName};
}
List list = new ArrayList (new Apple("CrunchyGold"),
new ShowBusiness("MoulingRouge"), new Integer(2));
entonces en Haskell necesitas envolver explícitamente cosas en ShowableInterface
, en Java este ShowableInterface
se hace implícitamente en la creación de objetos.
el crédito es para #haskell IRC por explicarme esto hace un año, más o menos.
Haría algo como esto:
newtype Strings = Strings { getStrings :: [String] }
newtype DiffList a = DiffList { getDiffList :: [a] -> [a] }
instance Monoid (DiffList a) where
mempty = DiffList id
DiffList f `mappend` DiffList g = DiffList (f . g)
class ShowList a where
showList'' :: DiffList String -> a
instance ShowList Strings where
showList'' (DiffList xs) = Strings (xs [])
instance (Show a, ShowList b) => ShowList (a -> b) where
showList'' xs x = showList'' $ xs `mappend` DiffList (show x :)
showList = showList'' mempty
Ahora puede crear una ShowList
siguiente manera:
myShowList = showList 1 "blah" 3.14
Puede obtener una lista de cadenas utilizando getStrings
siguiente manera:
myStrings = getStrings myShowList
Esto es lo que está pasando:
Un valor del tipo
ShowList a => a
podría ser:- O bien una lista de cadenas envueltas en un contenedor
Strings
newtype. - O una función de una instancia de
Show
a una instancia deShowList
.
- O bien una lista de cadenas envueltas en un contenedor
Esto significa que la función
showList
es una función de argumento variódica que toma una cantidad arbitraria de valores imprimibles y finalmente devuelve una lista de cadenas envueltas en un contenedor de tipo nuevo deStrings
.Eventualmente puede llamar a
getStrings
en un valor del tipoShowList a => a
para obtener el resultado final. Además, no necesita hacer ninguna coerción de tipo explícita usted mismo.
Ventajas:
- Puede agregar nuevos elementos a su lista cuando lo desee.
- La sintaxis es sucinta No tiene que agregar el
show
manual frente a cada elemento. - No hace uso de ninguna extensión de idioma. Por lo tanto, también funciona en Haskell 98.
- Obtienes lo mejor de ambos mundos, escribe seguridad y una gran sintaxis.
- Usando listas de diferencias, puede construir el resultado en tiempo lineal.
Para obtener más información sobre funciones con argumentos variados, lea la respuesta a la siguiente pregunta:
¿Cómo funciona Haskell printf?
Las HList
estilo HList
funcionarían, pero es posible reducir la complejidad si solo necesita trabajar con listas de existenciales restringidos y no necesita la otra maquinaria HList
.
Así es como manejo esto en mi paquete existentialist
:
{-# LANGUAGE ConstraintKinds, ExistentialQuantification, RankNTypes #-}
data ConstrList c = forall a. c a => a :> ConstrList c
| Nil
infixr :>
constrMap :: (forall a. c a => a -> b) -> ConstrList c -> [b]
constrMap f (x :> xs) = f x : constrMap f xs
constrMap f Nil = []
Esto se puede usar así:
example :: [String]
example
= constrMap show
(( ''a''
:> True
:> ()
:> Nil) :: ConstrList Show)
Podría ser útil si tienes una lista grande o posiblemente si tienes que hacer muchas manipulaciones a una lista de existenciales restringidos.
Usando este enfoque, tampoco es necesario codificar la longitud de la lista en el tipo (o los tipos originales de los elementos). Esto podría ser algo bueno o malo dependiendo de la situación. Si desea conservar toda la información del tipo original, una HList
es probablemente el camino a seguir.
Además, si (como es el caso de Show
) solo hay un método de clase, el enfoque que recomendaría sería aplicar ese método a cada elemento de la lista directamente como en la respuesta de ErikR o la primera técnica en la respuesta de Phadej.
Parece que el problema real es más complejo que solo una lista de valores de Showable, por lo que es difícil dar una recomendación definitiva de cuál de estos específicamente es el más apropiado sin información más concreta.
Sin embargo, uno de estos métodos probablemente funcionaría bien (a menos que la arquitectura del código en sí pueda simplificarse para que no se tope con el problema en primer lugar).
Generalizando a existenciales contenidos en tipos de mayor nivel
Esto se puede generalizar a clases superiores como esta:
data AnyList c f = forall a. c a => f a :| (AnyList c f)
| Nil
infixr :|
anyMap :: (forall a. c a => f a -> b) -> AnyList c f -> [b]
anyMap g (x :| xs) = g x : anyMap g xs
anyMap g Nil = []
Al usar esto, podemos (por ejemplo) crear una lista de funciones que tienen tipos de resultados visibles.
example2 :: Int -> [String]
example2 x = anyMap (/m -> show (m x))
(( f
:| g
:| h
:| Nil) :: AnyList Show ((->) Int))
where
f :: Int -> String
f = show
g :: Int -> Bool
g = (< 3)
h :: Int -> ()
h _ = ()
Podemos ver que esta es una verdadera generalización al definir:
type ConstrList c = AnyList c Identity
(>:) :: forall c a. c a => a -> AnyList c Identity -> AnyList c Identity
x >: xs = Identity x :| xs
infixr >:
constrMap :: (forall a. c a => a -> b) -> AnyList c Identity -> [b]
constrMap f (Identity x :| xs) = f x : constrMap f xs
constrMap f Nil = []
Esto permite que el example
original de la primera parte funcione con esta nueva formulación, más general, sin cambios en el código de example
existente, excepto en el cambio :>
a >:
(incluso este pequeño cambio podría evitarse con sinónimos de patrones) No estoy del todo seguro, ya que no lo he intentado y, a veces, los sinónimos de patrones interactúan con la cuantificación existencial de formas que no entiendo del todo).
Mi respuesta es básicamente la misma que la de ErikR: el tipo que mejor cumple con tus requisitos es [String]
. Pero entraré un poco más en la lógica que creo que justifica esta respuesta. La clave está en esta cita de la pregunta:
[...] cosas que tienen una propiedad en común, es decir, podrían convertirse en cuerdas.
Llamemos a este tipo Stringable
. Pero ahora la observación clave es esta:
-
Stringable
es isomorphic paraString
!
Es decir, si su afirmación anterior es toda la especificación del tipo Stringable
, entonces hay un par que funciona con estas firmas:
toString :: Stringable -> String
toStringable :: String -> Stringable
... tal que las dos funciones son inversas. Cuando dos tipos son isomorfos, cualquier programa que use cualquiera de los tipos puede reescribirse en términos del otro sin ningún cambio en su semántica. ¡Así que Stringable
no te permite hacer nada que String
no te permita hacer!
En términos más concretos, el punto es que esta refactorización está garantizada para funcionar sin importar qué:
- En cada punto de tu programa donde conviertes un objeto en un
Stringable
y loStringable
en un[Stringable]
, convierte el objeto en unString
y pega eso en un[String]
. - En cada punto de su programa que consume un
Stringable
al aplicarle aStringable
, ahora puede eliminar la llamada atoString
.
Tenga en cuenta que este argumento se generaliza a tipos más complejos que Stringable
, con muchos "métodos". Entonces, por ejemplo, el tipo de "cosas que puedes convertir en String
o Int
" es isomorfo a (String, Int)
. El tipo de "cosas que puedes convertir en una String
o combinarlas con un Foo
para producir una Bar
" es isomorfo a (String, Foo -> Bar)
. Y así. Básicamente, esta lógica conduce a la codificación del "registro de métodos" que otras respuestas han planteado.
Creo que la lección que se puede extraer de esto es la siguiente: se necesita una especificación más completa que solo "se puede convertir en una cadena" para justificar el uso de cualquiera de los mecanismos que se mencionaron. Entonces, por ejemplo, si agregamos el requisito de que los valores de Stringable
pueden ser Stringable
al tipo original, un tipo existencial ahora quizás se vuelva justificable:
{-# LANGUAGE GADTs #-}
import Data.Typeable
data Showable = Showable
Showable :: (Show a, Typeable a) => a -> Stringable
downcast :: Typeable a => Showable -> Maybe a
downcast (Showable a) = cast a
Este tipo Showable
no es isomorfo para String
, porque la restricción Typeable
nos permite implementar la función Showable
que nos permite distinguir entre diferentes Showable
s que producen la misma cadena. Una versión más rica de esta idea se puede ver en este "ejemplo de forma" Gist.
Puede almacenar funciones parcialmente aplicadas en la lista.
Supongamos que estamos construyendo un trazador de rayos con una forma diferente que puede intersecar.
data Sphere = ...
data Triangle = ...
data Ray = ...
data IntersectionResult = ...
class Intersect t where
intersect :: t -> Ray -> Maybe IntersectionResult
instance Intersect Sphere where ...
instance Intersect Triangle where ...
Ahora, podemos aplicar parcialmente la intersect
para obtener una lista de Ray -> Maybe IntersectionResult
tal como:
myList :: [(Ray -> Maybe IntersectionResult)]
myList = [intersect sphere, intersect triangle, ...]
Ahora, si quieres obtener todas las intersecciones, puedes escribir:
map ($ ray) myList -- or map (/f -> f ray) myList
Esto se puede extender un poco para manejar una interfaz con múltiples funciones, por ejemplo, si desea obtener algo de una forma:
class ShapeWithSomething t where
getSomething :: t -> OtherParam -> Float
data ShapeIntersectAndSomething = ShapeIntersectAndSomething {
intersect :: Ray -> Maybe IntersectionResult,
getSomething :: OtherParam -> Float}
Algo que no sé es la sobrecarga de este enfoque. Necesitamos almacenar el puntero a la función y el puntero a la forma y esto para cada función de la interfaz, que es mucho en comparación con el vtable compartido generalmente utilizado en el lenguaje OO. No tengo idea si GHC puede optimizar esto.
Puede crear su propio operador para reducir el ruido de sintaxis:
infixr 5 <:
(<:) :: Show a => a -> [String] -> [String]
x <: l = show x : l
Entonces puedes hacer:
λ > (1 :: Int) <: True <: "abs" <: []
["1","True","/"abs/""]
Esto no es [1 :: Int, True, "abs"]
pero no mucho más.
Lamentablemente, no puede volver a sintaxis [...]
con RebindableSyntax
.
Otro enfoque es usar HList
y conservar toda la información de tipo, es decir, sin downcasts, sin actualizaciones:
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
import GHC.Exts (Constraint)
infixr 5 :::
type family All (c :: k -> Constraint) (xs :: [k]) :: Constraint where
All c ''[] = ()
All c (x '': xs) = (c x, All c xs)
data HList as where
HNil :: HList ''[]
(:::) :: a -> HList as -> HList (a '': as)
instance All Show as => Show (HList as) where
showsPrec d HNil = showString "HNil"
showsPrec d (x ::: xs) = showParen (d > 5) (showsPrec 5 x)
. showString " ::: "
. showParen (d > 5) (showsPrec 5 xs)
Y después de todo eso:
λ *Main > (1 :: Int) ::: True ::: "foo" ::: HNil
1 ::: True ::: "foo" ::: HNil
λ *Main > :t (1 :: Int) ::: True ::: "foo" ::: HNil
(1 :: Int) ::: True ::: "foo" ::: HNil
:: HList ''[Int, Bool, [Char]]
Hay varias formas de codificar listas heterogéneas , en HList
es una, también hay generics-sop
con NP I xs
. Depende de lo que intente lograr en un contexto más amplio, si este es el enfoque de preservar todos los tipos es lo que necesita.
Si realmente quieres, puedes usar una lista heterogénea. Este enfoque realmente no es útil para Show, porque tiene un único método y todo lo que puedes hacer es aplicarlo, pero si tu clase tiene múltiples métodos, esto podría ser útil.
{-# LANGUAGE PolyKinds, KindSignatures, GADTs, TypeFamilies
, TypeOperators, DataKinds, ConstraintKinds, RankNTypes, PatternSynonyms #-}
import Data.List (intercalate)
import GHC.Prim (Constraint)
infixr 5 :&
data HList xs where
None :: HList ''[]
(:&) :: a -> HList bs -> HList (a '': bs)
-- | Constraint All c xs holds if c holds for all x in xs
type family All (c :: k -> Constraint) xs :: Constraint where
All c ''[] = ()
All c (x '': xs) = (c x, All c xs)
-- | The list whose element types are unknown, but known to satisfy
-- a class predicate.
data CList c where CL :: All c xs => HList xs -> CList c
cons :: c a => a -> CList c -> CList c
cons a (CL xs) = CL (a :& xs)
empty :: CList c
empty = CL None
uncons :: (forall a . c a => a -> CList c -> r) -> r -> CList c -> r
uncons _ n (CL None) = n
uncons c n (CL (x :& xs)) = c x (CL xs)
foldrC :: (forall a . c a => a -> r -> r) -> r -> CList c -> r
foldrC f z = go where go = uncons (/x -> f x . go) z
showAll :: CList Show -> String
showAll l = "[" ++ intercalate "," (foldrC (/x xs -> show x : xs) [] l) ++ "]"
test = putStrLn $ showAll $ CL $
1 :&
''a'' :&
"foo" :&
[2.3, 2.5 .. 3] :&
None