it-roy-ru.com

Декартово произведение 2 списков в Haskell

Я хочу произвести декартово произведение из 2 списков на Хаскеле, но я не могу понять, как это сделать. Декартово произведение дает все комбинации элементов списка:

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)]

Это не настоящий домашний вопрос и не связан с каким-либо подобным вопросом, но способ, которым эта проблема решается, может помочь с тем, на котором я застрял.

59
Callum Rogers

Это очень легко с пониманием списка. Чтобы получить декартово произведение списков xs и ys, нам просто нужно взять кортеж (x,y) для каждого элемента x в xs и каждого элемента y в ys.

Это дает нам следующее понимание списка:

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

Как отмечалось в других ответах, использование понимания списков является наиболее естественным способом сделать это в Haskell.

Однако, если вы изучаете Haskell и хотите поработать над разработкой интуиции для классов типов, таких как Monad, очень интересно выяснить, почему это несколько более короткое определение эквивалентно:

import Control.Monad (liftM2)

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

Возможно, вам никогда не захочется писать это в реальном коде, но основная идея - это то, что вы будете постоянно видеть в Haskell: мы используем liftM2, чтобы поднять немонадную функцию (,) в монаду - в данном случае конкретно список монада.

Если это не имеет никакого смысла или бесполезно, забудьте об этом - это просто еще один способ взглянуть на проблему.

57
Travis Brown

Если ваши входные списки относятся к одному и тому же типу, вы можете получить декартово произведение произвольного числа списков, используя sequence (используя монаду List). Это даст вам список списков вместо списка кортежей:

> 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

Существует очень элегантный способ сделать это с помощью Applicative Functors:

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)]

Основная идея состоит в том, чтобы применить функцию к «завернутым» аргументам, например,.

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

В случае списков, функция будет применена ко всем комбинациям, так что все, что вам нужно сделать, это "Tuple" их с помощью (,).

Смотрите http://learnyouahaskell.com/functors-applicative-functors-and-monoids#applicative-functors или (более теоретически) http://www.soi.city.ac.uk/~ross/ документы/Applicative.pdf для деталей.

43
Landei

Другие ответы предполагают, что два входных списка конечны. Часто идиоматический код на 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
    ]

Эта реализация немного неудачна: повторяющаяся операция индексации списка !! становится все более дорогой, что приводит к очень плохой асимптотической производительности. Более эффективная реализация возьмет вышеупомянутую идею, но осуществит это, используя застежки-молнии. Итак, мы разделим нашу бесконечную сетку на три формы, подобные этой:

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-мерных картезианских потребностей.

11
Daniel Wagner

Еще один способ сделать это - использовать аппликативы:

import Control.Applicative

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

Правильный путь заключается в использовании списочных представлений, как уже указывали другие люди, но если вы хотите сделать это без использования списочных представлений по какой-либо причине, то вы можете сделать это:

cartProd :: [a] -> [b] -> [(a,b)]
cartProd xs [] = []
cartProd [] ys = []
cartProd (x:xs) ys = map (\y -> (x,y)) ys ++ cartProd xs ys
11
Stuart Golodetz

Еще один способ, используя нотацию do:

cartProd :: [a] -> [b] -> [(a,b)]
cartProd xs ys = do x <- xs
                    y <- ys
                    return (x,y)
10
gawi

Что ж, одним из самых простых способов сделать это было бы использование списков:

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

Я полагаю, что именно так я и поступлю, хотя я не эксперт по Haskell (ни в коем случае).

6
James Cunningham

что-то вроде:

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

Это работа для sequenceing. Монадическая реализация этого может быть:

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]]

Как вы можете заметить, вышеприведенное напоминает реализацию map чистыми функциями, но в монадическом типе. Соответственно, вы можете упростить его до

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

Просто добавив еще один способ для энтузиастов, используя только рекурсивное сопоставление с образцом. 

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

Вот моя реализация n-арного декартова произведения:

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