I am getting through the excellent book "Real-World Functional Programming" by Tomas Petricek and Jon Skeet (two SO gurus, btw, thanks to both of them). The following function, at page 277, introduces a way of computing a three point average on an array taking into account side values as special ones:
let blurArray (arr:float[]) =
let res = Array.create arr.Length 0.0
res.[0] <- (arr.[0] + arr.[1]) / 2.0
res.[arr.Length-1] <- (arr.[arr.Length - 2] + arr.[arr.Length -1 ] )/2.0
for i in 1 .. arr.Length - 2 do
res.[i] <- (arr.[i-1] + arr.[i] + arr.[i+1]) / 3.0
res
I understand the function is immutable for the external world, even if internally adopts mutation and assignment. Also, while it is truly imperative, it can me composed and be adopted preserving a declarative style. Nonethless, I tried to come up with a functional solution, as exercise to get more familiar with higher order functions and the like. I will report my solution and then will express my questions. First I define a helper function
let computeElementsAverage (myArray:float[]) myIndex (myElement:float) =
match myIndex with
| 0 -> (myArray.[0..1] |> Array.average )
| lastInd when lastInd = (myArray.Length -1 ) -> (myArray.[lastInd-1..lastInd] |> Array.average )
| anIndex -> (myArray.[anIndex -1 .. anIndex + 1 ] |> Array.average )
(I could have abstracted out the (list of indeces -> float) which is a pattern on the match clauses, but I considered it an overkill).
Then the equivalent of the blurArray becomes:
let blurArray' (arr:float[]) =
let mappingFcn = arr |> computeElementsAverage
arr |> (Array.mapi mappingFcn)
Finally my questions:
first and foremost what would be the most recommended functional way of proceeding? I specifically do not like the fact that I am forced to declare a final element of element type (float) in computeElementsAverage because of the requirements of Array.mapi. Are there any better methods to do that, avoiding an argument which will not be used?
Secondly: performancewise my code is much slower, which was expected; but it runs 1/10 faster than the original code; any other solution which would still rely on high order functions without getting such a big hit on performance?
Finally what is the general preferred way to perform computation accross a data structure (e.g.: arrays) which depends on several multiple indeces? The way I came up with,as you can see, is using the mapi scanning function and using a closure which contains the structure itself; what is your preferred method?
P.S.: (the original version of blurArray uses int[] as input, I have just modified into float[] to use List.average in my version)
I think a nice and more functional alternative would be to use Array.init
. This function lets you create an array by specifying a function that is used to calculate the element for each location.
This still looks very much like the original code, but it now does not need any explicit mutation (this is now hidden in Array.init
). In fact, I'd probably use this in a revised version of the book :-).
let blurArray (arr:float[]) =
Array.init arr.Length (fun i ->
if i = 0 then (arr.[0] + arr.[1]) / 2.0
elif i = arr.Length - 1 then (arr.[arr.Length - 2] + arr.[arr.Length - 1] )/2.0
else (arr.[i-1] + arr.[i] + arr.[i+1]) / 3.0 )
Next, you could decide that there are more functions you want to write where you do something with the neighbourhood of each point in the array - and you may want to specify the neighbourhood size. You could write something like:
let fillArray offset f (arr:float[]) =
Array.init arr.Length (fun i ->
f arr.[max 0 (i-offset) .. min (arr.Length-1) (i+offset)])
Here, the function f
is called with a sub-array of neighbours of each point with at most offset
neighbours to the left & right (this does not get out of bounds thanks to the max
and min
checks). Now you can write blur as:
arr |> fillArray 1 Seq.average
In the fillArray
, creating the sub-array will be a bit inefficient - you could probably make it faster by using ArraySegment
or by copying relevant parts of the array to a local mutable array. But it does look quite nice and functional!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With