Timing code using computation expressions

While building a link checking tool, I wanted to log the time it took to check a link. For demonstration purposes, we’ll define the link checking function as follows:

let linkIsValid () =
  // Simulate link checking by sleeping for a second
  System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1.))

  true

To measure this function’s execution time, we’ll use the Stopwatch class, which is designed specifically for this purpose:

// This namespace contains the Stopwatch class
open System.Diagnostics

// Create the stopwatch and immediately start measuring time
let stopwatch = Stopwatch.StartNew()

// Execute the function we want to time
let isValid = linkIsValid()

// Stop measuring time
stopwatch.Stop()

// Output the measured (elapsed) time
printfn "%A" stopwatch.Elapsed

If we run this code, the output will look like this:

00:00:01.0155117

To allow re-using the link timing functionality, we’ll encapsulate the above code in a function which returns the link check result and the elapsed time as a tuple:

let linkIsValidWithTiming() =
  let stopwatch = Stopwatch.StartNew()
  let linkStatus = linkIsValid()
  stopwatch.Stop()

  // Return the link status _and_ the elapsed time
  linkStatus, stopwatch.Elapsed

let isValid, elapsed = linkIsValidWithTiming()
printfn "%A" elapsed

Generalizing

What if we want to mesure the execution time of other functions? Well, we can generalize the linkIsValidWithTiming function to work with other functions by taking the function to time as a parameter:

let executeAndTime func =
  let stopwatch = Stopwatch.StartNew()

  // Call the `func` parameter (which is a function)
  let funcResult = func()

  stopwatch.Stop()

  funcResult, stopwatch.Elapsed

We can then pass in the linkIsValid function as an argument to get its result and its timing information:

let isValid, elapsed = executeAndTime linkIsValid
printfn "%A" elapsed

Timing type

To make our code more explicit, let’s define a type to hold timing results:

type Timed<'T> =
  { Result: 'T
    Elapsed: TimeSpan }

We can then return this type from our executeAndTime function:

let executeAndTime func =
  let stopwatch = Stopwatch.StartNew()
  let result = func()
  stopwatch.Stop()

  { Result = result; Elapsed = stopwatch.Elapsed }

let timedResult = executeAndTime linkIsValid
printfn "%A" timedResult.Elapsed

To me, this version has better motivational transparency 1 as it makes it more explicit what the code is doing.

Timing asynchronous functions

To allow for non-blocking link checking, let’s define an asynchronous version of the linkIsValid function:

let linkIsValidAsync () = async {
  do! Async.Sleep(1000)
  return true
}

If we time this function, the elapsed time seems to be off:

00:00:00.0010091

As the linkIsValidAsync function should run for at least 1000 milliseconds, this can’t be right. The problem is that we don’t wait for the async function to complete. This means that we’re not timing the execution of the function from begin to end, but only the time it takes to start executing. To ensure that the asynchronous function call blocks until it has completed, we can pipe it into Async.RunSynchronously:

let executeAndTime func =
  let stopwatch = Stopwatch.StartNew()
  let funcResult = func() |> Async.RunSynchronously
  stopwatch.Stop()

  { Result = funcResult; Elapsed = stopwatch.Elapsed }

Our asynchronous function is now timed correctly:

00:00:01.0390756

However, if we try to time the old, synchronous linkIsValid function, we get a compile error:

[FS0001] Type mismatch. Expecting a
  'unit -> Async<'a>'
but given a
  'unit -> bool'
The type 'Async<'a>' does not match the type 'bool'

The compiler informs us that Async.RunSynchronously expects an Async<'a> argument, but that it received a bool argument, which is indeed what the (synchronous) linkIsValid function returns.

To fix this, let’s revert back to our previous, synchronous version of the executeAndTime function (by removing Async.RunSynchronously). We can then pass in a lambda as the argument where we pipe the linkIsValidAsync result to Async.RunSynchronously:

let timedResult = executeAndTime (fun() -> linkIsValidAsync() |> Async.RunSynchronously)

Using this approach, we can time both synchronous and asynchronous functions, without the executeAndTime function having to be aware of the difference. The output confirms that we correctly time our asynchronous function:

00:00:01.0390756

Computation Expression

While the function-based approach works well, wouldn’t it be great if we could this instead?

let timedResult = timed {
  return linkIsValid()
}

This code looks very similar to the async syntax we used before in our linkIsValidAsync function:

let linkIsValidAsync () = async {
  do! Async.Sleep(1000)
  return true
}

As it turns out, the async syntax is provided by an F# feature called computation expressions, which provides a convenient syntax for computations that can be sequenced and combined. Besides the built-in computation expressions (like async), one can also define custom computation expressions, so let’s try and build a timed computation expression.

Computation expressions are implemented as classes, which are known as builder types. To get the nice syntax we saw above, one has to bind an instance of the builder type to an identifier. The name of the identifier determines how one can use the computation expression in code.

// Empty build class
type TimedBuilder() = class end

// Builder class instance. The name determines how to use the computation expression
let timed = TimedBuilder()

This will allow us to do:

let timedResult = timed {}

If we try to compile this code, we get can error:

[FS0003] This value is not a function and cannot be applied.

While the message is slightly cryptic, it tries to tell us that we’re not doing anything in our computation expression. This starts to make sense if you realize that computation expressions are translated to method invocations on the builder type instance (in our case: timed). Let’s try to mimic the async computation expression and return the result of calling the linkIsValid function:

let timedResult = timed {
  return linkIsValid()
}

This time, we get the following compile error:

[FS0708] This control construct may only be used if the computation expression builder defines a 'Return' method

This is actually very descriptive! It tells us that to be able to use the return keyword inside our custom computation expression, its builder (type) needs to define a Return method. If we check the documentation, the Return method should be of type 'T -> M<'T>. You should read this as: it takes a “regular” type and returns a computation expression-specific type that “wraps” the “regular” type. For our computation expression, our “wrapped” type is the Timed<'T> type. Let’s add the Return member to the TimedBuilder class:

type TimedBuilder() =
  member x.Return(value) = { Result = value; Elapsed = TimeSpan.Zero }

We can then use this as follows:

let timedResult = timed {
  return linkIsValid()
}
printfn "%A" timedResult.Elapsed

This time, the code runs successfully and outputs:

00:00:00

Let’s try and time the function execution:

type TimedBuilder() =
  member x.Return(value) =
    let stopwatch = Stopwatch.StartNew()
    let result = value()
    stopwatch.Stop()

    { Result = result; Elapsed = stopwatch.Elapsed }

As with our executeAndTime function, the Return method now expects its parameter to be a function, which means that the computation expression should not return the result of invoking the linkIsValid function, but return the function itself:

let timedResult = timed {
  return linkIsValid
}

And this works! Nice and clean, right?

Improved function handling

Our current implementation is not idiomatic though. When executing functions in a computation expression, the Delay member should be implemented. Its signature is (unit -> M<'T>) -> M<'T>, which means that it takes a function without parameters that returns the “wrapped” type and also returns the “wrapped” type. Let’s implement this method:

type TimedBuilder() =
  member x.Delay(func) =
    let stopwatch = Stopwatch.StartNew()
    let timedResult = func()
    stopwatch.Stop()

    { timedResult with Elapsed = timedResult.Elapsed + stopwatch.Elapsed }

The time measuring code is the same, the biggest change is that when we invoke the func parameter, we receive a Timed<'T> value. We then deconstruct this value and return a new Timed<'T> instance, but with the elapsed time as measured by the stopwatch added to the elapsed time returned by the function. If we try to compile this, we get:

[FS0708] This control construct may only be used if the computation expression builder defines a 'Return' method

Whoops. We’re still using the return keyword in our computation expression invocation, which means that we still have to implement the Return method. Note though that any value passed to the Return method is automatically wrapped in a “delay” function and passed to the Delay method to execute. We can use our original Return method implementation:

type TimedBuilder() =
  member x.Return(value) = { Result = value; Elapsed = TimeSpan.Zero }

  member x.Delay(func) =
    let stopwatch = Stopwatch.StartNew()
    let timedResult = func()
    stopwatch.Stop()

    { timedResult with Elapsed = timedResult.Elapsed + stopwatch.Elapsed }

We’ll have to change our computation expression to actually invoke the linkIsValid function again:

let timedResult =
  timed {
    return linkIsValid()
  }

And we’re back to things working. Note that we can now also return a constant value from our computation expression (although that admittedly doesn’t make much sense):

let timedResult =
  timed {
    return 2
  }

And with that, we have created a computation expression that elegantly allows us to measure function execution time using a nice and concise syntax.

Conclusion

Computation expressions are a great way to help make your code more expressive. While writing a computation expression takes a little getting used to, they are not that hard once you’re familiar with their implementation rules. The compiler is also quite helpful, outputting detailed error message that help pinpoint what methods to implement.