Let’s grab a page from our high school literature classes and compare how different uses of language can accomplish a similar task. We will compare the function that removes every n’th element from a list between Python and Haskell implementations. See the Haskell function from the first post below:
-- Problem 16: Drop every n-th entry
dropEveryHelper :: [a] -> Int -> Int -> [a]
dropEveryHelper [] _ _ = []
dropEveryHelper xs n cnt
| (cnt `mod` n == 0) && ( cnt > 0) = dropEveryHelper (tail xs) n (cnt + 1)
| otherwise = x : dropEveryHelper (tail xs) n (cnt + 1)
where x = head xs
dropEvery :: [a] -> Int -> [a]
dropEvery xs n = dropEveryHelper xs n 1
If I call dropEvery xs n, xs will never be changed by the function. It has no side-effects. In addition, as long as I don’t change xs or n, the function will always return the same value. These two properties make dropEvery a pure function. In Haskell, functions are by default, but not always, pure. For other languages such as Python, this is not always so. Let’s take a look at a function that accomplishes the same task in Python:
def dropEverySideEffect(xs, n):
toRemove = [xs[i] for i in range(n-1, len(xs), n)]
for el in toRemove:
xs.remove(el)
return xs
If we call dropEverySideEffects(xs, n) where xs=[1,2,3,4,5,6],n=2, we will get the correct result [1,3,5]. However, xs will now be [1,3,5] in its original scope. This is a side-effect. For a small program, this may be easy to manage, but if xs is being passed to many different functions, this could get problematic very fast. Haskell stops us from doing this by forcing us to use mainly pure functions. In Python, we could still avoid side-effects as can be seen in the below example:
def dropEveryNoSideEffect(xs, n):
ys = []
for i in range(len(xs)):
if (i + 1) % n != 0:
ys += [xs[i]]
return ys
By creating a new list, we avoid altering xs in a different scope, but the responsibility to avoid side-effects is on the programmer here. In the case of Haskell, the language stops us from using side-effects.