-- Using `iterate`
collatz :: Integer -> Maybe Integer
collatz n
| n > 0 = Just (countSteps n)
| otherwise = Nothing
where
countSteps = toInteger . length . takeWhile (/= 1) . iterate nextStep
nextStep k = if even k then k `div` 2 else 3 * k + 1
-- Using `unfold`
collatz :: Integer -> Maybe Integer
collatz n
| n > 0 = Just (countSteps n)
| otherwise = Nothing
where
countSteps = toInteger . length . unfoldr nextStep
nextStep 1 = Nothing
nextStep k = Just (k, if even k then k `div` 2 else 3 * k + 1)
This approach neatly disentangles all four concerns of
- computing the next Collatz step,
- computing the sequence of all Collatz steps,
- truncating this sequence at the first
1
, and - counting the number of steps,
using only functions from the standard library for all of these except one.
iterate
and unfoldr
When you have a 'seed' element and a way of computing every next element from it, you can use iterate
or unfoldr
to produce the entire sequence.
iterate
(documentation) uses the given function to iteratively compute the next element from the previous one:
powersOfTwo = iterate (2 *) 1
-- >>> take 5 powersOfTwo
-- [1,2,4,8,16]
The list produced by iterate
is always infinite, so you might need to truncate it yourself.
unfoldr
(documentation) is more general than iterate
:
- In addition to determining what the next element should be, the given function also gets to decide when the list should end.
It does this by returning a
Just
when the list should be extended with another element, andNothing
when the list should end. - Also, new list elements are not computed from old ones, but instead from 'seed' values that can be of a different type. Every time a next element is computed, a new 'seed' for the rest of the list is produced as well.
In the following example we compute the letters of the alphabet from their place in the sequence.
alphabet :: [Char]
alphabet = unfoldr nextLetter 0
where
nextLetter :: Int -> Maybe (Char, Int)
nextLetter index
| index == 26 = Nothing
| otherwise = Just (letterAt index, index + 1)
letterAt index = chr (ord 'a' + index)
-- >>> alphabet
-- "abcdefghijklmnopqrstuvwxyz"
iterate
can be regarded as a special case of unfoldr
.
Indeed, it could have been defined as
iterate :: (a -> a) -> a -> [a]
iterate f = unfoldr (\x -> Just (x, f x))
In this approach
Given any number, nextStep
will compute the next Collatz number.
This is the only logic that is provided by us instead of by the standard library.
nextStep
is used by iterate
or unfoldr
to generate all the steps.
The sequence of steps should be terminated at the first 1
.
unfoldr
does this by itself, but iterate
needs some help from takeWhile
.
With a complete sequence of steps, the only thing that remains to be done is to count them.
We simply use length
for this.
Haskell's laziness helps this approach be efficient.
length
requests elements from the list of steps one by one.
These elements are generated on demand and discarded shortly after.
The full list of elements is never realized in memory: here the list functions less like a container and more like a generator from some other languages.
As a result, this solution operates in constant memory.