In the last post we talked about using unfolds to zip and more in OCaml. In this post we write the equivalent functions in Haskell.
Zip is in the standard library in Haskell. It’s actually so elegant that there isn’t much point using unfolds instead. To zip two lists, simply use the zip function:
Prelude> zip [1,2,3] [4,5,6,7] [(1,4),(2,5),(3,6)] Prelude> zip [] [1,2] []
To zip more than two lists, you can use the standard function of zip that goes up to seven in Data.List. Beyond that, one can easily write the zip8 function the same way as the zip7 function in the standard library:
You can run the command runhaskell zip8.hs in a terminal (in the directory of your zip8.hs file) and see the example results printed.
If we want the zip to stop under different conditions, we can specify it in the pattern matching:
If we want the output list to have a different configuration, we can modify the function. E.g., to repeat the second input:
Another example: the second item of the tuple of the output list is the sum of the elements of the input list:
Even though unfolds is not directly in these Haskell code, the recursive component is still there. It can be helpful to think in terms of unfolds when your output is a list.
Comparison to OCaml
Haskell’s zip function can behave differently from OCaml’s. The difference stems from that Haskell is lazy while OCaml is eager. Because Haskell is lazy or non-strict, the order of passing in an invalid input matters. E.g., passing in an undefined second input (using undefined) returns an empty list:
Prelude> zip [] undefined
[]
Because the zip function was defined with the first pattern matching as an empty list, Haskell does not check the second input and returns the matched result regardless of the second input. However, when the first input is undefined, an error occurs, because Haskell cannot perform the first pattern matching properly:
Prelude> zip undefined [] *** Exception: Prelude.undefined CallStack (from HasCallStack): error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err undefined, called at <interactive>:3:5 in interactive:Ghci2
On the other hand, because OCaml is eager or strict, it evaluates all arguments before passing them into the function. Therefore, it doesn’t matter which input is ill-defined, OCaml returns an error.
(failwith in OCaml is similar to error in Haskell. It takes in a string and returns it in case of an error. Passing the string “undefined” to the failwith function gives us the equivalent of undefined in Haskell.)
utop # #use "zip.ml";; val unfold : ('a -> 'b -> bool) -> ('a -> 'b -> 'c * ('a * 'b)) -> 'a -> 'b -> 'c list = <fun> val zip : '_a list -> '_b list -> ('_a * '_b) list = <fun> utop # zip [] (failwith "undefined");; Exception: Failure "undefined". utop # zip (failwith "undefined") [];; Exception: Failure "undefined".
Similarly for the zip function that stops zipping on any zero element. In Haskell, it does not look at any element after zero:
$ *Main> zip_no_zero [1,2,0,undefined] [3,4] [(1,3),(2,4)]
In OCaml, the equivalent returns an error:
utop # zip_no_zero [1;2;0;(failwith "undefined")] [3;4;5];; Exception: Failure "undefined".
Which of the lazy or eager styles do you think is better? I don’t like that in Haskell the two inputs are not symmetric in this specific case, but I like that it can return a sane result when you have a work-in-progress function.
This post ended up deviating from recursion schemes, which we’ll return to in the next post. We’ll look at combining catamorphisms and anamorphisms and power up even more!
Leave a Reply