10 Tips for Productive F# Scripting
06 Feb 2016Scott Hanselman recently had a nice post on C# and F# REPLs, which reminded me of the time I started using F# scripts. Over time, I found out a couple of small tricks, which helped make the experience productive. I found about them mainly by accident, so I figured, let’s see if I can list them in one place! Some of these are super simple, some probably a bit obscure, but hopefully, one of them at least will make your path towards scripting nirvana an easier one…
Note: these tips are not necessarily ordered by usefulness. For that matter, there might or might not be exactly 10 of them :)
Tip 1: Use .fsx
Files for Interactive Coding
You can use the F# Interactive 2 ways: you can directly type code into FSI, the F# Interactive window, or you can write code in an .fsx
file, and select pieces of the code you want to execute. I recommend the second approach, for at least two reasons. First, FSI is a very primitive environment, .fsx
files provide a much richer experience (IntelliSense). Then this encourages writing clean scripts you can reuse later.
This is not specific to scripts, but… if you are on Visual Studio, do yourself a service and install the Visual F# Power Tools - you’ll get nice things such as better code highlighting, refactoring, and more.
To execute code interactively, simply type code in an .fsx
file, select a block of code, and hit Alt + Enter. The selected code will be evaluated, and the result will show up in the FSI window. In Visual Studio, you can also select code and right-click “Execute in Interactive”, but shortcuts are way faster.
You can also execute a single-line with Alt + '. I rarely use this option, but this can save you time because you don’t need to select the entire line of code.
In case the keyboard shortcuts to send code to FSI do not work anymore (ReSharper used to over-write them in the past), you can reset them in Visual Studio, by going to Tools / Options / Environment / Keyboard. The 2 commands you need to map are EditorContextMenus.CodeWindow.ExecuteInInteractive and EditorContextMenus.CodeWindow.ExecuteLineInInteractive.
You can also use these shortcuts from a regular .fs
file, which can be handy if you want to validate that a piece of code is behaving the way you want.
Interactive coding is by far my main usage for scripts - I use it extensively to prototype designs, run dumb tasks, or explore data or libraries. I realized recently that a few of my C# friends use LinqPad for the same purpose.
Tip 2: What is it
?
While I encourage working primarily from .fsx
files, the FSI window is also very helpful. I use it primarily for small verifications. For instance, I might have in my script file code like this:
let add x y =
x + y
Once I send it for evaluation into FSI, I will see the following show up in FSI:
val add : x:int -> y:int -> int
>
My function add
is now in memory, in my FSI session; I can start typing in the FSI window and use it:
> add 1 2;;
val it : int = 3
>
Enter does not trigger execution in FSI. The ;;
indicates to FSI “Please execute everything I just typed, up to that point”. This is useful if you want to type multiple lines of code in FSI, and execute them as a block.
it
: in ouradd 1 2
example, the result showed up asit
. We simply ran add, but didn’t assign the result to anything.it
now contains the result, until we run another expression. If you want to re-use that value, you can assign it in FSI, by doing for instancelet x = it;;
.
Once a value is loaded in your FSI session, it will remain there, available to you until you shadow it (in the example above,
x
will remain available, until I run for instancelet x = 42;;
). This is extremely convenient: for instance, you can load a data file oncelet data = File.ReadAllLines path
, and keep usingdata
for as long as you want, without having to reload it between code changes.
FSI often shows an abbreviated version of values for large items. For instance,
[1..999]
will show up asval it : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15; 16; 17; 18; 19; 20; 21; 22; 23; 24; 25; 26; 27; 28; 29; 30; 31; 32; 33; 34; 35; 36; 37; 38; 39; 40; 41; 42; 43; 44; 45; 46; 47; 48; 49; 50; 51; 52; 53; 54; 55; 56; 57; 58; 59; 60; 61; 62; 63; 64; 65; 66; 67; 68; 69; 70; 71; 72; 73; 74; 75; 76; 77; 78; 79; 80; 81; 82; 83; 84; 85; 86; 87; 88; 89; 90; 91; 92; 93; 94; 95; 96; 97; 98; 99; 100; ...]
- note the … at the end, which indicate that there is more.
What if you inadvertently started a very long computation, or an infinite loop? In Visual Studio, you can either kill the session entirely, by right-clicking over the FSI window and selecting “Reset Interactive Session” or Ctrl + Alt + R, or cancel the latest evaluation you requested (“Cancel Interactive Evaluation”, or Ctrl + Break.).
Tip 3: Run Scripts from the Command Line
Besides interactive scripting, you can also run a script from the command line, by using FSI.exe
:
>fsi.exe "C:\myscript.fsx"
FSI.exe
is typically located atC:\Program Files (x86)\Microsoft SDKs\F#\4.0\Framework\v4.0
. You can also install it separately, see fsharp.org/use section for instructions for various platforms.
You can define different behaviors in your script, depending on whether it is run interactively or from the command line, like this:
#if INTERACTIVE
let msg = "Interactive"
#else
let msg = "Not Interactive"
#endif
printfn "%s" msg
Updated, Sep 19: thanks Matt Klein for pointing the issue.
For more information on FSI from the command line, check the reference page here.
Updated, Feb 20: Ramon Soto Mathiesen points out that Tip 9 also applies to the command line.
Tip 4: Use Relative Paths
Sometimes, your script will reference another resource; for instance, you need to read the contents of a .txt
file somewhere. You can use absolute path, as in:
File.ReadAllLines @"C:/data/myfile.txt"
Pre-pending a string with
@
makes it a verbatim string, and ignore escape sequences, such as\
.
Use
/
rather than\
, so that path work both on Windows and Mono.
However, if that resource lives in a location relative to your script, consider using relative path, so that you can move your script folder around without breaking it.
Relative paths can be a bit tricky; for instance, running the following code interactively…
System.Environment.CurrentDirectory
… produces a potentially unexpected result in FSI:
val it : string = "C:\Users\Mathias Brandewinder\AppData\Local\Temp"
>
You can avoid these issues by using built-in constants, which refer respectively to the directory where the script lives, the script file name, and the current line of the script:
__SOURCE_DIRECTORY__
__SOURCE_FILE__
__LINE__
So if your folder structure was along these lines…
root
/src/script.fsx
/data/data.txt
… you could refer to the data file data.txt
from your script like this:
let path = System.IO.Path.Combine(__SOURCE_DIRECTORY__,"..","data/data.txt")
System.IO.File.ReadAllText path
Tip 5: Including Assemblies
By default, FSI loads FSharp.Core
and nothing else. If you want to use System.DateTime
, you will need to first open System
in your script. If you want to use an assembly that is not part of the standard .NET distribution, you will need to reference it first using #r
. Imagine for instance that you installed the Nuget package fsharp.data
; to use it in your script, you would do something like:
#r @"../packages/FSharp.Data.2.2.5/lib/net40/FSharp.Data.dll"
open FSharp.Data
When you execute
open System
in interactive, don’t worry if nothing seems to happen: the only result is a new>
showing up in FSI.
For assemblies that are part of .NET but not referenced by default, you can use a shorter version:
#r @"System.Xaml"
open System.Xaml
In Visual Studio, you can right-click a reference from Solution Explorer, and send to F# interactive. You can then directly open it, and start using it in FSI.
Updated, Feb 20: Sergey Tihon shared an interesting comment, explaining where Tip 5 can sometimes go wrong. I’d say, try Tip 5 first, but be aware that this might at times not quite work:
@brandewinder don't load assemblies like in Tip 5 ) https://t.co/Owft1NmPoo
— Sergey Tihon (@sergey_tihon) February 7, 2016
Updated, Feb 20: F# open source contributor Don Syme share a related nice trick:
@jeroldhaas @sergey_tihon @brandewinder Use #I __SOURCE_DIRECTORY__, it is wondrous, very satisfying. All relative paths then work
— Don Syme (@dsyme) February 7, 2016
Tip 6: Use Paket
The Nuget package manager is useful to consume existing packages. However, by default, Nuget stores assemblies in a folder that includes the package version number. This is very impractical for a script. In our example above, if fsharp.data
gets an update, our script reference will be broken once we update the Nuget package:
#r @"../packages/FSharp.Data.2.2.5/lib/net40/FSharp.Data.dll"
Fixing the script requires manually editing the version number in the path, which quickly becomes a pain. Paket provides a better experience, because it stores packages without the version number, in this case, under:
#r @"../packages/FSharp.Data/lib/net40/FSharp.Data.dll"
Your scripts will now gracefully handle version number changes.
If you end up consuming numerous packages, you can make your life even easier, by referencing paths where assemblies might be searched for, using #I
:
#I @"../packages/
#r @"FSharp.Data/lib/net40/FSharp.Data.dll"
If your primary goal is to “just script”, consider using Atom or VSCode, with the Ionide plugin. You can create and run free-standing F# scripts, with beautiful Paket integration.
Tip 7: Include Files
You might want to use the code from an existing file in your script. Suppose that we have a code file Code.fs
somewhere, looking like this:
namespace Mathias
module Common =
let hello name = sprintf "Hello, %s" name
You can use that code from your script, by using the #load
directive:
#load "Code.fs"
open Mathias.Common
hello "World"
You might have to close and re-open the script file if you end up changing the contents of the file.
If the file you are attempting to load contains references to other assemblies or files, you might get an error on the
#load
statement: “One or more errors in loaded files. The namespace or module … is not defined”. Simply reference the missing assemblies above the#load
statement, so that your script uses the same dependencies as the file it refers to.
Tip 8: Profile your Code with #time
Another handy directive, #time
, turns on basic profiling. Once it is executed, for every block of code you send for execution you will see timing and garbage collection information. For instance, running this code…
#time
[| 1 .. 10000000 |] |> Array.map (fun x -> x * x)
… will produce the following in FSI:
--> Timing now on
Real: 00:00:00.887, CPU: 00:00:00.828, GC gen0: 2, gen1: 2, gen2: 2
val it : int [] =
[|1; 4; 9; 16; 25; 36; 49; // snipped for brevity
We get the wall time and CPU time it took, as well as some information about garbage collection in generations 0, 1 and 2. This would not replace a full-blown profiler, but this is an awfully convenient tool to figure out quickly if there are obvious ways to improve a piece of code.
Note that every time you execute #time
, the timer will be switched from on to off, or vice-versa. This is not always convenient; you can also explicitly set it to the desired state, like this:
#time "on"
// everything now is timed
#time "off"
If you are interested in profiling, you should take a look at PrivateEye; check out Greg Young’s talk at NDC Oslo 2015 to get a feel for what it does.
Tip 9: Turn 64-bits on
Hat tip to Rick Minerich for that one. I’ll refer you to his blog post to see how to set FSI to 64 bits to handle large datasets.
Tip 10: Bonus Material
Did you know that you could…
- debug an F# script? (around 0:12:35 in)
- inspect the objects in your FSI session with FsEye?
- change the FSI font size in Tools/Options/Environment/Fonts and Colors/Show Settings for/F# Interactive?
- add your own pretty-printer to FSI, like this?
- mess with your coworkers’ mental sanity, by executing
(*
(opening a multiline comment) in FSI? (credit: Tomas) - simplify loading references with Visual Studio and Power Tools? (credit: Kit Eason, see details in comments section).
And again… if you are not using the Visual F# Power Tools, you are missing out:
"Don't let your friends try #fsharp without installing @FSPowerTools." @dsyme at #ndclondon
— Tomas Petricek (@tomaspetricek) January 15, 2016
That’s what I got! I am sure I forgot some - do you have a useful or favorite trick to share?