This is a tiny etude on a theme given here, using Python: Provide a function that generates a human readable file size such as 10.0GiB, 13B, 42.2TiB from a given integer number. (A similar Ruby etude is here.) The function should

• gracefully handle any input (even negative numbers, although they don't make sense for counting bytes - maybe they could represent negative offsets)
• print numbers smaller than 1024 without a decimal point
• print all other numbers with one decimal place and the appropriate unit (KiB, MiB, etc.)

It's more verbose than the python counterpart, so the question is whether this can be made more terse.

    humanReadableBytes :: Integer -> String
humanReadableBytes size | null pairs = printf "%.0fZiB" (size'/1024^7)
| otherwise  = if unit=="" then printf "%dB" size
else printf "%.1f%sB" n unit
where
pairs = dropWhile ((1024<).abs.fst)
(zip (map ((size'/).(1024^)) [0..]) units) :: [(Double, String)]
size' = fromIntegral size
units = ["","Ki","Mi","Gi","Ti","Pi","Ei","Zi"]


Right away it can be noted that the use of String is suboptimal, but Text.Printf does not allow Text, so printf ought to be replaced.

We begin with the "loop". map ((size'/).(1024^)) [0..] is iterate (/1024) size. This has not necessarily the same effect, since (x/y)/y is not the same as x/y² when using floating point arithmetic, but it's fine for our cause. It also works like the Python code, which was your original intention.

We now have

humanReadableBytes :: Integer -> String
humanReadableBytes size | null pairs = printf "%.0fZiB" (size'/1024^7)
| otherwise  = if unit=="" then printf "%dB" size
else printf "%.1f%sB" n unit
where
pairs = zip (iterate (/1024) 'size)) units
size' = fromIntegral size :: Double
units = ["","Ki","Mi","Gi","Ti","Pi","Ei","Zi"]


And that's all we can improve in terms of terseness at that point without changing your code fundamentally. The main difference between the Python and the Haskell implementation is the mutation of the original num and the early exit from the function.

I would recommend to check the "B" case earlier by the way:

humanReadableBytes :: Integer -> String