Developing a functional pipe for the average score function

First, let's recap on how the average_score function was written:

function average_score(n = 10)
story_ids = fetch_top_stories()
println(now(), " Found ", length(story_ids), " stories")

top_stories = [fetch_story(id) for id in story_ids[1:min(n,end)]]
println(now(), " Fetched ", n, " story details")

avg_top_scores = mean(s.score for s in top_stories)
println(now(), " Average score = ", avg_top_scores)

return avg_top_scores
end

Although the code looks quite decent and simple to understand, let me point out some potential issues:

  • The top stories are retrieved via array comprehension syntax. The logic is a little busy and we won't be able to test this part of the code independently from the average_score function.
  • The println function is used for logging, but we seem to be replicating the code to display the current timestamp.

Now, we will refactor the code. The logic is largely linear, which makes it a good candidate for functional pipes. Conceptually, this is what we think about the computation:

It would be nice to design a function that works like this:

average_score2(n = 10) = 
fetch_top_stories() |>
take(n) |>
fetch_story_details |>
calculate_average_score

This is our second version of the same function, so we have named it average_score2.

For now, we just ignore the logging aspect to keep it simple. We will come back to this later. Since we have already defined the fetch_top_stories function, we just have to develop the other three functions, as follows:

take(n::Int) = xs -> xs[1:min(n,end)]

fetch_story_details(ids::Vector{Int}) = fetch_story.(ids)

calculate_average_score(stories::Vector{Story}) = mean(s.score for s in stories)

From the preceding code, we can see the following:

  • The take function takes an integer, n, and returns an anonymous function that returns the first n elements from an array.
  • The min function is used to ensure that it will take no more than the actual size of the array. 
  • The fetch_story_details function takes an array of story IDs and broadcasts the fetch_story function over them using the dot notation. 
  • The calculate_average_score function takes an array of Story objects and calculates the mean of the scores.

As a quick reminder, all of these functions accept a single argument as input so that they can participate in the functional pipe operation.

Now, let's get back to logging. Logging plays a funny role in functional pipes. It is designed to produce side effects and do not affect the result of computation. It is slippery in the sense that it just returns the same data that it received from its input. Since the standard println function returns nothing, we cannot use it directly in a piping operation. Instead, we must create a logging function that is smart enough to print what we want and yet returns the same data that it was passed.

In addition, we want to be able to format the output using the data that passes through the system. For that reason, we can utilize the Formatting package. It contains a flexible and efficient formatting facility. Let's build our own logging function, as follows:

using Formatting: printfmtln

logx(fmt::AbstractString, f::Function = identity) = x -> begin
let y = f(x)
print(now(), " ")
printfmtln(fmt, y)
end
return x
end

The logx function takes a format string and a possible transformer function, f. It returns an anonymous function that passes the transformed value to the printfmln function. It also automatically prefixes the log with the current timestamp. Most importantly, this anonymous function returns the original value of the argument.

To see how this logging function works, we can play with a few examples:

In the first example shown in the preceding screenshot, the logx function was called with just a format string, so the input coming via the pipe will be used as-is in the log. The second example passes the length function as the second argument of logx. The length function is then used to transform the input value for logging purposes.

Putting this all together, we can introduce logging to our functional pipe in our new average_score3 function, as follows:

average_score3(n = 10) = 
fetch_top_stories() |>
logx("Number of top stories = {}", length) |>
take(n) |>
logx("Limited number of stories = $n") |>
fetch_story_details |>
logx("Fetched story details") |>
calculate_average_score |>
logx("Average score = {}")

Occasionally, functional pipes can make the code easier to understand. Because conditional statements are not allowed in a piping operation, the logic is always linear.

You may be wondering how to handle conditional logic in functional pipe design. We'll learn about this in the next section.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.168.16