Декартовий твір з 2 списків у Хаскелі


74

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

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

Це не справжнє запитання домашнього завдання і не пов’язане з яким-небудь таким запитанням, але спосіб вирішення цієї проблеми може допомогти з тим, на якому я застряг.


Відповіді:


115

Це дуже просто з розумінням списків. Щоб отримати декартовий добуток зі списків xsі ys, нам просто потрібно взяти кортеж (x,y)для кожного елемента xв xsі кожного елемента yв ys.

Це дає нам таке розуміння списку:

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

2
Дякую, такий простий, але елегантний, справді допоміг з іншою проблемою :)
Каллум Роджерс

3
Добре також для Ерланга, дякую. Незначно змінюється синтаксис: cartProd (Xs, Ys) -> [{X, Y} || X <- Xs, Y <- Ys].
Dacav

72

Як зазначалося в інших відповідях, використання розуміння списку є найбільш природним способом зробити це в Хаскелі.

Якщо ви вивчаєте Haskell і хочете працювати над розробкою інтуїції щодо класів типу Monad, однак, це цікава вправа зрозуміти, чому це трохи коротше визначення еквівалентно:

import Control.Monad (liftM2)

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

Напевно, ви ніколи не захотіли б писати це в реальному коді, але основна ідея - це те, що ви будете постійно бачити в Haskell: ми використовуємо liftM2для підняття немонадичної функції (,)в монаду - в даному випадку конкретно список монада.

Якщо це не має жодного сенсу або не корисно, забудьте про це - це лише інший спосіб розглянути проблему.


4
Можливо, це гарна ідея дізнатись, що насправді в першу чергу роблять монади: P
Каллум Роджерс

19
Як виноска (через три роки): я здогадуюсь тепер, коли спочатку використовував liftM2тут монадику заради ясності (більше людей чуло про монади, ніж аплікативні функтори?), Але все, що вам потрібно, це екземпляр аплікативного функтора для списків , тому liftA2буде працювати рівнозначно.
Тревіс Браун,

58

Якщо ваші списки введення однакові, ви можете отримати декартовий добуток довільної кількості списків за допомогою 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]]

49

Існує дуже елегантний спосіб зробити це за допомогою аплікативних функторів:

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

У випадку зі списками функція застосовуватиметься до всіх комбінацій, тож усе, що вам потрібно зробити, це "скласти" їх (,).

Див http://learnyouahaskell.com/functors-applicative-functors-and-monoids#applicative-functors або (більш теоретичний) http://www.soi.city.ac.uk/~ross/papers/Applicative.pdf для деталі.


4
Дуже круто, що ви можете розширити кортеж за потреби: (,,) <$> [1..3] <*> [4..6] <*> [7..9]
Лендон Поч

17

Інші відповіді передбачають, що два введені списки скінченні. Часто ідіоматичний код Хаскелла включає нескінченні списки, і тому варто коротко прокоментувати, як виготовити нескінченний декартовий продукт на той випадок, коли це потрібно.

Стандартний підхід полягає у використанні діагоналізації; записавши один вхід вгорі, а інший вхід зліва, ми могли б написати двовимірну таблицю, яка містила повний декартовий добуток, як це:

   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-мірних декартових потреб.


Я щойно встановив Universe-1.1, і це казково. Щиро дякую. Так, ваше ім’я повсюди, тому ми знаємо, що йому довіряємо. Значення неможливо обчислити. Я робив діагоналі, і ці початкові результати виглядають точно. Декартові продукти - це біль, але ти вбиваєш біль. Ще раз дякую вам.
fp_mora

@fp_mora Радий почути, що ти насолоджуєшся цим!
Даніель Вагнер,

@ Даніель Вагнер Це знахідка. Мені було незрозуміло. Це нескінченні списки - це біль. Ви поводитесь з ними спритно.
fp_mora

15

Ще один спосіб досягти цього - використання додатків:

import Control.Applicative

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

12

Правильний спосіб - використовувати розуміння списків, на що вже вказували інші люди, але якщо ви хочете зробити це без використання розуміння списку з будь-якої причини, тоді ви можете зробити це:

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

3
Більш простий спосіб без розуміння спискуcartProd xs ys = xs >>= \x -> ys >>= \y -> [(x,y)]
Чак

5
Замість map (\y -> (x,y))вас можна писати map ((,) x).
Іц

1
@Chuck: Nice :) Для мене по-
хаскельськи минув

@Yitz: Так, гарний дзвінок. Я про це забув (див. Вище про те, "минуло вже мало часу") ...
Стюарт Голодец,

Розуміння @Stuart List може бути «правильним способом», але чи є насправді якийсь мінус, коли робити це таким чином? Мені здається чудовим, і це допомагає новачкові обговорити свої думки щодо простої реалізації декартових продуктів. +1
Лендон Поч

12

Ще один спосіб, використовуючи doпозначення:

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

6

Ну, один дуже простий спосіб це зробити - це розуміння списку:

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

Я гадаю, це як я це міг би зробити, хоча я не фахівець Хаскелла (будь-якими способами).



3

Це робота для sequenceінж. Монадичною реалізацією цього може бути:

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

0

Ось моя реалізація n-арного декартового продукту:

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

1
Як щодо crossProduct = sequence?
Даніель Вагнер,

Що робити, якщо список порожній? Я здогадуюсь, ви визначили тут неправильний базовий випадок.
dfeuer

0

Просто додайте ще один спосіб для ентузіастів, використовуючи лише рекурсивне узгодження шаблонів.

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

0

Рекурсивне узгодження шаблону з розумінням списку

crossProduct [] b=[]
crossProduct (x : xs) b= [(x,b)] ++ crossProduct xs b

cartProd _ []=[]
cartProd x (u:uv) = crossProduct x u ++ cartProd x uv

Відповіді лише на код не рекомендується. Будь ласка, додайте пояснення щодо того, як це вирішує проблему, або чим це відрізняється від існуючих відповідей. З огляду
Нік,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.