Home > Software design >  Cartesian product set generation
Cartesian product set generation

Time:07-07

I am trying to create a Haskell function to generate a Cartesian product of two lists

Here's my attempt:

cartesianProduct :: (a -> b -> c) -> [a] -> [b] -> [c]
cartesianProduct f x [] = x
cartesianProduct f y [] = y
cartesianProduct = f x y unionHelper func1 f xs ys

CodePudding user response:

If you're in a hurry, then the function is called liftA2 and it's actually built-in.

sort $ liftA2 (*) [1,2,4] [1,3,9] -- [1,2,3,4,6,9,12,18,36]

But, of course, that's not very enlightening, so let's talk about how we might do it ourselves.

The list type, [], is a functor. We can implement fmap directly using recursion.

fmap :: (a -> b) -> [a] -> [b]
fmap _ [] = []
fmap f (x:xs) = f x : fmap f xs

Now we have a way of to do something for each element of a list. Great, that's how we'll apply our final operation, but we need to generate all of the argument lists as well. Specifically, we want to produce the Cartesian product.

cross :: [a] -> [b] -> [(a, b)]

And we can do so recursively as well, using our fmap function to help out.

cross :: [a] -> [b] -> [(a, b)]
cross [] _ = []
cross (x:xs) ys = fmap ((,) x) ys    cross xs ys

Recursion on the first element. If the first argument is empty, then the Cartesian product is empty. If the first argument is nonempty, then the result should consist of the head paired with each (via fmap) element of the second argument, followed by the Cartesian product of the tail with the second list.

Now we have a way to get a [(Int, Int)]. And we want to multiply those elements, so it's just a matter of tying it all together.

apply2 :: (a -> b -> c) -> [a] -> [b] -> [c]
apply2 f xs ys = fmap (\(x, y) -> f x y) $ cross xs ys

Take the Cartesian product of the two lists, then apply our function f (uncurried) to each element.

This gives us our result, in a slightly different order than in your example. But it looks like you're sorting the results anyway, rather than producing them in an algorithmic order.

sort $ apply2 (*) [1,2,4] [1,3,9] -- [1,2,3,4,6,9,12,18,36]

Now, let's talk about why liftA2 works. The list type is an example of an applicative functor, represented by the Applicative typeclass. Whereas fmap (which is part of Functor) takes a single function and maps it over a functor (in our case, over several elements of a list), the fundamental applicative operation is <*> (pronounced "ap"), whose signature is

fmap  :: Functor f     =>   (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

Instead of mapping a single simple function over a functor value f a, we're mapping a function inside the functor f (a -> b) over a value also inside the functor f a. Now, this is a jumble of words, but in the case of lists it looks like

fmap  :: (a -> b) -> [a] -> [b]
(<*>) :: [a -> b] -> [a] -> [b]

Rather than applying a single function to several elements, we're applying several functions to several elements. (<*>) is actually just our cross function written in a slightly different way. It's combining two lists using every combination to produce a result of some type. (In fact, the relationship between our cross and the applicative (<*>) is rather deep mathematically, due to the fact that (->) and (,) are adjoint to each other, but that's getting off track)

So (<*>) is just cross by another name. And liftA2 is defined as (something equivalent to)

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = fmap f a <*> b

It's just doing fmap and (<*>), which is basically what our apply2 function was doing to begin with. Again, it's slightly shuffled around since we're using (->) rather than (,) (since our "ap" operator applies a function rather than building a tuple), but it's exactly the same idea.

Certainly, the liftA2 approach is snazzy and short, but don't let it blind you. There's nothing wrong with a recursive approach, especially as you're learning. If apply2 makes more sense to you and my explanation above of the applicative functors was nonsense, then go for it. You can go a long way in Haskell learning about recursion and data structures before really grokking functors, and that's fine. But when you're ready, I recommend Typeclassopedia for an excellent summary of the Haskell typeclasses that represent functors, applicative functors, monads, and all of the other lesser-known abstractions available to the programmer.

Good luck, and happy coding!

  • Related