Using F# scripts for interactive development

In a previous post we discussed some of the basics of scripting in F#.

Using script files and F# Interactive (FSI) allows interactive development which is the kind of scripting that we'll discuss in this post.

The interactive development workflow

A typical scripting session might look like this:

  1. Write an expression into an editor, such as Visual Studio or Visual Studio Code.
  2. Evaluate it by sending it to FSI, and keep adjusting until it returns the value you expect.
  3. Convert the expression into a function and send it to FSI.
  4. Write another expression where you call the new function.
  5. Send the new expression to FSI and check it returns the correct value.

Note that this example uses a single FSI session, which stores the state of any let bindings throughout.

A detailed example with the SAFE template

Let's look at how to add support for interactive development in a new SAFE template project.

Add a new Script.fsx file inside the Server project. In this file, load the Shared.fs code file:

1: 
#load "src/Shared/Shared.fs"

If we send this to FSI, we can now use the code in Shared.fs. Let's create a counter:

1: 
2: 
3: 
open Shared

let counter = { Value = 10 }

When we send this to FSI it returns val counter : Counter = {Value = 10;}, confirming the value has been created and named counter.

Suppose we want to create a function to double the value of a counter. We might start by writing an expression that works with our current counter:

1: 
{ counter with Value = counter.Value * 2 }

We can run this expression without assigning it to a value, just to see what the result is. FSI returns val it : Counter = {Value = 20;}.

Now that we're happy with this code we can generalise it into a function with a name:

1: 
2: 
let doubleCounter c =
    { c with Value = c.Value * 2 }

We can test this function out with a few examples:

1: 
2: 
3: 
4: 
5: 
counter |> doubleCounter // {Value = 20;}

{ Value = 3 } |> doubleCounter // {Value = 6;}

{ Value = 8 } |> doubleCounter // {Value = 16;}

The comments above show the value returned by FSI. It looks like our function works just as we expect. At this stage, we might choose to copy the function over into our real application code.

Using application dependencies during interactive development

If you want to write code that uses external dependencies, you can load them into an FSI session. The easiest way to do this in the context of a project is to use a Paket feature which generates load scripts. This assumes that Paket is used to manage dependencies, as it is in the SAFE template.

Let's add a line to the paket.dependencies file inside the Server group:

1: 
2: 
group Server
    generate_load_scripts: true

Now when we run paket install, Paket will generate scripts in the .paket\load directory. We can load the script that loads all dependencies for the Server group by adding this line at the top of our script file:

1: 
#load ".paket/load/netcoreapp2.2/Server/server.group.fsx"

When we run this line, all of the server dependencies are loaded into FSI and we can add code that uses an external library. For example, let's verify the string output when we use Thoth to verify our previously defined counter.

1: 
2: 
3: 
open Thoth.Json.Net

Encode.Auto.toString(0, counter) // """{"Value":10}"""

And we can also check that the round-trip encoding and decoding results in the starting value:

1: 
2: 
Encode.Auto.toString(0, counter)
|> Decode.Auto.fromString<Counter> // Ok {Value = 10;}

Scripting vs Testing

F# scripts to some extent compete with test-driven development as a way to do interactive development. However, scripts are not a complete replacement for tests. There are cases where unit tests or integration tests are still preferable.

Some things that scripts are good for compared to tests:

  • Exploring possible ways of writing some code before deciding what the API is.
  • Loading in larger amounts of data and exploring it and how it interacts with some code.
  • Testing interactions with external services or databases. It might be difficult create automated tests for these, and sometimes these tests can be brittle and produce false positives.

Some things that tests are good for compared to scripts:

  • Provide up-to-date documentation on the expected behaviour of functions using specific examples.
  • Helping to ensure regressions aren't introduced in difficult areas of code.

This makes tests more suitable for well-established areas of code with more complicated requirements, whereas scripts are more useful for quickly exploring code and data in earlier stages of development.

Summary

We have seen that F# scripts are valuable for interactive development and how we might actually use scripts in a simplified example with application code and with external dependencies. We've also seen when to use scripts instead of tests and vice versa.

To get the best use of scripting in F# apply the techiques above and consider what will fit best for your scenarios.