United Kingdom: +44 (0)208 088 8978

Seven tips for working with F# scripts

We're hiring Software Developers

Click here to find out more

Scripts are often useful for testing out isolated parts of your application without the need to create a test console rig / webpage, and can create a much quicker feedback cycle. In this two-part series, we'll cover some the basics and some more advanced tips for working effectively with scripts in F#.

Scripting basics

In F#, aside from normal .fs source files, we have .fsx script files. Script files can be run from a command line using F# Interactive (FSI). When you run FSI in this way, it loads the script, compiles the code in the script, runs the code and then exits.

One of the most common uses for scripts in F# projects is for build automation, for example using the FAKE build tool.

However, another common way to use F# scripts is to "send" individual segments of code to FSI from within your code editor. Although strictly speaking this is using FSI, and doesn't need to be done from a script file, it is usually referred to as scripting in F#.

To get the best use of scripting in F# make sure you use the tips mentioned here. Happy scripting! 😁

1. Don't type directly into FSI

Sometimes beginners will start to write code directly into the FSI command prompt, which seems quite natural since the command prompt is there. However, working this way has several limitations:

  • You won't get any auto-completion
  • You won't see compiler errors until you finish typing a section of code
  • You have to remember to type ;; at the end of your code.

It's much more effective to write code into a script file and then select parts of it to "send" to FSI: After highlighting some code, you can send it to FSI with Alt-Enter. After that, if you don't want to keep the line of code then just delete it. This also has the benefit of allowing you to reuse and share your script code in the future.

2. Keep your script code compiling

Code that compiles correctly is easier to understand, modify and add to. Type inference works better, and you won't have compounding errors distracting you from the real and relevant errors in the area you're currently working on.

As much as possible you should aim to keep your script code compiling correctly, even though it may not be strictly necessary to evaluate expressions and get the result you're looking for. This generally applies even if your code is temporary and will be thrown away.

3. Use Restart / Reset FSI where needed

As you send code to FSI during your scripting session, the state of the functions and values will change over time. After a while, you can lose track of this state or you can get into a bad state because of changing types and functions.
Sometimes, this can be resolved by re-sending all the relevent code to FSI, but sometimes you may find you'll need to blow away all of the state by restarting FSI completely and starting from scratch.

If you organise your scripting code sensibly this shouldn't be too much of a setback and the main downside will be that there will be a short wait to re-load code, dependencies and data.

4. Consider using #r rather than #load

The most common way of working with scripts against production code is to use the #load directive to import the code of an .fs file into the FSI session. This allows you to quickly make changes to production code and to reload them into the FSI session. However, using #load on larger projects can be difficult to maintain, as well as slow to load all the files. In such cases, consider switching to loading in the compiled DLL into your script instead. This will mean that instead of having to load in multiple F# files in the correct order etc., you can simply reference a single DLL. Just remember to always rebuild the DLL when you make changes to the code in it before referencing it!

5. Use Paket to load dependencies

Paket provides great support for loading dependencies through its "generate-load-scripts" function. Simply add this directive to your dependencies file, and Paket will create #load scripts for all your dependencies (as well as a "global" load file that loads everything in one go). This is also excellent for scratchpad / exploratory programming when trying out new Nuget packages - and even works when you utilise Paket's NuGet cache support.

6. Prefer using pure functions

Try to work with pure functions that have simple inputs and outputs. These are not only easier to compose, but they generally require less setup and are simpler to reason about. Doing this will make your scripts quicker to create, easier to work with and less effort to maintain.

7. Consider using helper scripts

You may end up with multiple scripts that are used against different parts of your codebase. Consider creating helper scripts that contain re-usable helper logic, such as setting up data to an initial known state, loading in data that can be re-used across scripts, mocks of datastores etc. This will allow you to create several small script that are each focused on one specific area on the real code under exploration, rather than all of them containing boilerplate setup at the start.
Hope you enjoyed this post! In the second part of our scripting series, we'll cover more advanced use cases for scripts.