10 Tips for Productive F# Scripting06 Feb 2016
Scott 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
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 >
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 our
add 1 2example, the result showed up as
it. We simply ran add, but didn’t assign the result to anything.
itnow 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 instance
let 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,
xwill remain available, until I run for instance
let x = 42;;). This is extremely convenient: for instance, you can load a data file once
let data = File.ReadAllLines path, and keep using
datafor 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 as
val 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.exeis typically located at
C:\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:
Pre-pending a string with
@makes it a verbatim string, and ignore escape sequences, such as
\, 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…
… 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 Systemin 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
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:
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:
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 @"../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 "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
#loadstatement: “One or more errors in loaded files. The namespace or module … is not defined”. Simply reference the missing assemblies above the
#loadstatement, 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?
Have a comment or a question? Ping me on Twitter, or use the comments section!