Haskell function to format a floating-point number with some precision I have not yet developed a satisfying coding style in Haskell. In the code snippet below, there is a lot wrong to my taste:


Helper functions which should be local to showEngFloat are defined outside.
let ... in indentation madness
More than 80 columns


Can you show me how this same code can be written in a less indentation and pollution prone way?

I am aware that the name I chose for my function is not well chosen. (show... functions should return a ShowS type, right?)

import Numeric

normalize :: Int -&gt; (Int,Int)
normalize dp =
    loop dp 0
    where
        loop dp e
            | dp &gt; 3 = loop (dp - 3) (e+3)
            | dp &lt; 1 = loop (dp + 3) (e-3)
            | otherwise = (dp,e)

toString :: [Int] -&gt; [Char]            
toString ia = concat (fmap show ia)

padZeroes nz v  = v ++ take nz (repeat '0')

showEngFloat :: RealFloat a =&gt; Int -&gt; a -&gt; [Char]
showEngFloat precision x
    | x &gt;= 0.0 = calc x
    | otherwise = "-" ++ calc (-x)
    where
        calc n =
            let (digits,dotPos0) = floatToDigits 10 n in
                let (ndigits,(dotPos1,e)) = (length digits,normalize dotPos0) in
                    let a
                            | ndigits &gt;= dotPos1 = toString (take dotPos1 digits)
                            | otherwise = padZeroes (dotPos1 - ndigits) (toString (take ndigits digits))
                            in
                            let b = padZeroes (precision - max dotPos1 ndigits) (toString (drop dotPos1 digits))
                                in
                                    let c = toString [e] in
                                        a ++ "." ++ b ++ "E" ++ c
Here's another way to write it (without changing too much). The functions toString and padZereos do sensible things on their own, so I kept them in the global namespace. Everything else is local. Having one big where-clause where you define everything local to a function is something you'll see often. Note that due to lazy evaluation you can define a_trimmed and a_padded, even if only one is used, so the other is never calculated. That further cleans up the code.

In a next step, I'd probably get rid of calc entirely, and fold sign handling into its body. Also, normalize looks like it should be calculated with divMod or quotRem.

showEngFloat :: RealFloat a =&gt; Int -&gt; a -&gt; [Char]
showEngFloat precision x = result where
  result = if x &gt;= 0 then calc x else '-' : calc (-x)
  normalize :: Int -&gt; (Int,Int)
  normalize dp = loop dp 0 where
    loop dp e
      | dp &gt; 3 = loop (dp - 3) (e+3)
      | dp &lt; 1 = loop (dp + 3) (e-3)
      | otherwise = (dp,e)
  calc n = a ++ "." ++ b ++ "E" ++ c where
    (digits,dotPos0) = floatToDigits 10 n
    ndigits = length digits
    (dotPos1,e) = normalize dotPos0
    a_trimmed = toString (take dotPos1 digits)
    a_padded  = padZeroes (dotPos1 - ndigits) (toString (take ndigits digits))
    a = if ndigits &gt;= dotPos1 then a_trimmed else a_padded
    b = padZeroes (precision - max dotPos1 ndigits) (toString (drop dotPos1 digits))
    c = toString [e]