Produit cartésien de 2 listes dans Haskell

Je souhaite produire le produit cartésien de 2 listes dans Haskell, mais je ne peux pas trouver comment le faire. Le produit cartésien donne toutes les combinaisons des éléments de la liste:

xs = [1,2,3]
ys = [4,5,6]

cartProd :: [a] -> [b] -> [(a,b)]
cartProd xs ys ==> [(1,4),(1,5),(1,6),(2,4),(2,5),(2,6),(3,4),(3,5),(3,6)]

Ce n'est pas une question de devoirs réelle et n'est pas liée à une telle question, mais la façon dont ce problème est résolu peut aider avec celui sur lequel je suis coincé.

57
demandé sur Rodrigo de Azevedo 2010-11-08 00:13:51

13 réponses

C'est très facile avec les compréhensions de liste. Pour obtenir le produit cartésien des listes de xs et ys, nous avons juste besoin de prendre le tuple (x,y) pour chaque élément de x dans xs et chaque élément de y dans ys.

Cela nous donne la compréhension de la liste suivante:

cartProd xs ys = [(x,y) | x <- xs, y <- ys]
89
répondu sepp2k 2011-06-19 14:45:34

Comme d'autres réponses l'ont noté, l'utilisation d'une compréhension de liste est la façon la plus naturelle de le faire dans Haskell.

Si vous apprenez Haskell et que vous voulez travailler sur le développement d'intuitions sur les classes de type comme Monad, cependant, c'est un exercice amusant pour comprendre pourquoi cette définition légèrement plus courte est équivalente:

import Control.Monad (liftM2)

cartProd :: [a] -> [b] -> [(a, b)]
cartProd = liftM2 (,)

Vous ne voudriez probablement jamais écrire ceci en code réel, mais l'idée de base est quelque chose que vous verrez dans Haskell tout le temps: nous utilisons liftM2 pour soulever le fonction non monadique (,) dans une monade-dans ce cas spécifiquement la monade de liste.

Si cela n'a aucun sens ou n'est pas utile, oubliez-le-c'est juste une autre façon de regarder le problème.

53
répondu Travis Brown 2010-11-07 21:52:58

Si vos listes d'entrée sont du même type, vous pouvez obtenir le produit cartésien d'un nombre arbitraire de listes en utilisant sequence (en utilisant la monade List). Ainsi, vous obtenez une liste de listes au lieu d'une liste de tuples:

> sequence [[1,2,3],[4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]
50
répondu newacct 2011-02-10 20:53:19

Il existe une façon très élégante de le faire avec les foncteurs applicatifs:

import Control.Applicative

(,) <$> [1,2,3] <*> [4,5,6]
-- [(1,4),(1,5),(1,6),(2,4),(2,5),(2,6),(3,4),(3,5),(3,6)]

L'idée de base est d'appliquer une fonction sur des arguments "enveloppés", par exemple

(+) <$> (Just 4) <*> (Just 10)
-- Just 14

Dans le cas des listes, la fonction sera appliquée à toutes les combinaisons, donc tout ce que vous avez à faire est de les "tuple" avec (,).

Voir http://learnyouahaskell.com/functors-applicative-functors-and-monoids#applicative-functors ou (plus théorique) http://www.soi.city.ac.uk / ~ ross / papers / Applicative. pdf pour plus de détails.

42
répondu Landei 2010-11-08 08:45:53

La bonne façon est d'utiliser les compréhensions de liste, comme d'autres l'ont déjà souligné, mais si vous vouliez le faire sans utiliser les compréhensions de liste pour une raison quelconque, alors vous pouvez le faire:

cartProd :: [a] -> [b] -> [(a,b)]
cartProd xs [] = []
cartProd [] ys = []
cartProd (x:xs) ys = map (\y -> (x,y)) ys ++ cartProd xs ys
11
répondu Stuart Golodetz 2010-11-07 21:30:09

Une autre façon d'y parvenir est d'utiliser des applicatifs:

import Control.Applicative

cartProd :: [a] -> [b] -> [(a,b)]
cartProd xs ys = (,) <$> xs <*> ys
11
répondu Paul 2010-11-08 03:11:59

Encore une autre façon, en utilisant la notation do:

cartProd :: [a] -> [b] -> [(a,b)]
cartProd xs ys = do x <- xs
                    y <- ys
                    return (x,y)
10
répondu gawi 2010-11-08 01:30:53

D'autres réponses supposent que les deux listes d'entrée sont finies. Souvent, le code idiomatique Haskell inclut des listes infinies, et il est donc utile de commenter brièvement la façon de produire un produit cartésien infini au cas où cela serait nécessaire.

L'approche standard consiste à utiliser la diagonalisation; en écrivant une entrée le long du haut et l'autre entrée le long de la gauche, nous pourrions écrire une table à deux dimensions contenant le produit cartésien complet comme ceci:

   1  2  3  4 ...
a a1 a2 a3 a4 ...
b b1 b2 b3 b4 ...
c c1 c2 c3 c4 ...
d d1 d2 d3 d4 ...

.  .  .  .  . .
.  .  .  .  .  .
.  .  .  .  .   .

Bien sûr, travailler sur n'importe quelle ligne nous donnera infiniment des éléments avant d'atteindre la ligne suivante; de même, aller en colonne serait désastreux. Mais nous pouvons suivre des diagonales qui descendent et à gauche, en repartant un peu plus loin vers la droite chaque fois que nous atteignons le bord de la grille.

a1

   a2
b1

      a3
   b2
c1

         a4
      b3
   c2
d1

...et ainsi de suite. Dans l'ordre, cela nous donnerait:

a1 a2 b1 a3 b2 c1 a4 b3 c2 d1 ...

Pour coder cela dans Haskell, nous pouvons d'abord écrire la version qui produit la table à deux dimensions:

cartesian2d :: [a] -> [b] -> [[(a, b)]]
cartesian2d as bs = [[(a, b) | a <- as] | b <- bs]

Un la méthode inefficace de diagonalisation consiste à itérer d'abord le long des diagonales, puis le long de la profondeur de chaque diagonale, en retirant l'élément approprié à chaque fois. Pour simplifier l'explication, je suppose que les deux listes d'entrée sont infinies, donc nous n'avons pas à jouer avec la vérification des limites.

diagonalBad :: [[a]] -> [a]
diagonalBad xs =
    [ xs !! row !! col
    | diagonal <- [0..]
    , depth <- [0..diagonal]
    , let row = depth
          col = diagonal - depth
    ]

Cette implémentation est un peu malheureuse: l'opération d'indexation de liste répétée {[9] } devient de plus en plus coûteuse au fur et à mesure, donnant une assez mauvaise performance asymptotique. Un plus une mise en œuvre efficace prendra l'idée ci-dessus mais l'implémentera en utilisant des fermetures à glissière. Donc, nous allons diviser notre grille infinie en trois formes comme ceci:

a1 a2 / a3 a4 ...
     /
    /
b1 / b2 b3 b4 ...
  /
 /
/
c1 c2 c3 c4 ...
---------------------------------
d1 d2 d3 d4 ...

 .  .  .  . .
 .  .  .  .  .
 .  .  .  .   .

Le triangle supérieur gauche sera les bits que nous avons déjà émis; le quadrilatère supérieur droit sera des lignes qui ont été partiellement émises mais qui contribueront toujours au résultat; et le rectangle inférieur sera des lignes que nous n'avons pas encore commencé à émettre. Pour commencer, le triangle supérieur et supérieur quadrilatère sera vide, et le rectangle du bas sera la grille entière. À chaque étape, nous pouvons émettre le premier élément de chaque rangée dans le quadrilatère supérieur( déplaçant essentiellement la ligne inclinée par un), puis Ajouter une nouvelle rangée du rectangle inférieur au quadrilatère supérieur (déplaçant essentiellement la ligne horizontale par un).

diagonal :: [[a]] -> [a]
diagonal = go [] where
    go upper lower = [h | h:_ <- upper] ++ case lower of
        []         -> concat (transpose upper')
        row:lower' -> go (row:upper') lower'
        where upper' = [t | _:t <- upper]

Bien que cela semble un peu plus compliqué, il est nettement plus efficace. Il gère également les limites en vérifiant que nous avons pointé dans le plus simple version.

Mais vous ne devriez pas écrire tout ce code vous-même, bien sûr! Au lieu de cela, vous devriez utiliser le paquetuniverse . Dans Data.Universe.Helpers, Il y a (+*+), quels paquets ensemble les fonctions cartesian2d et diagonal ci-dessus pour donner juste le fonctionnement du produit cartésien:

Data.Universe.Helpers> "abcd" +*+ [1..4]
[('a',1),('a',2),('b',1),('a',3),('b',2),('c',1),('a',4),('b',3),('c',2),('d',1),('b',4),('c',3),('d',2),('c',4),('d',3),('d',4)]

Vous pouvez également voir les diagonales elles-mêmes si cette structure devient utile:

Data.Universe.Helpers> mapM_ print . diagonals $ cartesian2d "abcd" [1..4]
[('a',1)]
[('a',2),('b',1)]
[('a',3),('b',2),('c',1)]
[('a',4),('b',3),('c',2),('d',1)]
[('b',4),('c',3),('d',2)]
[('c',4),('d',3)]
[('d',4)]

Si vous avez plusieurs listes à produire ensemble, itérer (+*+) peut injustement biaiser certaines listes; vous pouvez utiliser choices :: [[a]] -> [[a]] pour vos besoins de produits cartésiens à n Dimensions.

9
répondu Daniel Wagner 2017-11-15 02:34:14

Eh bien, un moyen très facile de le faire serait avec des compréhensions de liste:

cartProd :: [a] -> [b] -> [(a, b)]
cartProd xs ys = [(x, y) | x <- xs, y <- ys]

Ce que je suppose est la façon dont je ferais cela, bien que je ne sois pas un expert Haskell (par tous les moyens).

6
répondu James Cunningham 2010-11-07 21:20:49

Quelque Chose comme:

cartProd x y = [(a,b) | a <- x, b <- y]
5
répondu vichle 2010-11-07 21:21:02

C'est un travail pour sequenceing. Une implémentation monadique de celui-ci pourrait être:

cartesian :: [[a]] -> [[a]]
cartesian [] = return []
cartesian (x:xs) = x >>= \x' -> cartesian xs >>= \xs' -> return (x':xs')

*Main> cartesian [[1,2,3],[4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]

Comme vous pouvez le remarquer, ce qui précède ressemble à l'implémentation de map par des fonctions pures mais de type monadique. En conséquence, vous pouvez le simplifier jusqu'à

cartesian :: [[a]] -> [[a]]
cartesian = mapM id

*Main> cartesian [[1,2,3],[4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]
2
répondu Redu 2017-05-10 10:33:49

Voici ma mise en œuvre du produit cartésien n-ary:

crossProduct :: [[a]] -> [[a]]
crossProduct (axis:[]) = [ [v] | v <- axis ]
crossProduct (axis:rest) = [ v:r | v <- axis, r <- crossProduct rest ]
0
répondu Christian Oudard 2016-04-12 17:35:16

Il suffit D'ajouter un moyen de plus pour l'enthousiaste, en utilisant uniquement la correspondance de motif récursif.

cartProd :: [a]->[b]->[(a,b)]
cartProd _ []=[]
cartProd [] _ = []
cartProd (x:xs) (y:ys) = [(x,y)] ++ cartProd [x] ys  ++ cartProd xs ys ++  cartProd xs [y] 
0
répondu Manoj R 2018-09-25 13:53:10