3
votes

Accumuler le nombre de valeurs dans une liste de tuples dans Haskell

J'essaie d'analyser une liste en utilisant une chaîne de modèle qui indique les types de valeurs (annuel et trimestriel). J'ai besoin d'accumuler les numéros de trimestre dans la sortie résultante. Jusqu'à présent, j'ai trouvé ceci:

row = [100, 10, 40, 25, 25]
fmt = "aqqqq"
expected = [('a',1,100),('q',1,10),('q',2,40),('q',3,25),('q',4,25)]

count :: Char -> String -> Int
count letter str = length $ filter (== letter) str

split :: String -> [a] -> [(Char, Int, a)]
split fmt row = [(freq, count freq (fmt' i), x)   
               | (freq, x, i) <- zip3 fmt row [0..]]
               where fmt' i = take (i+1) fmt

-- split "aqqqq" [100, 10, 40, 25, 25]
-- [('a',1,100),('q',1,10),('q',2,40),('q',3,25),('q',4,25)]

J'imagine qu'il devrait y avoir quelque chose de plus lisible et plus performant que ce code, ou même une super ligne unique.

J'ai aussi expérimenté en développant "aqqqq" en liste de tuples [('a', 1), ('q', 1), ('q', 2), ('q', 3 ), ('q', 4)] et plus tard en ajoutant des valeurs; c'est peut-être une meilleure façon car j'aurais besoin de spécifier le format une fois pour plusieurs lignes.


0 commentaires

3 Réponses :


6
votes

Si vous avez déjà une fonction expand pour développer "aqqqq" en liste de tuples, vous pouvez accomplir le reste avec zipWith : < pre> XXX

La fonction expand produit des tuples de type Num t => (Char, t) . J'ai appelé les valeurs à l'intérieur de ce tuple p (pour période ) et ix (pour index ). La compression de cette liste de tuples avec row produit également des valeurs, que je, dans l'expression lambda, appelle simplement x.


1 commentaires

Merci pour la suggestion de zipWith et l'explication approfondie! Je vais rassembler expand et zipWith dans une publication distincte pour référence.



1
votes

Basé sur la suggestion de @Mark Seemann, voici une liste complète avec une solution. J'ai changé lambda en fonction nommée pour un peu plus de lisibilité et j'ai introduit un type pour le format de ligne.

[('a',1,100),('q',1,10),('q',2,40),('q',3,25),('q',4,25)] 

Le résultat est comme prévu:

makeSplitter fmt = \values -> zipWith merge pos values
      where 
        merge (freq, period) value = (freq, period, value)
        pos = expand fmt 
splitRow = makeSplitter "aqqqq" 
a = splitRow [100, 10, 40, 25, 25]

Une réflexion après coup - J'étend toujours la chaîne de format chaque fois que j'analyse la ligne, probablement même le curry parse = split '"aqqqq" retardera simplement le calcul. Voici mon essai pour créer une fonction de lecture dédiée:

*Main> split' "aqqqq" [100, 10, 40, 25, 25]
[('a',1,100),('q',1,10),('q',2,40),('q',3,25),('q',4,25)]

un est le résultat attendu, comme ci-dessus

count :: Char -> String -> Int
count letter str = length $ filter (== letter) str

type RowFormat = [Char]
expand :: RowFormat -> [(Char, Int)]
expand pat = [(c, count c (take (i+1) pat)) | (c, i) <- zip pat [0..]]

split' :: RowFormat -> [a] -> [(Char, Int, a)]
split' fmt values = zipWith merge (expand fmt) values
      where merge (freq, period) value = (freq, period, value) 


2 commentaires

Comme vous le dites, une application partielle de votre solution originale ne rendrait pas les choses plus rapides. Votre dernière définition de makeSplitter est bonne. Si vous le souhaitez, vous pouvez rendre implicites les valeurs lamdba ici: makeSplitter fmt = zipWith merge pos where ... . Je pense que ce qui se passe est également plus clair si vous vous débarrassez simplement de la liaison pos : makeSplitter fmt = zipWith merge (expand fmt) where ...


Merci, @amalloy. Je pense que les liaisons excessives que j'ai utilisées sont dues à un contexte de programmation impératif, je les ai supprimées du code.



2
votes

Le problème principal ici est de savoir comment convertir la chaîne, disons "aqqqq" en la liste de fréquence des caractères apparaissant dans la chaîne. c'est-à-dire que nous voulons:

[('a',1,100),('q',1,10),('q',2,40),('q',3,25),('q',4,25),('a',2,100),('q',5,10),
('q',6,40)]    

Une fois la liste de fréquences construite, nous pouvons utiliser zip3 pour produire la liste attendue des tuples comme:

take 8 $ split (cycle "aqqqq") (cycle [100, 10, 40, 25, 25])

De toute évidence, nous ne pouvons pas utiliser map pour produire la liste de fréquences souhaitée car la valeur doit être accumulée. Pour le résoudre, je recommande d'utiliser Data.Map afin d'améliorer la complexité de calcul de O (n) à O (log n) .

Il est simple de compter la fréquence en utilisant insertWith comme:

import Data.Map as Map (empty, lookup, insertWith)
import Data.Maybe (fromMaybe)

countFreq  c m = insertWith (+) c 1 m
accumValue c m = fromMaybe 0 (Map.lookup c m) + 1

split::String -> [a] -> [(Char, Int, a)]
split fmt row = zip3 fmt (mkAccumList fmt Map.empty) row
    where mkAccumList (c:cs) m = accumValue c m : mkAccumList cs (countFreq c m)
          mkAccumList [] _ = []

et de récupérer la valeur accumulée en utilisant chercher comme:

mkAccumList (c:cs) m = accumValue c m : mkAccumList cs (countFreq c m)

maintenant, il est simple de créer la liste souhaitée comme:

accumValue c m = fromMaybe 0 (Map.lookup c m) + 1

put tous ensemble:

countFreq  c m = insertWith (+) c 1 m

Pour travailler avec une liste infinie:

[('a',1,100),('q',1,10),('q',2,40),('q',3,25),('q',4,25)]

donne

"aqqqq" => [1, 1, 2, 3, 4]


0 commentaires