Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

F#  Module 1 – Start here: F# for .NET developers

Install .NET and run F# Interactive

Install the .NET SDK that carries F#, pin it for a folder with global.json, run dotnet fsi as a REPL and a script runner, and pull NuGet packages into scripts.

  • Beginner
  • 10 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Install the .NET SDK, confirm its F# compiler version and pin the SDK for a folder with global.json
  • Use dotnet fsi as a REPL and as a script runner, with ;; and the
  • Add NuGet packages to scripts with exact versions and explain what happens on the first run
  • Set up an editor, VS Code with Ionide, Visual Studio or Rider, for F# scripts and projects

Before you start

What you’ll learn · Before you start

What you will learn

  • Install the .NET SDK, confirm its F# compiler version and pin the SDK for a folder with global.json
  • Use dotnet fsi as a REPL and as a script runner, with ;; and the
  • Add NuGet packages to scripts with exact versions and explain what happens on the first run
  • Set up an editor, VS Code with Ionide, Visual Studio or Rider, for F# scripts and projects

Before you start

On this page

Installing F# means installing one thing, the .NET SDK: The kit you install to build .NET code: the dotnet command, MSBuild, NuGet, the C# and F# compilers, F# Interactive and a .NET runtime. Without it, dotnet fsi and dotnet build cannot run.: SDK 10.0.401 carries the F# compiler, F# Interactive: The REPL and script runner of the .NET SDK, started with dotnet fsi. At the prompt it compiles and runs each entry that ends with ;; and answers with the name, type and value. and a copy of FSharp.Core 10.1.401. On the computer that recorded this lesson, a one-line script then took 2.5 to 3.6 seconds from the command to the end of its output, nearly all of it starting .NET and compiling the script: a cost that shapes how you use scripts.

This lesson installs and checks the SDK, pins it for a folder, uses F# Interactive both as a prompt you type at and as a script runner, pulls a NuGet package into a script with an exact version, and sets up an editor.

The .NET SDK is the only thing to install

Microsoft’s install page for F# offers three routes. On a computer where you build from the command line or with VS Code, install the .NET SDK from Microsoft’s download page or your platform’s package manager, as the .NET install guides for Windows, macOS and Linux describe. With Visual Studio, F# comes with the ASP.NET and web development workload. On a build server, the page says, the .NET SDK has everything you need. Install the .NET 10 SDK: .NET 10 is the long-term support release this track uses.

Then check what you got. These are the checks on the recording computer, in a folder whose global.json pins SDK 10.0.401 (DOTNET_ROOT is the folder the SDK is installed in):

What the SDK installed, checked from a terminal Python · check_session.py
from session import run

# The checks after installing the .NET SDK: the SDK dotnet picks in this folder, every SDK it could pick, the F#
# Interactive inside it, and the F# files the SDK folder holds (DOTNET_ROOT is the folder the SDK is installed in).
run("""
dotnet --version
dotnet --list-sdks
dotnet fsi --help | head -n 1
ls "$DOTNET_ROOT/sdk/10.0.401/FSharp" | grep -E '^(fsc|fsi|FSharp[.]Core)[.]dll$'
ls "$DOTNET_ROOT/sdk/10.0.401/FSharp/library-packs"
""")

Output

$ dotnet --version
10.0.401
$ dotnet --list-sdks
10.0.401 […/dotnet-root/sdk]
$ dotnet fsi --help | head -n 1
Microsoft (R) F# Interactive version 15.2.401.0 for F# 10.0
$ ls "$DOTNET_ROOT/sdk/10.0.401/FSharp" | grep -E '^(fsc|fsi|FSharp[.]Core)[.]dll$'
fsc.dll
FSharp.Core.dll
fsi.dll
$ ls "$DOTNET_ROOT/sdk/10.0.401/FSharp/library-packs"
FSharp.Core.10.1.401.nupkg

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_session.py

dotnet --version prints the SDK that dotnet picks in this folder and --list-sdks every SDK installed. The first line of dotnet fsi --help names F# Interactive and the language it compiles, F# 10.0. The last two commands show that F# lives inside the SDK folder: fsc.dll is the compiler that builds projects, fsi.dll is F# Interactive, and library-packs holds the FSharp.Core 10.1.401 package, so a new F# project finds FSharp.Core without downloading it.

Installing only the runtime

Wrong: installing the .NET runtime alone, which can run built programs but has no compiler. Right: install the SDK, which includes a runtime as well. Without an SDK, dotnet fsi and dotnet build have nothing to run, because both F# Interactive and the compiler are part of the SDK.

global.json pins the SDK for a folder, scripts included

Several SDKs can be installed side by side, and by default every dotnet command uses the newest one. A global.json: A JSON file that pins the .NET SDK for its folder and every folder below it. dotnet looks for one in the current folder and each parent, and its rollForward value says how strictly the version must match. file chooses another for its folder and every folder below it: dotnet looks for one in the current folder, then in each parent, and the first it finds decides. The F# 6 notes add that dotnet fsi follows it too, so a script is checked and run by the SDK the folder pins. This session writes one with dotnet new globaljson, then pins a folder to an SDK that is not installed:

Pin a folder to an SDK with global.json Python · pin_session.py
from session import run

# global.json pins the SDK for a folder and every folder below it, for dotnet fsi as well as for builds.
# mywork pins the installed SDK; pinned-ahead asks for 10.0.500, which is not installed here.
run("""
mkdir mywork pinned-ahead
cd mywork
dotnet new globaljson --sdk-version 10.0.401 --roll-forward latestPatch
cat global.json
dotnet --version
cd ../pinned-ahead
dotnet new globaljson --sdk-version 10.0.500 --roll-forward disable
!dotnet fsi --help
""", terminal=True)

Output

$ mkdir mywork pinned-ahead
$ cd mywork
$ dotnet new globaljson --sdk-version 10.0.401 --roll-forward latestPatch
The template "global.json file" was created successfully.
$ cat global.json
{
  "sdk": {
    "rollForward": "latestPatch",
    "version": "10.0.401"
  }
}
$ dotnet --version
10.0.401
$ cd ../pinned-ahead
$ dotnet new globaljson --sdk-version 10.0.500 --roll-forward disable
The template "global.json file" was created successfully.
$ dotnet fsi --help
The command could not be loaded, possibly because:
  * You intended to execute a .NET application:
      The application 'fsi' does not exist or is not a managed .dll or .exe.
  * You intended to execute a .NET SDK command:
      A compatible .NET SDK was not found.

Requested SDK version: 10.0.500
global.json file: pinned-ahead/global.json

Installed SDKs:
10.0.401 […/dotnet-root/sdk]

Install the [10.0.500] .NET SDK or update [pinned-ahead/global.json] to match an installed SDK.

Learn about SDK resolution:
https://aka.ms/dotnet/sdk-not-found
[exit status 155]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pin_session.py

In mywork the pin is met and dotnet --version prints 10.0.401. In pinned-ahead nothing matches 10.0.500, so even dotnet fsi --help stops with exit status 155 and names the global.json that asked for it. How strictly a pin matches is its rollForward value:

rollForward With "version": "10.0.401" Use it for
disable exactly 10.0.401, nothing else reproducing a recorded output exactly
latestPatch the newest 10.0.4xx installed, 10.0.401 or later projects: patches apply, features stay put
latestFeature the newest 10.0.xxx installed, 10.0.401 or later projects that may take a newer feature band
left out patch: 10.0.401, else the newest 10.0.4xx the default when a version is given

Every examples folder of this track pins 10.0.401 with disable, so the recorded runs come from exactly that SDK. In your own projects prefer latestPatch: Microsoft supports only the newest servicing update, and a pin that refuses every patch also refuses its security fixes.

Key idea

One SDK carries everything F# needs, and a global.json in a folder decides which installed SDK every dotnet command there uses, dotnet fsi included.

Checkpoint

Check: What the .NET SDK holds · Pinning the SDK with global.json

  1. Question 1 of 2 Where did the F# compiler and F# Interactive of the recording computer come from?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: From the .NET SDK, whose FSharp folder holds fsc.dll and fsi.dll

    The .NET SDK carries F#: the compiler for projects, F# Interactive for scripts and the prompt, and a copy of the FSharp.Core package in library-packs.

  2. Question 2 of 2 In pinned-ahead, global.json asks for SDK 10.0.500 with rollForward disable, and only 10.0.401 is installed. What did dotnet fsi --help do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It stopped with exit status 155 and named the global.json that asked for 10.0.500

    global.json applies to every dotnet command in its folder and below, F# Interactive included. A pin that no installed SDK meets stops them all, with a message that names the file.

F# Interactive runs an entry once you end it with two semicolons

dotnet fsi with no file starts F# Interactive, a REPL: Read, evaluate, print loop: a prompt that reads an entry, runs it, prints the result and waits for the next one, so code can be tried a line at a time.: it reads an entry, compiles and runs it, prints the result and waits for the next. An entry ends with two semicolons, ;;; until you type them, Enter only starts a new line of the same entry, shown after a - prompt. Here is a first session:

A first session in F# Interactive Python · repl_session.py
from session import fsi

# A first session in F# Interactive: each entry ends with ;; and the answer names every value with its type.
fsi([
    "let price = 250;;",
    "let total qty =\n    price * qty;;",
    "total 4;;",
    "it + 1;;",
    "let price = 300;;",
    "total 4;;",
    "totl 4;;",
    "#quit;;",
])

Output

$ dotnet fsi
Microsoft (R) F# Interactive version 15.2.401.0 for F# 10.0
Copyright (c) Microsoft Corporation. All Rights Reserved.

For help type #help;;

> let price = 250;;
val price: int = 250

> let total qty =
-     price * qty;;
val total: qty: int -> int

> total 4;;
val it: int = 1000

> it + 1;;
val it: int = 1001

> let price = 300;;
val price: int = 300

> total 4;;
val it: int = 1000

> totl 4;;

  totl 4;;
  ^^^^

stdin(8,1): error FS0039: The value or constructor 'totl' is not defined. Maybe you want one of the following:
   total

> #quit;;

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 repl_session.py

Every answer is a val line with a name, the type F# Interactive inferred and the value. total 4 has no name of its own, so its value is bound to it, which the next entry used to get 1001. The typo totl stopped with error FS0039, and the message suggested total; the session carried on, because a failed entry changes nothing. Step through the six entries before it:

Interactive · message by message Six entries in F# Interactive, and what each one leaves behind Step through the session. Each entry travels to F# Interactive when it ends with ;; and comes back as a val line.
Six entries in F# Interactive, and what each one leaves behind, step 7 of 7. Messages so far: You to F# Interactive: let price = 250;;; F# Interactive to .NET runtime: compile and run; F# Interactive to You: val price: int = 250; F# Interactive to You: val total: qty: int -> int; F# Interactive to You: val it: int = 1000; You to F# Interactive: let price = 300;;; F# Interactive to You: val it: int = 1000.Youtyping at the > promptF# Interactivedotnet fsi.NET runtimeruns what it compiledlet price = 250;;compile and runval price: int = 250val total: qty: int -> intval it: int = 1000let price = 300;;val it: int = 1000

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. An entry ends with ;;. You type let price = 250 and end it with ;;. Until the two semicolons, pressing Enter only starts a new line of the same entry, shown after a - prompt.
  2. Compiled, then run. F# Interactive compiles the entry as the compiler would compile a file, then runs it at once.
  3. The answer names the type. It answers with a val line: the name, the type it inferred and the value. price is an int.
  4. A function is a value too. total uses price, so it keeps the price that exists now. Its val line gives its type: it takes an int called qty and returns an int.
  5. An unnamed result is it. total 4 has no name, so F# Interactive binds its value to it. The next entry, it + 1, gets 1001 from it.
  6. A new price hides the old. let price = 300 does not change the first price. It defines a new value with the same name, which every later entry sees. This is shadowing.
  7. total still sees 250. total 4 is still 1000, because total was compiled with the first price. To change what total computes, define total again after the new price.

The surprise is the last answer. let price = 300 did not change price: it defined a second value with the same name, which hides the first one from later entries. That is shadowing: Defining a new value with a name that is already in use. Later code sees the new value, while code compiled earlier keeps the value it was compiled with., and total was compiled with the first price, so it still answers 1000.

Redefining a value and expecting old functions to follow

Wrong: let price = 300;; and then total 4;;, expecting 1200. Right: run the definition of total again after the new price, or make price a parameter: let total price qty = price * qty.

Lines that start with # are directives to F# Interactive rather than F# code. #quit ends the session, #time switches timing on and off, and since F# 9 #help followed by a name shows that function’s documentation:

#time and #help in a session Python · directives_session.py
from session import fsi

# Two directives of F# Interactive: #time measures every entry, #help shows a function's documentation.
fsi([
    '#time "on";;',
    "List.sum [ for i in 1L .. 1_000_000L -> i * i ];;",
    '#time "off";;',
    "#help List.map;;",
    "#quit;;",
], args=["--nologo"], keep_times=True)

Output

$ dotnet fsi --nologo
> #time "on";;

--> Timing now on

> List.sum [ for i in 1L .. 1_000_000L -> i * i ];;
Real: 00:00:00.079, CPU: 00:00:00.014, GC gen0: 0, gen1: 0, gen2: 0
val it: int64 = 333333833333500000L

> #time "off";;

--> Timing now off

> #help List.map;;

Description:
Builds a new collection whose elements are the results of applying the given function
to each of the elements of the collection.

Remarks:
This is an O(n) operation, where n is the length of the list.

Parameters:
- mapping: The function to transform elements from the input list.
- list: The input list.
Returns:
The list of transformed elements.

Examples:
let inputs = [ "a"; "bbb"; "cc" ]

inputs |> List.map (fun x -> x.Length)
// Evaluates to [ 1; 3; 2 ]

Full name: Microsoft.FSharp.Collections.ListModule.map
Assembly: FSharp.Core.dll

> #quit;;

This output changes from run to run: The times that #time reports differ from run to run and from computer to computer; the result and the documentation do not.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 directives_session.py

With timing on, every entry reports its real time, the processor time it used and how many garbage collections of each generation ran. Summing a million squares took under a tenth of a second here, with no collection. The other directives that matter early are #r for packages and assemblies, which the last part of this lesson uses, #load for another script and #i for a package source.

Checkpoint

Check: Entries, it and shadowing at the prompt

  1. Question 1 of 2 After let price = 300;; in the session, what did total 4;; print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: val it: int = 1000

    A second let with the same name shadows the first: later entries see the new value, while anything compiled earlier keeps the old one. Define total again after price to change what it computes.

  2. Question 2 of 2 At the > prompt you type let total qty = and press Enter. What does F# Interactive do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It waits for the rest of the entry, showing a - prompt, until a line ends with ;;

    F# Interactive collects lines until ;; and then compiles and runs the whole entry, which is how a multi-line function such as total was entered.

A script runs from top to bottom and prints only what it prints

A script: An F# source file with the .fsx extension that dotnet fsi compiles and runs from top to bottom, with no project file. It can reference NuGet packages with #r directives. is a .fsx file that dotnet fsi script.fsx compiles and runs from top to bottom. Arguments after the file name reach the script in fsi.CommandLineArgs, whose first element is always the script’s own name. This one adds up the whole numbers it is given:

tally.fsx: add up the numbers on the command line FSX · tally.fsx
#!/usr/bin/env -S dotnet fsi
// tally.fsx: adds up the whole numbers given after the script's name.
let args = fsi.CommandLineArgs
printfn "Arguments: %A" args

let numbers = args[1..]
let bad = numbers |> Array.filter (fun s -> not (fst (System.Int32.TryParse s)))

if bad.Length > 0 then
    eprintfn "Not a whole number: %s" (String.concat ", " bad)
    exit 2

let total = numbers |> Array.sumBy int
let twice = total * 2   // a script prints only what it prints: there is no val line for this
printfn "%d numbers, total %d" numbers.Length total
tally.fsx with good and bad arguments Python · script_session.py
from session import run

# tally.fsx run as a script, with good and bad arguments, then directly through its first line (macOS and Linux).
run("""
dotnet fsi tally.fsx 12 30 8
!dotnet fsi tally.fsx 12 thirty 8
chmod +x tally.fsx
./tally.fsx 5 5
""")

Output

$ dotnet fsi tally.fsx 12 30 8
Arguments: [|"tally.fsx"; "12"; "30"; "8"|]
3 numbers, total 50
$ dotnet fsi tally.fsx 12 thirty 8
Arguments: [|"tally.fsx"; "12"; "thirty"; "8"|]
Not a whole number: thirty
[exit status 2]
$ chmod +x tally.fsx
$ ./tally.fsx 5 5
Arguments: [|"./tally.fsx"; "5"; "5"|]
2 numbers, total 10

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 script_session.py

Three things differ from a session at the prompt. Nothing is echoed: let twice = total * 2 defines a value but prints no val line, so a script shows only what it prints itself. A problem can end the script with a status of your choosing, here exit 2 after eprintfn wrote the message to standard error, which a calling script or a build step can test. And on macOS and Linux the first line, #!/usr/bin/env -S dotnet fsi, lets the file run as a command once it is executable, as ./tally.fsx 5 5 did; on Windows, run dotnet fsi tally.fsx.

Each run starts .NET and F# Interactive afresh, and that start is most of the cost of a short script:

How long a one-line script takes Python · startup_session.py
from session import run

# How long a one-line script takes from start to finish (/usr/bin/time -p prints real, user and system seconds).
run("""
/usr/bin/time -p dotnet fsi hello.fsx
/usr/bin/time -p dotnet fsi hello.fsx
""", keep_times=True)

Output

$ /usr/bin/time -p dotnet fsi hello.fsx
Hello from F# on .NET 10.0.12
real 3.55
user 1.19
sys 0.63
$ /usr/bin/time -p dotnet fsi hello.fsx
Hello from F# on .NET 10.0.12
real 2.46
user 1.15
sys 0.32

This output changes from run to run: Start-up times differ from run to run and from computer to computer; the program's output does not.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 startup_session.py

The two runs of a one-line script took 3.6 and 2.5 seconds of real time on the recording computer, which was busy with other work, and about 1.2 seconds of processor time each on the user line; the million squares above took under a tenth of a second. Almost all of a short script’s time is starting the runtime and compiling the script. Keep related work in one script rather than many tiny ones, and explore at the prompt, where the start-up is paid once.

Checkpoint

Check: Running a script

  1. Question 1 of 2 For dotnet fsi tally.fsx 12 30 8, what is fsi.CommandLineArgs[0]?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: "tally.fsx", the script's own name

    fsi.CommandLineArgs holds the script's name followed by its arguments, so the arguments start at index 1, as in tally.fsx's args[1..].

  2. Question 2 of 2 Why did dotnet fsi tally.fsx 12 thirty 8 end with [exit status 2]?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The script wrote the message with eprintfn and then called exit 2

    A script can end with any exit status through exit, so a calling script or a build step can tell success from failure; messages for people go to standard error with eprintfn.

Scripts reference NuGet packages with #r, pinned to a version

A script can use any NuGet package without a project file: #r "nuget: Name, Version" at its top makes dotnet fsi restore the package before the script runs. This session has no network, so it first shows what that means for a package that is in no source the computer can reach. Then it packs two versions of a small library, Paise, into a local folder, and runs scripts whose nuget.config names that folder as their only package source:

scripts/pinned.fsx: one package, one exact version FSX · scripts/pinned.fsx
#r "nuget: Paise, 1.0.0"

printfn "Paise %s: %s" Paise.Money.version (Paise.Money.format 123456L)
A package reference, offline, then from a local source Python · nuget_session.py
from session import run

# The network is off in this session. First a script whose package is in no source this computer can reach; then a
# folder of packages made here, two versions of a small library called Paise, and scripts that a nuget.config points
# at that folder.
run("""
!dotnet fsi offline/pinned.fsx
dotnet pack Paise -o feed -p:Version=1.0.0 -v q
dotnet pack Paise -o feed -p:Version=1.1.0 -v q
ls feed
cd scripts
/usr/bin/time -p dotnet fsi pinned.fsx
/usr/bin/time -p dotnet fsi pinned.fsx
ls ~/.nuget/packages/paise
dotnet fsi latest.fsx
""", keep_times=True)

Output

$ dotnet fsi offline/pinned.fsx


offline/pinned.fsx(1,1): error FS0999: ~/.packagemanagement/nuget/Projects/<id>/Project.fsproj : error NU1301: Unable to load the service index for source https://api.nuget.org/v3/index.json.
[exit status 1]
$ dotnet pack Paise -o feed -p:Version=1.0.0 -v q
$ dotnet pack Paise -o feed -p:Version=1.1.0 -v q
$ ls feed
Paise.1.0.0.nupkg
Paise.1.1.0.nupkg
$ cd scripts
$ /usr/bin/time -p dotnet fsi pinned.fsx
Paise 1.0.0: Rs 1234.56
real 9.17
user 2.74
sys 0.87
$ /usr/bin/time -p dotnet fsi pinned.fsx
Paise 1.0.0: Rs 1234.56
real 3.85
user 1.02
sys 0.27
$ ls ~/.nuget/packages/paise
1.0.0
$ dotnet fsi latest.fsx
Paise 1.1.0: Rs 1234.56

This output changes from run to run: Restore and start-up times differ from run to run and from computer to computer; the steps and what the scripts print do not.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 nuget_session.py

The offline run failed before a line of the script ran, with error NU1301: the package was in no folder on the computer and nuget.org was out of reach. With the local source, the first run of pinned.fsx restored Paise 1.0.0 and took 9.2 seconds of real time; the second found it in ~/.nuget/packages, the global packages folder: The folder where NuGet keeps every package version it has restored, ~/.nuget/packages by default. A later restore of the same version takes it from there instead of from a source., and took 3.9. Step through the first run, then the second:

Interactive · stage by stage What dotnet fsi does with a package reference Step down the column for the first run of pinned.fsx; the branch on the right is the second run, which finds the package already there.
What dotnet fsi does with a package reference, step 7 of 7. Showing Read the #r lines, Write a small project, In the package folder?, Restore from a source, Reference the DLLs, Compile and run. In focus: Restore from a source.found: no restoreRead the #r linesnuget: Paise, 1.0.0Write a small projectProject.fsproj, out of sightIn the package folder?~/.nuget/packagesRestore from a sourcesources in nuget.configReference the DLLsPaise.dll for this runCompile and runthe rest of the script

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Directives first. Before compiling anything, dotnet fsi collects the script's #r "nuget: …" lines. pinned.fsx asks for exactly Paise 1.0.0.
  2. A project for the packages. It writes a small project that references those packages, under .packagemanagement in your home folder. The error of the offline run names that project.
  3. Not there yet. On the first run Paise 1.0.0 is not in the package folder, so it must come from a package source.
  4. Restore. NuGet restores the package from the sources nuget.config names, here only the local feed folder, and keeps a copy in ~/.nuget/packages/paise/1.0.0. With no reachable source, the run stops here with error NU1301.
  5. Reference and run. Paise.dll is referenced for this run, the script compiles and prints Paise 1.0.0. The first run took 9.2 s of real time in the recorded session.
  6. The second run. The second run finds Paise 1.0.0 in the package folder and skips the restore, which is why it took 3.9 s instead of 9.2 s.
  7. Without a version. latest.fsx writes

The last command is the reason to pin. latest.fsx is the same script with #r "nuget: Paise" and no version, and the F# Interactive reference says such a reference takes the highest version available that is not a preview. The moment 1.1.0 existed, the same script printed Paise 1.1.0: an unpinned script can change its output with nobody touching it. A nuget.config next to the script decides where packages may come from, which matters for build agents without internet access; #i "nuget: <folder>" adds a source too, but the reference notes it does not accept relative paths yet.

Packages run with your permissions

A script’s packages run with everything your account can do. Microsoft’s F# Interactive reference gives the same advice as for any package reference: only include dependencies from sources you trust. Pin the versions you checked, too, so a new release cannot slip in.

Checkpoint

Check: Package references in scripts

  1. Question 1 of 2 pinned.fsx printed Paise 1.0.0, and latest.fsx, the same script without a version, printed Paise 1.1.0. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A reference without a version takes the highest version the sources have that is not a preview

    Without a version, the package a script gets depends on what its sources hold on the day it runs. Pin the version you checked, and the script prints the same thing next month.

  2. Question 2 of 2 Why was the second run of pinned.fsx faster than the first?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Paise 1.0.0 was already in ~/.nuget/packages, so nothing had to be restored

    The first run of a script with a new package reference restores it into the global packages folder. Later runs that ask for the same version find it there and only compile and run the script.

Three editors understand F# through the same compiler

Any editor can open a .fsx file, but these three check F# as you type, run scripts in F# Interactive and load projects:

Editor, and where it runs How F# support comes Sending code to F# Interactive
VS Code with Ionide: Windows, macOS, Linux the Ionide for F# extension, a community project select the code, then Alt+Enter
Visual Studio: Windows the ASP.NET and web development workload select the code, then Alt+Enter
JetBrains Rider: Windows, macOS, Linux the F# Support plugin, bundled and on by default select the code, then Send Selection to F# Interactive

Microsoft’s install page for F# names Ionide as the only plugin VS Code needs, and its VS Code guide describes Ionide as an open-source project built and maintained by members of the F# community. Whichever editor you choose, it uses the SDK that the folder’s global.json pins, the same one dotnet fsi uses at the command line. Notebooks are no longer an option to plan around: Polyglot Notebooks and .NET Interactive, which ran F# in notebooks, were deprecated and their repository archived, so this track uses .fsx scripts for exploring.

Send code from a file to F# Interactive

In VS Code with Ionide, put let price = 250 and let total qty = price * qty in a new file, scratch.fsx, select both lines and press Alt+Enter. The F# Interactive pane answers with the same val lines as the first session above; then type total 4;; in the pane.

Checkpoint

Check: Editors for F#

  1. One question A folder pins SDK 10.0.401 in global.json, and you open it in VS Code with Ionide. Which SDK checks and runs its scripts?

    Choose one answer.

    Show the answer: One question

    Answer: The pinned 10.0.401, as dotnet fsi uses at the command line in that folder

    The editor and the command line use the same SDK and the same compiler, chosen by global.json, so they agree about what a script means.

Practise: installing, running and referencing

Here are questions on the SDK, global.json, the prompt, scripts, package references and editors. They arrive one by one, weakest idea first, and whichever answer you pick, the explanation of why it works or fails opens straight away:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 Which command lists every .NET SDK installed on the computer?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: dotnet --list-sdks

    --version answers which SDK this folder uses, --list-sdks which SDKs exist, and --list-runtimes which runtimes can run built programs.

  2. Question 2 of 12 A teammate's laptop has only the .NET runtime installed. What happens when they run dotnet fsi tally.fsx?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: It fails, because F# Interactive is part of the SDK, not of the runtime

    Install the SDK on every computer where code is written or scripts are run; the runtime alone suits computers that only run built programs.

  3. Question 3 of 12 No global.json exists in a folder or in any folder above it. Which SDK does dotnet use there?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: The newest SDK installed

    Without a pin, every dotnet command uses the newest SDK installed. A global.json changes that for its folder and every folder below it.

  4. Question 4 of 12 A global.json gives version 10.0.401 with rollForward latestPatch. SDKs 10.0.401, 10.0.402 and 10.0.500 are installed. Which one runs?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: 10.0.402

    An SDK version has a feature band, the hundreds of its last number. latestPatch stays in the band and takes its newest patch, so security fixes apply while new features wait for a deliberate change.

  5. Question 5 of 12 What does it hold in an F# Interactive session?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: The value of the last entry that had no name of its own

    F# Interactive binds every unnamed result to it, so you can keep working with the value you just computed.

  6. Question 6 of 12 At the prompt you correct a helper function and enter its new definition. A function you entered earlier that calls the helper still gives the old, wrong result. Why, and what fixes it?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: The earlier function was compiled with the old helper; enter the earlier function again, or reload the whole script

    Each entry is compiled against the values that exist at that moment. After changing a definition, enter again everything that depends on it, or keep the code in a script and load it again with #load.

  7. Question 7 of 12 A script contains only let total = List.sum [1; 2; 3]. What does dotnet fsi script.fsx print?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: Nothing at all

    At the prompt every entry is answered with a val line; a script runs silently except for its own output, so it must print what it means to show.

  8. Question 8 of 12 A build step runs 200 small F# scripts one after another, each doing a few milliseconds of work. What would you change first?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: Combine the work into one script, or a few, so that start-up is paid once instead of 200 times

    Each script run starts the runtime and compiles the script before any work happens. For many small tasks, one run that does them all, or a compiled program, is much faster.

  9. Question 9 of 12 Which line makes a script use exactly version 1.0.0 of the Paise package?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: #r "nuget: Paise, 1.0.0"

    #r "nuget: Name, Version" pins a version; #r "nuget: Name" takes the highest one available that is not a preview.

  10. Question 10 of 12 A build agent has no internet access, and a script on it starts with #r "nuget: Paise, 1.0.0". What lets it run?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: A nuget.config next to the script that names an internal feed or folder holding Paise 1.0.0

    Restore needs a source that has the package, or the package already in the global packages folder. A nuget.config with an internal feed or a folder makes scripts work where nuget.org cannot be reached.

  11. Question 11 of 12 What gives Visual Studio Code its F# support?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: The Ionide for F# extension

    Ionide, a community project, adds F# checking, F# Interactive and project loading to VS Code, using the SDK that the folder pins.

  12. Question 12 of 12 You work on macOS and want F# checked as you type and code sent to F# Interactive. Which choice fits?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: VS Code with Ionide, or JetBrains Rider

    On macOS and Linux the choices are VS Code with Ionide and Rider; on Windows, Visual Studio as well. All of them use the same SDK and compiler.

Interview questions

Warm-up (fresher to 2–5 years): what is F# Interactive, and when would you use a script instead of a project?

F# Interactive, started with dotnet fsi, is the REPL and script runner that ships in the .NET SDK. At the prompt it compiles and runs each entry ending with ;; and prints a val line with the name, type and value; given a .fsx file it runs the script from top to bottom and prints only what the script prints. Scripts suit exploration, automation, data checks and one-off tasks: they need no project file, can reference NuGet packages with #r "nuget: Name, Version", and receive arguments in fsi.CommandLineArgs. A project suits code that several files make up, that is compiled into an assembly, tested, packaged or deployed, and code that must start fast, because a script compiles on every run.

Two harder questions follow, about a script whose output changed by itself and about scripts on build agents without internet access:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A nightly F# script that builds a sales report printed different totals this week, although nobody changed the script or its data. It starts with #r lines for two packages, without versions. How do you investigate, and how do you stop it happening again?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Your team's F# build scripts must run on build agents that have no internet access, and some of them reference NuGet packages with #r. What do you set up so that they work there, and how do you check it before the agents depend on it?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • The .NET SDK is the one thing to install: it carries the F# compiler, F# Interactive and FSharp.Core, and dotnet --version and dotnet fsi --help show what you got.
  • A global.json decides which installed SDK every dotnet command in its folder uses, dotnet fsi included; latestPatch suits projects, disable reproduces a recorded output exactly.
  • At the prompt, an entry ends with ;;, an unnamed result becomes it, and a second let with the same name shadows the first while older functions keep the value they were compiled with.
  • A script runs from top to bottom, prints only what it prints, reads its arguments from fsi.CommandLineArgs and can end with exit; most of a short script’s time is start-up.
  • #r "nuget: Name, Version" restores a package into the global packages folder on the first run; pin the version, or the script changes its output whenever a newer one appears.

Terms in this lesson

.NET SDK
The kit you install to build .NET code: the dotnet command, MSBuild, NuGet, the C# and F# compilers, F# Interactive and a .NET runtime. Without it, dotnet fsi and dotnet build cannot run.
F# Interactive
The REPL and script runner of the .NET SDK, started with dotnet fsi. At the prompt it compiles and runs each entry that ends with ;; and answers with the name, type and value.
global packages folder
The folder where NuGet keeps every package version it has restored, ~/.nuget/packages by default. A later restore of the same version takes it from there instead of from a source.
global.json
A JSON file that pins the .NET SDK for its folder and every folder below it. dotnet looks for one in the current folder and each parent, and its rollForward value says how strictly the version must match.
REPL
Read, evaluate, print loop: a prompt that reads an entry, runs it, prints the result and waits for the next one, so code can be tried a line at a time.
script
An F# source file with the .fsx extension that dotnet fsi compiles and runs from top to bottom, with no project file. It can reference NuGet packages with #r directives.
shadowing
Defining a new value with a name that is already in use. Later code sees the new value, while code compiled earlier keeps the value it was compiled with.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.