Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# functional coupled iteration: performance issue and preferred functional style

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)

like image 306
pacta_sunt_servanda Avatar asked Mar 18 '17 17:03

pacta_sunt_servanda


1 Answers

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!

like image 160
Tomas Petricek Avatar answered Oct 01 '22 03:10

Tomas Petricek