It's wild how easily imperative languages leak implementation details
24 September 2023Functional programmers always go on about how purity makes programs “easier to reason about”, but honestly, it’s much more important that purity seems to be the only way to keep programs from leaking their internal implementation details everywhere.
Imagine you’re writing an implementation of List.map2
in
OCaml (zipWith
in Haskell). In OCaml, this throws an
exception or returns None
if the lists have different
lengths, so you might write it like this.
let map2 f list1 list2 =
if List.compare_length list1 list2 <> 0 then
None
else
let go list1 list2 = match list1, list2 with
| ([], _) | (_, []) -> []
| (x :: xs, y :: ys) -> f x y :: go xs ys
in
Some (go list1 list2)
This works, so you publish it, get a bunch of people to use it and forget about it for a few months.
One day you look back at your code and notice that this is kind of inefficient since it needs to traverse the lists twice, once to check the length and once to actually do the mapping. You realize that it’s possible to merge the two traversals and only bail out at the end, so you rewrite your code
let map2 f list1 list2 =
match list1, list2 with
| ([], []) -> Some []
| ([], _) | (_, []) -> None
| (x :: xs, y :: ys) -> Option.map (fun rest -> f x y :: rest) (map2 f xs ys)
And congrats: You just broke your public interface!
If f
performs a side effect, the first implementation
would never perform that effect on lists with different lengths, whereas
the second one will until it hits the end of the shorter one.
Good luck finding that bug.