简体   繁体   中英

Combining three lists into a list of 3-tuples in Haskell

I am trying to write a function that takes three lists as arguments and creates one list with a triple from each list consecutively.

The example I was given is this: zip3Lists [1, 2, 3] [4, 5, 6] ['a', 'b', 'c'] would produce [(1, 4, 'a'), (2, 5, 'b'), (3, 6, 'c')] .

What I have so far is this:

zipThree [] [] [] = []
zipThree [] [] [x] = [x]
zipThree [] [x] [] = [x]
zipThree [x] [] [] = [x]
zipThree (x:xs) (y:ys) (z:zs) = (x, y, z) : zipThree xs ys zs

and it is giving me this error:

haskell1.hs:32:33: error:
    • Occurs check: cannot construct the infinite type: c ~ (c, c, c)
      Expected type: [c]
        Actual type: [(c, c, c)]
    • In the expression: (x, y, z) : zipThree xs ys zs
      In an equation for ‘zipThree’:
          zipThree (x : xs) (y : ys) (z : zs) = (x, y, z) : zipThree xs ys zs
    • Relevant bindings include
        zs :: [c] (bound at haskell1.hs:32:27)
        z :: c (bound at haskell1.hs:32:25)
        ys :: [c] (bound at haskell1.hs:32:20)
        y :: c (bound at haskell1.hs:32:18)
        xs :: [c] (bound at haskell1.hs:32:13)
        x :: c (bound at haskell1.hs:32:11)
        (Some bindings suppressed; use -fmax-relevant-binds=N or -fno-max-relevant-binds)

First of all let's add a type signature. From the question it seems as if the following type signature is appropriate: zipThree :: [a] -> [b] -> [c] -> [(a, b, c)]

This takes 3 lists (containing possibly different types of objects) and then produces a list of triples.

You handle the empty list case fine: zipThree [] [] [] = []

Then the problem occurs. As stated in the comments you have cases for the lists having different lengths but that give a different type of output.

I'll annotate the types next to each line so you can see:

zipThree [] [] [x] = [x] :: [c]
zipThree [] [x] [] = [x] :: [b]
zipThree [x] [] [] = [x] :: [a]

These don't fit with the other two cases that have type [(a, b, c)] .

You mentioned in the comments that you will just presume the lengths are the same size therefore just removing these cases is sufficient. This gives:

zipThree [] [] [] = []
zipThree (x:xs) (y:ys) (z:zs) = (x, y, z) : zipThree xs ys zs

Which provides the correct output ( [(1, 4, 'a'), (2, 5, 'b'), (3, 6, 'c')] ) for the input you gave ( [1, 2, 3] [4, 5, 6] ['a', 'b', 'c'] ).

This function of course will fail on inputs where the lists are of different lengths. One way to stop a straight up error and allow you to handle the issue would be to wrap the result in a Maybe.

First we need to change the type to: zipThree :: [a] -> [b] -> [c] -> Maybe [(a, b, c)]

The Maybe data type can either be a value wrapped in Just so Just a or Nothing .

For the empty list we want to give just the empty list: zipThree [] [] [] = Just [] .

Naturally you might think that the next case should be: zipThree (x:xs) (y:ys) (z:zs) = Just $ (x, y, z) : zipThree xs ys zs .

But this doesn't work. Don't forget zipThree xs ys zs now has type Maybe [(a, b, c)] whereas (x, y, z) has type (a, b, c) so we can't add it to the list.

What we need to do is check the result of zipThree xs ys zs if it failed at some point during the recursion then it will be Nothing so we just want to pass that Nothing along again. If it succeeded and gave us Just as then we want to add our (x, y, z) to that list. We can check which case is relevant using case of :

zipThree (x:xs) (y:ys) (z:zs) = case zipThree xs ys zs of
    Nothing -> Nothing
    Just as -> Just $ (x, y, z) : as

We will know our lists aren't the same length if at some point during the recursion some lists are empty and others aren't. This doesn't match either pattern we have at the moment [] [] [] or (x:xs) (y:ys) (z:zs) so we need one final catch all case to give us that Nothing and prevent the error:

zipThree _ _ _ = Nothing

This gives a final definition of:

zipThree :: [a] -> [b] -> [c] -> Maybe [(a, b, c)]
zipThree [] [] [] = Just []
zipThree (x:xs) (y:ys) (z:zs) = case zipThree xs ys zs of
    Nothing -> Nothing
    Just as -> Just $ (x, y, z) : as
zipThree _ _ _ = Nothing

The results for the examples are:

zipThree [1, 2, 3] [4, 5, 6] ['a', 'b', 'c', 'd'] = Nothing

and

zipThree [1, 2, 3] [4, 5, 6] ['a', 'b', 'c'] = Just [(1, 4, 'a'), (2, 5, 'b'), (3, 6, 'c')] .

Hope this helps, feel free to ask for clarification :)

EDIT: As suggested in the comments the following definitions would stop short in the case the lists are different lengths:

zipThree :: [a] -> [b] -> [c] -> [(a, b, c)]
zipThree (x:xs) (y:ys) (z:zs) = (x, y, z) : zipThree xs ys zs
zipThree _ _ _ = []

zipThree :: [a] -> [b] -> [c] -> Maybe [(a, b, c)]
zipThree (x:xs) (y:ys) (z:zs) = case zipThree xs ys zs of
    Nothing -> Just [(x, y, z)] -- Change is here
    Just as -> Just $ (x, y, z) : as
zipThree _ _ _ = Nothing

PS Thanks to the guy who added the missing Just in an edit.

There is this ZipList type defined in Control.Applicative module which is in fact exactly thought for this job.

ZipList type is derived from the List type like

newtype ZipList a = ZipList { getZipList :: [a] }
                    deriving ( Show, Eq, Ord, Read, Functor, Foldable
                             , Generic, Generic1)

Unlike normal List s it's Applicative instance does not work on combinations but one to one on corresponding elements like zipping. Hence the name ZipList . This is the Applicative instance of ZipList

instance Applicative ZipList where
    pure x = ZipList (repeat x)
    liftA2 f (ZipList xs) (ZipList ys) = ZipList (zipWith f xs ys)

The advantage of zipList is we chain up indefinitely many lists to zip with. So when zipWith7 is not sufficient you may still carry on with a ZipList . So here is the code;

import Control.Applicative

zip'mAll :: [Int] -> [Int] -> String -> [(Int,Int,Char)]
zip'mAll xs ys cs = getZipList $ (,,) <$> ZipList xs <*> ZipList ys <*> ZipList cs

*Main> zip'mAll [1,2,3] [4,5,6] "abc"
[(1,4,'a'),(2,5,'b'),(3,6,'c')]

Firstly, we need a type signature, as stated by James Burton, who lists a suitable one also:

zipThree :: [a] -> [b] -> [c] -> [(a, b, c)]

Essentially, this type signature says that, given three lists of any type a, b or c, a list of three-value tuples whose type is (a, b, c) shall be produced.

If we disregard the need to handle invalid cases (empty lists, variable-length lists), we next need to implement a valid case which produces the correct tuple from the lists given. Your statement

zipThree (x:xs) (y:ys) (z:zs) = (x, y, z) : zipThree xs ys zs 

is valid. Therefore, thus far we have:

zipThree :: [a] -> [b] -> [c] -> [(a, b, c)]
zipThree (x:xs) (y:ys) (z:zs) = (x, y, z) : zipThree xs ys zs 

The problem occurs when you introduce the cases for your invalid lists:

zipThree [] [] [x] = [x]
zipThree [] [x] [] = [x]
zipThree [x] [] [] = [x]

When one of these cases is matched, the type attempting to be bound is invalid due to being of type [x], where type (x, y, z) is expected.

You could exhaustively attempt to match base cases before recursively accessing the function again. However, you could also simply declare the case

zipThree _ _ _ = []

after, which will end the recursion with invalid input.

Putting this altogether, we are left with:

zipThree :: [a] -> [b] -> [c] -> [(a, b, c)]
zipThree (x:xs) (y:ys) (z:zs) = (x, y, z) : zipThree xs ys zs
zipThree _ _ _                = []


What's good about this implementation is that the recursion ends when any list is empty, thus stopping short for uneven lists, eg

zipThree [1, 2, 3] [4, 5, 6] [7, 8]

would produce

[(1, 4, 7), (2, 5, 8)]

Good luck!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM