其他答案假设两个输入列表是有限的。通常,惯用的 Haskell 代码包含无限列表,因此值得简要评论一下如何在需要时生成无限笛卡尔积。
标准方法是使用对角化;将一个输入写在顶部,另一个输入写在左边,我们可以写一个包含完整笛卡尔积的二维表,如下所示:
1 2 3 4 ...
a a1 a2 a3 a4 ...
b b1 b2 b3 b4 ...
c c1 c2 c3 c4 ...
d d1 d2 d3 d4 ...
. . . . . .
. . . . . .
. . . . . .
当然,在任何单行上工作都会在到达下一行之前给我们无限的元素;同样按列进行将是灾难性的。但是我们可以沿着向下和向左的对角线前进,每次到达网格边缘时,都会从更远的右边开始。
a1
a2
b1
a3
b2
c1
a4
b3
c2
d1
...等等。按顺序,这将给我们:
a1 a2 b1 a3 b2 c1 a4 b3 c2 d1 ...
要在 Haskell 中编码,我们可以先编写生成二维表的版本:
cartesian2d :: [a] -> [b] -> [[(a, b)]]
cartesian2d as bs = [[(a, b) | a <- as] | b <- bs]
一种低效的对角化方法是先沿对角线迭代,然后沿每个对角线的深度迭代,每次都拉出适当的元素。为了解释的简单起见,我假设两个输入列表都是无限的,所以我们不必搞乱边界检查。
diagonalBad :: [[a]] -> [a]
diagonalBad xs =
[ xs !! row !! col
| diagonal <- [0..]
, depth <- [0..diagonal]
, let row = depth
col = diagonal - depth
]
这个实现有点不幸:重复的列表索引操作!! 变得越来越昂贵,渐近性能很差。更有效的实现将采用上述想法,但使用 zippers 实现它。因此,我们将无限网格划分为如下三种形状:
a1 a2 / a3 a4 ...
/
/
b1 / b2 b3 b4 ...
/
/
/
c1 c2 c3 c4 ...
---------------------------------
d1 d2 d3 d4 ...
. . . . .
. . . . .
. . . . .
左上角的三角形将是我们已经发出的位;右上角的四边形将是部分发射但仍对结果有贡献的行;底部的矩形将是我们尚未开始发射的行。首先,上面的三角形和上面的四边形是空的,下面的矩形是整个网格。在每一步中,我们可以发出上四边形中每一行的第一个元素(基本上将斜线移动一个),然后从底部矩形添加一个新行到上四边形(基本上将水平线向下移动一个)。
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]
虽然这看起来有点复杂,但它的效率要高得多。它还处理我们在更简单的版本中进行的边界检查。
当然,您不应该自己编写所有这些代码!相反,您应该使用universe 包。在Data.Universe.Helpers中,有(+*+),将上面的cartesian2d和diagonal函数封装在一起,只做笛卡尔积运算:
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)]
如果该结构变得有用,您还可以看到对角线本身:
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)]
如果您有许多列表要一起生成,迭代(+*+) 可能会不公平地偏向某些列表;您可以使用 choices :: [[a]] -> [[a]] 来满足您的 n 维笛卡尔积需求。