haskell

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:

  1. Un valor del tipo ShowList a => a podría ser:

    1. O bien una lista de cadenas envueltas en un contenedor Strings newtype.
    2. O una función de una instancia de Show a una instancia de ShowList .
  2. 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 de Strings .

  3. Eventualmente puede llamar a getStrings en un valor del tipo ShowList a => a para obtener el resultado final. Además, no necesita hacer ninguna coerción de tipo explícita usted mismo.

Ventajas:

  1. Puede agregar nuevos elementos a su lista cuando lo desee.
  2. La sintaxis es sucinta No tiene que agregar el show manual frente a cada elemento.
  3. No hace uso de ninguna extensión de idioma. Por lo tanto, también funciona en Haskell 98.
  4. Obtienes lo mejor de ambos mundos, escribe seguridad y una gran sintaxis.
  5. 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:

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é:

  1. En cada punto de tu programa donde conviertes un objeto en un Stringable y lo Stringable en un [Stringable] , convierte el objeto en un String y pega eso en un [String] .
  2. En cada punto de su programa que consume un Stringable al aplicarle a Stringable , ahora puede eliminar la llamada a toString .

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