Інші відповіді передбачають, що два введені списки скінченні. Часто ідіоматичний код Хаскелла включає нескінченні списки, і тому варто коротко прокоментувати, як виготовити нескінченний декартовий продукт на той випадок, коли це потрібно.
Стандартний підхід полягає у використанні діагоналізації; записавши один вхід вгорі, а інший вхід зліва, ми могли б написати двовимірну таблицю, яка містила повний декартовий добуток, як це:
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
]
Ця реалізація трохи невдала: повторна операція індексації списку !!
стає все дорожчою і дорожчою в міру розвитку, що дає досить погану асимптотичну продуктивність. Більш ефективна реалізація сприйме вищезазначену ідею, але реалізує її на блискавках. Отже, ми розділимо нашу нескінченну сітку на три форми так:
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]
Хоча це виглядає дещо складніше, але значно ефективніше. Він також обробляє межі перевірки, які ми застосували в простішій версії.
Але не варто писати весь цей код самостійно, звичайно! Натомість вам слід використовувати пакет Всесвіту . В 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-мірних декартових потреб.