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