41 votes

Liste des "showables" à la manière de la POO en Haskell ?

Je veux construire une liste de différentes choses qui ont une propriété en commun, à savoir qu'elles peuvent être transformées en chaîne. L'approche orientée objet est simple : définir l'interface Showable et faire en sorte que les classes d'intérêt l'implémentent. Le deuxième point peut en principe poser un problème lorsque vous ne pouvez pas modifier les classes, mais supposons que ce ne soit pas le cas. Dans ce cas, vous créez une liste de Showable et le remplir avec des objets de ces classes sans aucun bruit supplémentaire (par exemple, l'upcasting est généralement fait implicitement). Preuve de concept en Java est donné ici .

Ma question est la suivante : quelles sont les options dont je dispose pour cela en Haskell ? Je discute ci-dessous des approches que j'ai essayées et qui ne me satisfont pas vraiment.

Approche 1 : existentielles. Fonctionne mais c'est moche.

{-# LANGUAGE ExistentialQuantification #-}
data Showable = forall a. Show a => Sh a

aList :: [Showable]
aList = [Sh (1 :: Int), Sh "abc"]

Le principal inconvénient pour moi ici est la nécessité de Sh lors du remplissage de la liste. Cela ressemble beaucoup aux opérations d'upcast qui sont implicitement effectuées dans les langages OO.

Plus généralement, le wrapper factice Showable pour la chose qui est déjà dans la langue - Show classe de type - ajoute du bruit supplémentaire dans mon code. Pas bon.

Approche 2 : impredicatifs. Désiré mais ne fonctionne pas.

Le type de liste le plus simple pour moi, et ce que je désire vraiment, serait le suivant :

{-# LANGUAGE ImpredicativeTypes #-}
aList :: [forall a. Show a => a]
aList = [(1 :: Int), "abc"]

En plus de cela ( comme j'ai entendu ) ImpredicativeTypes est "fragile au mieux et cassé au pire". il ne compile pas :

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

et la même erreur pour "abc" . (Note : signature de type pour 1 : sans elle, je reçois un message encore plus bizarre : Could not deduce (Num a) arising from the literal ‘1’ ).

Approche 3 : Les types Rank-N avec une sorte de listes fonctionnelles (listes de différences ?).

Au lieu de problèmes ImpredicativeTypes on préfèrerait probablement un système plus stable et plus largement accepté. RankNTypes . Cela signifie essentiellement : déplacer le site souhaité forall a. Show a => a constructeur hors type (c'est-à-dire [] ) aux types de fonctions simples. Par conséquent, nous avons besoin d'une représentation des listes en tant que fonctions ordinaires. Comme je l'ai à peine entendu, il existe de telles représentations. La seule dont j'ai entendu parler est celle des listes de différences. Mais en Dlist paquet le type principal est le bon vieux data donc nous revenons aux imprédicatifs. Je n'ai pas approfondi cette ligne car je soupçonne qu'elle pourrait produire un code plus verbeux que dans l'approche 1. Mais si vous pensez que ce n'est pas le cas, veuillez me donner un exemple.

Ligne de fond Comment attaquer une telle tâche en Haskell ? Pourriez-vous donner une solution plus succincte qu'en langage OO (notamment au lieu de remplir une liste - voir le commentaire pour le code dans l'approche 1) ? Pouvez-vous commenter la pertinence des approches listées ci-dessus ?

UPD (sur la base des premiers commentaires) : la question est bien sûr simplifiée pour des raisons de lisibilité. Le vrai problème est plutôt de savoir comment stocker des choses qui partagent la même classe de type, c'est-à-dire qui peuvent être traitées ultérieurement de plusieurs façons ( Show n'a qu'une seule méthode, mais les autres classes peuvent en avoir plusieurs). Cela écarte les solutions qui suggèrent d'appliquer show au moment de remplir une liste.

20 votes

Il y a une différence essentielle entre Java et Haskell qui vous échappe. En Java, l'information sur le type est pas perdu lors de l'upcasting, ce qui vous permet de downcasting à nouveau et de faire d'autres choses non utiles. String y plus tard (à condition de choisir le bon type pour le downcast). En Haskell, une fois que vous avez fait un upcast, l'information sur le type est perdu et vous ne pouvez plus vous baisser. Donc vous pourriez tout aussi bien avoir une liste de String s. Voir aussi Dynamic qui offre une interface plus riche que "simplement". String ".

0 votes

@DanielWagner pas vraiment sûr que le downcasting soit lié au sujet. Je n'ai besoin que d'upcasting. Oui, je peux transformer cela en une liste de String comme mentionné dans la réponse de @ErikR, mais la question porte davantage sur la lisibilité : répétitif. show n'a pas l'air bien pour moi.

1 votes

Il n'est pas toujours facile de dire si le downcasting est lié ou non au sujet traité -- les gens l'incluent souvent dans leurs hypothèses au point d'oublier de le mentionner. (Ce n'est pas la première fois que ce sujet est abordé, ni ici ni sur IRC ;-) Quant à "ça ne me semble pas bon", eh bien, il n'y a pas de comptabilité pour le goût. Si vous pouvez donner une plainte objective et technique, nous pourrons peut-être discuter de la mesure dans laquelle elle peut être traitée.

38voto

user5402 Points 8479

Puisque l'évaluation est paresseuse en Haskell, pourquoi ne pas simplement créer une liste des chaînes de caractères réelles ?

showables = [ show 1, show "blah", show 3.14 ]

0 votes

C'est une option, merci ! Pourtant, cela ressemble au problème mentionné dans la première approche : une répétition bizarre qui ressemble à un (up)casting. Les exemples dans mon post sont extrêmement simplifiés : dans une application réelle, j'obtiens des choses montrables à partir de différents algorithmes et je préfère ne pas les mélanger avec le show.

1 votes

Vous devrez les rassembler dans une liste de toute façon et ensuite vous pourrez map show .

12 votes

@arrowd Ce n'est pas bien typé : vous ne pouvez pas les rassembler dans une liste avant de les appliquer. show C'est essentiellement ce dont se plaint le corps de la question telle que posée.

18voto

David Young Points 4640

Le site HList -Mais il est possible de réduire la complexité si l'on n'a besoin de travailler qu'avec des listes d'existentiels contraints et que l'on n'a pas besoin de l'autre type de solution. HList machines.

Voici comment je gère ça dans mon existentialist paquet :

{-# 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       = []

On peut alors l'utiliser comme suit :

example :: [String]
example
  = constrMap show
              (( 'a'
              :> True
              :> ()
              :> Nil) :: ConstrList Show)

Cela pourrait être utile si vous avez une grande liste ou éventuellement si vous devez faire beaucoup de manipulations sur une liste d'existentiels contraints.

En utilisant cette approche, vous n'avez pas non plus besoin d'encoder la longueur de la liste dans le type (ou les types originaux des éléments). Cela peut être une bonne ou une mauvaise chose en fonction de la situation. Si vous voulez préserver toutes les informations sur le type original, un fichier de type HList est probablement la meilleure solution.

En outre, si (comme c'est le cas de Show ) il n'y a qu'une seule méthode de classe, l'approche que je recommanderais serait d'appliquer cette méthode à chaque élément de la liste directement comme dans la réponse d'ErikR ou la première technique dans la réponse de phadej.

On dirait que le problème réel est plus complexe qu'une simple liste de Show -Il est donc difficile de recommander avec certitude laquelle de ces valeurs est la plus appropriée sans informations plus concrètes.

L'une de ces méthodes serait probablement la bonne (à moins que l'architecture du code lui-même ne puisse être simplifiée de manière à éviter le problème dès le départ).

Généralisation aux existentiels contenus dans les types supérieurs

Cela peut être généralisé à des types supérieurs comme celui-ci :

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       = []

En utilisant ceci, nous pouvons (par exemple) créer une liste de fonctions qui ont Show -les types de résultats possibles.

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 _ = ()

Nous pouvons voir que c'est une vraie généralisation en définissant :

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                = []

Cela permet à l'original example de la première partie de ce document pour fonctionner en utilisant cette nouvelle formulation plus générale, sans modification de l'approche existante. example sauf à changer :> à >: (même ce petit changement pourrait être évité avec des synonymes de motifs. Je n'en suis pas totalement sûr car je n'ai pas essayé et parfois les synonymes de modèle interagissent avec la quantification existentielle d'une manière que je ne comprends pas complètement).

1 votes

Joli ! J'aime beaucoup ta réponse, mais pourrais-tu m'expliquer comment généraliser cette solution à une liste de fonctions comme [Int -> Showable] ?

1 votes

@ArtemPelenitsyn J'ai ajouté un moyen de le faire dans ma réponse.

1 votes

@ArtemPelenitsyn Je dois dire que je suis un peu curieux de savoir quelle est votre application particulière de ceci. Même si j'ai passé un certain temps à faire fonctionner ce genre de choses dans le passé, je ne l'ai jamais vraiment utilisé pour quoi que ce soit et je me suis toujours demandé s'il y avait des applications pratiques pour cela.

11voto

user2407038 Points 4636

Si vous le voulez vraiment, vous pouvez utiliser une liste hétérogène. Cette approche n'est pas vraiment utile pour Show, car il n'a qu'une seule méthode et tout ce que vous pouvez faire est de l'appliquer, mais si votre classe a plusieurs méthodes, cela pourrait être utile.

{-# 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

9voto

Oleg Grenrus Points 1193

Vous pouvez créer votre propre opérateur pour réduire le bruit de la syntaxe :

infixr 5 <:

(<:) :: Show a => a -> [String] -> [String]
x <: l = show x : l

Donc vous pouvez le faire :

 > (1 :: Int) <: True <: "abs" <: []
["1","True","\"abs\""]

Ce n'est pas [1 :: Int, True, "abs"] mais pas beaucoup plus longtemps.

Malheureusement, vous ne pouvez pas relier [...] syntaxe avec RebindableSyntax .


Une autre approche consiste à utiliser HList et préserver tous les informations de type, c'est-à-dire pas de downcasts, pas de upcasts :

{-# 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)

Et après tout ça :

 *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]]

Il existe plusieurs façons de coder liste hétérogène en HList est unique, il y a aussi generics-sop avec NP I xs . Cela dépend de ce que vous essayez d'obtenir dans un contexte plus large, si cette approche consistant à préserver tous les types est ce dont vous avez besoin.

8voto

Aadit M Shah Points 17951

Je ferais quelque chose comme ça :

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

Maintenant, vous pouvez créer un ShowList comme suit :

myShowList = showList 1 "blah" 3.14

Vous pouvez récupérer une liste de chaînes de caractères en utilisant getStrings comme suit :

myStrings = getStrings myShowList

Voici ce qui se passe :

  1. Une valeur de type ShowList a => a pourrait être :

    1. Soit une liste de chaînes de caractères enveloppées dans un fichier de type Strings enveloppe de nouveau type.
    2. Ou une fonction d'une instance de Show à une instance de ShowList .
  2. Cela signifie que la fonction showList est une fonction à argument variadique qui prend un nombre arbitraire de valeurs imprimables et renvoie éventuellement une liste de chaînes de caractères enveloppées dans un fichier Strings enveloppe de nouveau type.

  3. Vous pouvez éventuellement appeler getStrings sur une valeur de type ShowList a => a pour obtenir le résultat final. En outre, vous n'avez pas besoin d'effectuer vous-même une coercition de type explicite.

Avantages :

  1. Vous pouvez ajouter de nouveaux éléments à votre liste quand vous le souhaitez.
  2. La syntaxe est succincte. Vous n'avez pas besoin d'ajouter manuellement show devant chaque élément.
  3. Il ne fait appel à aucune extension de langage. Par conséquent, il fonctionne également en Haskell 98.
  4. Vous obtenez le meilleur des deux mondes, la sécurité du type et une excellente syntaxe.
  5. En utilisant des listes de différences, vous pouvez construire le résultat en temps linéaire.

Pour plus d'informations sur les fonctions avec des arguments variadiques, lisez la réponse à la question suivante :

Comment fonctionne printf en Haskell ?

0 votes

Serait-il possible de réarranger les choses pour éviter que le temps d'exécution reverse ? Peut-être une sorte d'astuce d'indexation ?

1 votes

@dfeuer Vous pouvez utiliser une liste de différences au lieu d'une chaîne de caractères. Ainsi, showList' :: ShowList a => DiffList String -> a . Je vais mettre à jour ma réponse.

4 votes

@dfeuer Awww. Merci pour la modification. Maintenant mon code a un smiley :)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X