Extreme branchless: FooBarQix
This whole series about branchless started with FizzBuzz.
Today, I'd like to have a look at FooBarQix which is a variation with the following rules:
- If the number is divisible by 3, write “Foo” instead of the number
- If the number is divisible by 5, add “Bar”
- If the number is divisible by 7, add “Qix”
- For each digit 3, 5, 7, add “Foo”, “Bar”, “Qix” in the digits order
We can come up with some tests:
forM_ [(1, "1"), (2, "2"), (3, "FooFoo")] $ \(param, result) ->
it (show param <> " should be " <> result) $
foobarqix param `shouldBe` result
it "All multiple of three have 'Foo'" $
property $ \n ->
isInfixOf "Foo" $ foobarqix (3 * n)
it "All multiple of five have 'Bar'" $
property $ \n ->
isInfixOf "Bar" $ foobarqix (5 * n)
it "All multiple of seven have 'Qix'" $
property $ \n ->
isInfixOf "Qix" $ foobarqix (7 * n)
it "All number ending with three should end with 'Foo'" $
property $ \n ->
isSuffixOf "Foo" $ foobarqix (10 * n + 3)
it "All number ending with five should end with 'Bar'" $
property $ \n ->
isSuffixOf "Bar" $ foobarqix (10 * n + 5)
it "All number ending with seven should end with 'Qix'" $
property $ \n ->
isSuffixOf "Qix" $ foobarqix (10 * n + 7)
Then we can borrow FizzBuzz design, which does the first part of the rules:
foobarqix (Natural f) = head $ f tail $ error "0 is not defined" : foobarqixStream
foobarqixStream = zipWith fromMaybe numbers $ zipWith (<>) foos $ zipWith (<>) bars qixs
where
numbers = show <$> [1 ..]
foos = cycle [Nothing, Nothing, Just "Foo"]
bars = cycle [Nothing, Nothing, Nothing, Nothing, Just "Bar"]
qixs = cycle [Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Just "Qix"]
Next step is a little more tricky, we have to split the Natural
number into digits.
Let's begin with a simple divMod
(base 10), which, for any Natural
gives
the div n 10
and mod n 10
result:
data Chained a = Chained { value :: a, next :: Chained a }
chainedOf (Natural n) = (.value) . n (.next)
divMod10_0, divMod10_1, divMod10_2, divMod10_3, divMod10_4, divMod10_5, divMod10_6, divMod10_7, divMod10_8, divMod10_9 :: Natural -> Chained (Natural, Natural)
divMod10_0 u = Chained (u, 0) $ divMod10_1 u
divMod10_1 u = Chained (u, 1) $ divMod10_2 u
divMod10_2 u = Chained (u, 2) $ divMod10_3 u
divMod10_3 u = Chained (u, 3) $ divMod10_4 u
divMod10_4 u = Chained (u, 4) $ divMod10_5 u
divMod10_5 u = Chained (u, 5) $ divMod10_6 u
divMod10_6 u = Chained (u, 6) $ divMod10_7 u
divMod10_7 u = Chained (u, 7) $ divMod10_8 u
divMod10_8 u = Chained (u, 8) $ divMod10_9 u
divMod10_9 u = Chained (u, 9) $ divMod10_0 (u + 1)
Then we should somehow iterate on it until we have a list:
splitBase10 n = splitBase10 u <> [mod]
where (u, mod) = chainedOf n $ divMod10_0 0
It somehow does not as there are no base condition.
In order to achieve this, we have to distinguish 0
, returning an empty list:
splitBase10 n = runNatural n (const nonNull) []
where (u, mod) = chainedOf n $ divMod10_0 0
nonNull = splitBase10 u <> [mod]
Then we need to refactor our main function, so we can distinguish
number
-string and FooBarQuix
-string:
foobarqix n = fromMaybe (show n) $ fetchStr n
where
fetchStr n' = head $ runNatural n' tail (Nothing : foobarqixStream)
foobarqixStream = zipWith (<>) foos $ zipWith (<>) bars qixs
where
foos = cycle [Nothing, Nothing, Just "Foo"]
bars = cycle [Nothing, Nothing, Nothing, Nothing, Just "Bar"]
qixs = cycle [Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Just "Qix"]
Finally, we can bind splitBase10
, giving a [Natural]
and fetch the
corresponding Foo
/Bar
/Qix
:
foobarqix n = fromMaybe (show n) $ fetchStr n <> foldMap fetchStr (splitBase10 n)
where
fetchStr n' = head $ runNatural n' tail (Nothing : foobarqixStream)
foobarqixStream = zipWith (<>) foos $ zipWith (<>) bars qixs
where
foos = cycle [Nothing, Nothing, Just "Foo"]
bars = cycle [Nothing, Nothing, Nothing, Nothing, Just "Bar"]
qixs = cycle [Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Just "Qix"]
The step 2 adds the requirement of tracking 0
.
We could be tempted to simply duplicate fetchStr
:
foobarqix n = fromMaybe (show n) $ fetchStr n <> foldMap fetchStrDigit (splitBase10 n)
where
fetchStr n' = head $ runNatural n' tail (Nothing : foobarqixStream)
fetchStrDigit n' = head $ runNatural n' tail $ zipWith (<>) (Just "*" : repeat Nothing) (Nothing : foobarqixStream)
It works well... expect when you have only digits (e.g. 101
gives *
instead of 1*1
),
because fromMaybe
expect any Just
.
We should change our tactic:
- have a
*
in ourfromMaybe
default case - only add a
*
inFoo
/Bar
/Qix
when it's already aJust
foobarqix n =
fromMaybe (foldMap showDigit digits) $
(fetchStr n <> foldMap fetchStr digits) *> (fetchStr n <> foldMap fetchStrDigit digits)
where
digits = splitBase10 n
fetchStr n' = head $ runNatural n' tail (Nothing : foobarqixStream)
fetchStrDigit n' = head $ runNatural n' tail $ zipWith (<>) (Just "*" : repeat Nothing) (Nothing : foobarqixStream)
showDigit n' = head $ runNatural n' tail ("*" : map return ['1' .. '9'])
Note: this code contains two tricks:
- It computes
Foo
/Bar
/Qix
twice:- A first time without
*
, then it is applied to*>
which return the right-most value when the left is aJust
- A second time with
*
, which is eventually returned
- A first time without
- The
fromMaybe
default case is not really ashow
anymore, but a mapping/indexed vector to*123456789
So far so good, I enjoyed this code kata as, with few variations, it drastically changes the exercise.