web-dev-qa-db-fra.com

Produit cartésien de 2 listes en Haskell

Je souhaite produire le produit cartésien de 2 listes en Haskell, mais je ne sais pas 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 devoir mais ne se rapporte à aucune question de ce type, mais la façon dont ce problème est résolu peut aider à résoudre un problème qui m’attache.

59
Callum Rogers

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

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

cartProd xs ys = [(x,y) | x <- xs, y <- ys]
94
sepp2k

Comme d'autres réponses l'ont noté, utiliser une liste de compréhension est le moyen le plus naturel de le faire en Haskell.

Si vous étudiez Haskell et souhaitez développer des intuitions sur les classes de types telles que Monad, il est toutefois amusant de 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 cela dans du code réel, mais l'idée de base est quelque chose que vous verrez tout le temps dans Haskell: nous utilisons liftM2 pour transformer la fonction non monadique (,) en monade, dans ce cas spécifiquement la liste monade.

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

57
Travis Brown

Si vos listes d'entrées sont du même type, vous pouvez obtenir le produit cartésien d'un nombre arbitraire de listes à l'aide de sequence (à l'aide de List monad). Cela vous donnera 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
newacct

Il existe un moyen très élégant de faire cela 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 les arguments "encapsulés", par exemple.

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

Dans le cas de listes, la fonction s'appliquera à toutes les combinaisons, il vous suffira donc de les "Tupler" avec (,).

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

43
Landei

D'autres réponses supposent que les deux listes d'entrées sont finies. Souvent, le code idiomatique de Haskell inclut des listes infinies. Il est donc intéressant de commenter brièvement la manière 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 en haut et l'autre en bas à gauche, nous pourrions écrire un tableau à 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 une seule ligne nous donnera une infinité d’éléments avant d’atteindre la ligne suivante; de la même manière, une colonne serait désastreuse. Mais nous pouvons suivre des diagonales qui descendent et se dirigent vers la gauche, en repartant un peu plus loin à droite chaque fois que nous atteignons le bord de la grille.

a1

   a2
b1

      a3
   b2
c1

         a4
      b3
   c2
d1

...etc. Dans l'ordre, cela nous donnerait:

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

Pour coder cela en Haskell, nous pouvons d’abord écrire la version qui produit le tableau à deux dimensions:

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

Une méthode inefficace de diagonalisation consiste à itérer d’abord en diagonale, puis en profondeur de chaque diagonale, en tirant à chaque fois l’élément approprié. Pour simplifier l'explication, je suppose que les deux listes d'entrées sont infinies, nous n'avons donc pas à nous soucier de 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 fâcheuse: l'opération d'indexation par liste répétée !! devient de plus en plus coûteuse au fur et à mesure, ce qui donne une très mauvaise performance asymptotique. Une implémentation plus efficace prendrait l’idée ci-dessus mais l’appliquerait à l’aide de fermetures à glissière. Nous allons donc diviser notre grille infinie en trois formes comme celle-ci:

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

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

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

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 la vérification des limites sur laquelle nous nous sommes penchés dans la version simplifiée.

Mais vous ne devriez pas écrire tout ce code vous-même, bien sûr! Au lieu de cela, vous devriez utiliser le paquet universe . Dans Data.Universe.Helpers , il y a (+*+) , qui regroupe les fonctions cartesian2d et diagonal ci-dessus pour donner uniquement l'opération de produit cartésienne:

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 biaiser injustement certaines listes; vous pouvez utiliser choices :: [[a]] -> [[a]] pour vos besoins en produits cartésiens à n dimensions.

11
Daniel Wagner

Une autre méthode consiste à utiliser des applications:

import Control.Applicative

cartProd :: [a] -> [b] -> [(a,b)]
cartProd xs ys = (,) <$> xs <*> ys
11
Paul

La bonne façon consiste à utiliser la compréhension de liste, comme d'autres personnes l'ont déjà souligné, mais si vous vouliez le faire sans utiliser de compréhension de liste pour une raison quelconque, 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
Stuart Golodetz

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
gawi

Eh bien, un moyen très simple de faire cela serait de comprendre la liste:

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

Je suppose que c'est ce que je ferais, bien que je ne sois pas un expert de Haskell (loin de là).

6
James Cunningham

quelque chose comme:

cartProd x y = [(a,b) | a <- x, b <- y]
5
vichle

C'est un travail pour sequenceing. Une implémentation monadique 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 constater, ce qui précède ressemble à l’implémentation de map par des fonctions pures mais en 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
Redu

Il suffit d'ajouter un autre moyen pour les amateurs, en utilisant uniquement la correspondance de motif récursive. 

cartProd :: [a]->[b]->[(a,b)]
cartProd _ []=[]
cartProd [] _ = []
cartProd (x:xs) (y:ys) = [(x,y)] ++ cartProd [x] ys  ++ cartProd xs ys ++  cartProd xs [y] 
0
Manoj R

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

crossProduct :: [[a]] -> [[a]]
crossProduct (axis:[]) = [ [v] | v <- axis ]
crossProduct (axis:rest) = [ v:r | v <- axis, r <- crossProduct rest ]
0
Christian Oudard