Wednesday 30 September 2015

I stumbled upon a 'functional programming interview question' recently; and I came up with a really nice functional programming answer, so feel free to drop the quotes actually.

Question: Given 'n' and a stream of floats, return a stream of the running average of the most recent n elements. If there aren't n elements yet output the average of all the elements available.

I looked at the problem and my first thought was a scanl maintaining the sum, count and the last n elements, with an appropriate update function.
Something like:

`avg :: Int -> [Float] -> [Float]
avg b = map getAvg . tail . scanl f def 
    where 
      getAvg (x, _, _) = x
      def = (0, 0, [])
      f :: (Float, Int, [Float]) -> Float -> (Float, Int, [Float])
      f (_, 0, _) v = (v, 1, [v])
      f (avg, n', x:xs) v | n' == b = (avg - x/n + v/n, n', xs++[v])
                          | otherwise = (avg*n/(n+1) + v/(n+1.0), n'+1, x:xs++[v])
          where 
            n :: Float
            n = fi n'

fi = fromIntegral`

We could improve the update function by tracking the sum and calculating the average at the time of `getAvg`
`avg :: Int -> [Float] -> [Float]
avg b = map getAvg . tail . scanl f def 
    where 
      getAvg (x, n, _) = x/fi n
      def = (0, 0, [])
      f :: (Float, Int, [Float]) -> Float -> (Float, Int, [Float])
      f (_, 0, _) v = (v, 1, [v])
      f (s, n', x:xs) v | n' == b = (s - x + v, n', xs++[v])
                        | otherwise = (s + v, n'+1, x:xs++[v])`

While that's pretty functional what with the scanl but I want to improve it.
The inspiration: `avg(i) = (f(i) - f(i-n))/n(i)`

So what do we need in order to calculate the average at some random point in the stream?
The sum of the last n elements and the min(number of elements, n).

Let f(i) | i < 0 = 0
            | otherwise = sum of all elements upto the ith index.
Sum of the last n elements = f(i) - f(i-n)


In the spirit of Haskell we can combine these quite cleanly:
Sum of the elements: `f(i) = scanl (+) 0 l !! i`
Sum of the elements n elements back: `f(i-n) = replicate n 0 ++ f(i) !! i`
`let lSum = scanl (+) 0 l`
Sum of the last n elements : `f(i) - f(i-n) = zipWith (-) lSum (replicate n 0 ++ lSum) !! i`
Number of elements so far: `n(i) = map (fi . min n) [1..] !! i`
Average: `avg(i) = (f(i) - f(i-n))/n(i)`

Join 'em together:  
f :: Int -> [Float] -> [Float]
f n l = zipWith (/) (zipWith (-) lSum lSum') lLen where lLen = map (fi . min n) [1..] lSum = tail (scanl (+) 0 l) lSum' = replicate n 0 ++ lSum  
fi = fromIntegral 

Bootiful.

No comments:

Post a Comment