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

What F# is and where it fits in .NET

How F# relates to C#, the .NET SDK, FSharp.Core and the runtime, what functional-first changes in everyday code, and where F# earns a place in a .NET team.

  • 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

  • Explain how F# code, the F# compiler, FSharp.Core and the .NET runtime fit together
  • Name the four version numbers of an F# setup and say what sets each one
  • Compare the defaults of F# and C# using one rule written in both languages
  • Choose where F# fits in a .NET team and name what adopting it costs
What you’ll learn

What you will learn

  • Explain how F# code, the F# compiler, FSharp.Core and the .NET runtime fit together
  • Name the four version numbers of an F# setup and say what sets each one
  • Compare the defaults of F# and C# using one rule written in both languages
  • Choose where F# fits in a .NET team and name what adopting it costs
On this page

In the 2025 Stack Overflow survey 1.3 % of respondents said they had used F#, against 27.8 % for C#. Yet a C# team that adds an F# project keeps its runtime, its libraries and its NuGet packages: the F# compiler writes the same intermediate code as the C# compiler, and the two programs in this lesson print the same five lines. What changes is the language’s defaults, and the defaults decide where F# pays for itself.

This lesson takes an F# script apart to show which piece of .NET does what, reads the four version numbers that every F# setup has, writes one pricing rule in both languages, and ends with a way to decide where F# belongs in your own code.

An F# program is a .NET program with one extra library

Here is a small script that asks the program running it two questions: which F# library and runtime it got, and which assembly each of five types comes from. At the end it hands an F# record to System.Text.Json, a .NET library written in C#:

versions.fsx: what runs an F# script FSX · versions.fsx
// What runs this script, and which assembly each type it uses comes from.
open System
open System.Reflection
open System.Runtime.InteropServices
open System.Text.Json

let core = typeof<int list>.Assembly
let informational = core.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion

printfn "FSharp.Core  %s (package %s)" (string (core.GetName().Version)) (informational.Split('-', '+')[0])
printfn "Runtime      %s" RuntimeInformation.FrameworkDescription
printfn "Platform     %s" RuntimeInformation.RuntimeIdentifier
printfn ""

let from (t: Type) = t.Assembly.GetName().Name

for t in [ typeof<int list>; typeof<int option>; typeof<string>; typeof<Collections.Generic.List<int>>; typeof<JsonSerializer> ] do
    printfn "%-16s comes from %s" t.Name (from t)

// A .NET library working on an F# type: System.Text.Json serialises an F# record.
type Order = { Item: string; Qty: int }

printfn ""
printfn "%s" (JsonSerializer.Serialize { Item = "pen"; Qty = 3 })
versions.fsx, run with dotnet fsi Python · versions_session.py
from session import run

# versions.fsx asks the running program which FSharp.Core and which runtime it got, and where five types come from.
run("""
dotnet fsi versions.fsx
""")

Output

$ dotnet fsi versions.fsx
FSharp.Core  10.1.0.0 (package 10.1.401)
Runtime      .NET 10.0.12
Platform     osx-arm64

FSharpList`1     comes from FSharp.Core
FSharpOption`1   comes from FSharp.Core
String           comes from System.Private.CoreLib
List`1           comes from System.Private.CoreLib
JsonSerializer   comes from System.Text.Json

{"Item":"pen","Qty":3}

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

Three facts are in that output. The script ran on .NET 10.0.12, the runtime a C# program on the same computer uses. F#’s list and option types are .NET classes, FSharpList and FSharpOption, and they come from FSharp.Core: F#'s own library: the list, option and Result types, printf, and the List, Seq and Option modules. Every F# program uses it, and a built F# program ships FSharp.Core.dll beside its own assembly., while String and List<T> come from System.Private.CoreLib, the runtime’s own core library. And JsonSerializer, which knows nothing about F#, turned the F# record into JSON.

Treating F# as a separate platform

Wrong: F# needs a runtime, a package feed or libraries of its own. Right: an F# project runs on the same .NET runtime and references the same NuGet packages as a C# project, which is how versions.fsx called System.Text.Json directly. The only F#-only library is FSharp.Core.

The F# compiler, which ships inside the .NET SDK, checks the program and translates it into IL: Intermediate language: the instructions that every .NET compiler, F# and C# alike, writes into assemblies. The .NET runtime compiles them to machine code just before each method first runs., the same intermediate language that C# compiles to, stored in an assembly. When the program starts, the .NET runtime loads that assembly together with every library it calls, and compiles each method to machine code the first time something calls it. Follow versions.fsx from its file to the processor:

Interactive · stage by stage From an F# file to a running program Step down the column. The F# compiler writes IL, the .NET runtime turns IL into machine code, and FSharp.Core comes along.
From an F# file to a running program, step 6 of 6. Showing F# source files, The F# compiler, IL in an assembly, FSharp.Core.dll, The .NET runtime, Machine code. In focus: Machine code.F# source files.fs files and .fsx scriptsThe F# compilerin the .NET SDKIL in an assemblythe IL C# compiles toFSharp.Core.dlllists, options, printfThe .NET runtimeloads, then JIT-compilesMachine coderuns on the processor

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

Every step as text
  1. You write F#. F# code lives in .fs files, which a project compiles, or in .fsx scripts, which F# Interactive runs. versions.fsx is a script, run with dotnet fsi.
  2. The compiler checks it. The F# compiler checks every type, works out the ones you did not write, and translates the code into IL. It ships inside the .NET SDK, so installing the SDK installs F#.
  3. IL, as from C#. IL is the intermediate language that every .NET language compiles to. An F# assembly is an ordinary .NET assembly, so a C# project can reference it and call its public members.
  4. FSharp.Core comes along. F# code also uses FSharp.Core.dll, F#'s own library. F# lists and options are its types FSharpList and FSharpOption, as versions.fsx printed, and a built F# program ships a copy beside it.
  5. The runtime loads it all. The .NET runtime loads the assembly, FSharp.Core and the .NET libraries it calls, such as System.Text.Json, the same libraries a C# program uses.
  6. Machine code runs. The first time a method is called, the runtime compiles its IL for this processor and keeps the result for later calls. At this point an F# method and a C# method look exactly alike.

FSharp.Core is the one extra piece: F#’s own library, with lists, options, printf and the List, Seq and Option modules. Its notes tell application authors to always deploy FSharp.Core as part of a compiled application, and the build output of a new F# project holds FSharp.Core.dll beside the program’s own assembly, as the third lesson of this module shows.

Key idea

F# compiles to the same IL as C# and runs on the same runtime with the same libraries; the one extra piece is FSharp.Core, F#’s own library, which every F# program loads.

Checkpoint

Check: The pieces of an F# program

  1. Question 1 of 2 versions.fsx printed the assembly each type comes from. Where does an F# int list come from?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: FSharp.Core

    F# keeps its own types, such as lists and options, in FSharp.Core, the one F#-only library. Everything else the script used, from String to JsonSerializer, came from the same .NET libraries a C# program uses.

  2. Question 2 of 2 Your F# script must read spreadsheet files, and the best library for it is a NuGet package written in C#. What do you do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Reference the package and call its classes, as a C# project would

    An F# program is a .NET program. It references NuGet packages, the base class library and C# assemblies exactly as a C# program does.

Four version numbers, each set in a different place

An F# setup has four version numbers, and each is decided somewhere else. Two of them, and the compiler’s own version, can be read in a terminal, here in the folder whose global.json pins the SDK this track uses:

The SDK, F# Interactive and the language versions it accepts Python · toolchain_session.py
from session import run

# The SDK this folder's global.json pins, the F# Interactive inside it, and the F# language versions its compiler knows.
run("""
dotnet --version
dotnet fsi --help | head -n 1
dotnet fsi --langversion:?
""")

Output

$ dotnet --version
10.0.401
$ dotnet fsi --help | head -n 1
Microsoft (R) F# Interactive version 15.2.401.0 for F# 10.0
$ dotnet fsi --langversion:?
Supported language versions:
preview
default
latest
latestmajor
4.6
4.7
5.0
6.0
7.0
8.0
9.0
10.0 (Default)
11.0

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

dotnet --version names the SDK, 10.0.401. Its F# Interactive reports itself as version 15.2.401.0 for F# 10.0: the compiler has a version of its own, and 10.0 is the language it compiles by default, marked (Default) in the list of language versions it accepts. The other two numbers, FSharp.Core 10.1.401 and the runtime 10.0.12, are the ones versions.fsx printed above.

Version This track Where it is set
The .NET SDK 10.0.401 The SDK you install; a global.json file picks one for a folder
The F# language 10.0 The default of the compiler inside the SDK; LangVersion in a project changes it
FSharp.Core 10.1.401 The package the SDK references for you; a library may pin a lower one
The .NET runtime 10.0.12 The project’s target framework, net10.0, and the newest 10.0 patch installed

The first three numbers come with the SDK: its compiler sets the default language version, and the SDK brings the FSharp.Core package that matches it. The FSharp.Core notes say that application authors do not have to reference FSharp.Core themselves, while package authors should pin a version, as low as the library allows: FSharp.Core is binary compatible across versions of the language, so a library built against an older FSharp.Core still runs in an application that ships a newer one. The runtime is chosen when the program starts. Microsoft’s version-selection guide says the host picks the latest patch installed for the target framework, so a program built for net10.0 runs on the newest 10.0 patch the computer has. Walk through the F# releases and the .NET each one came with:

Timeline · 7 events in order F# releases and the .NET each came with Walk through the releases in order. Each F# version came with a .NET version and is the default language of that SDK.
  1. F# 5

    Packages in scripts, string interpolation scripts

    F# 5 came with .NET 5. A script can reference a NuGet package with #r "nuget: …", and strings gained $"…" interpolation and nameof.

  2. F# 6

    task expressions and xs[0]

    F# 6 came with .NET 6. task { … } writes .NET tasks directly, the way C# async methods do, and indexing dropped its dot: xs[0] instead of xs.[0].

  3. F# 7

    Static abstract interface members

    F# 7 came with .NET 7. Interfaces can declare static abstract members, the feature behind generic math in .NET, and F# checks the required properties of C# types.

  4. F# 8

    _.Name and nested copy and update

    F# 8 came with .NET 8. _.Name stands for a lambda that only reads a property, and one copy-and-update expression can change a field of a nested record.

  5. F# 9

    Nullable reference types

    F# 9 came with .NET 9. With nullness checking switched on, string | null says that a value from .NET may be null and the compiler checks every use; union cases gained .Is properties such as IsEmail.

  6. F# 10

    Scoped warnings and and! in task this track

    F# 10 came with .NET 10, the long-term support release this track uses. A

  7. F# 11

    Next, with .NET 11 preview

    The next F# comes with .NET 11, which is a release candidate. The compiler of SDK 10.0.401 already lists 11.0 among the language versions it accepts, but 10.0 is its default.

The timeline has 7 events, from F# 5 to F# 11.

Which versions this track uses

Every example of this track ran on the .NET SDK 10.0.401 with the 10.0.12 runtime and F# 10. Microsoft supports only the newest servicing update of a release, and patches keep compatibility, so install the newest .NET 10 SDK for your own work; the next lesson shows how to pin one for a folder.

Checkpoint

Check: The four version numbers

  1. Question 1 of 2 A new F# project is built with SDK 10.0.401 and sets no LangVersion. Which F# language version does it compile?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: F# 10.0

    The default language version comes from the compiler inside the SDK. A newer SDK brings a newer default, and LangVersion in a project file chooses another version on purpose.

  2. Question 2 of 2 You are about to publish an F# library on NuGet. What should you do about FSharp.Core?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Pin the oldest FSharp.Core that has everything the library uses

    Applications take the FSharp.Core their SDK gives them. Libraries pin one, as low as they can, so that applications on older and newer SDKs can all use them.

Functional-first changes the defaults, not the platform

Take one pricing rule: a standard customer pays the full amount, a member gets 5 % off, or 10 % after five years, and a partner gets a negotiated rate. Here it is in C# 14, with records and a switch expression, as a current C# code base would write it:

Discount.cs: the rule in C# 14 C# · discount-cs/Discount.cs
// The same pricing rule in C# 14: records for the data, a switch expression for the rule.
Invoice[] invoices =
[
    new("Asha", 1200m, new Standard()),
    new("Ben", 1200m, new Member(2)),
    new("Chen", 1200m, new Member(7)),
    new("Dana", 1200m, new Partner(0.15m)),
];

foreach (var invoice in invoices)
{
    Console.WriteLine($"{invoice.Customer,-6} {Total(invoice),8:F2}");
}

// A with expression copies the record; the old one does not change.
var renewed = invoices[1] with { Tier = new Member(5) };
Console.WriteLine($"{renewed.Customer}: {Total(invoices[1]):F2} before renewal, {Total(renewed):F2} after");

static decimal DiscountRate(Tier tier) => tier switch
{
    Standard => 0m,
    Member { Years: >= 5 } => 0.10m,
    Member => 0.05m,
    Partner p => p.Rate,
    _ => throw new ArgumentOutOfRangeException(nameof(tier)),
};

static decimal Total(Invoice invoice) => invoice.Amount * (1m - DiscountRate(invoice.Tier));

abstract record Tier;
sealed record Standard : Tier;
sealed record Member(int Years) : Tier;
sealed record Partner(decimal Rate) : Tier;
record Invoice(string Customer, decimal Amount, Tier Tier);

Output

Asha    1200.00
Ben     1140.00
Chen    1080.00
Dana    1020.00
Ben: 1140.00 before renewal, 1080.00 after

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run Discount.cs

And here is the same rule in F#, as a script, followed by a session that runs it and then loads it into F# Interactive, which prints the type of everything the script defines:

discount.fsx: the rule in F# FSX · discount/discount.fsx
// One pricing rule written the F# way: a union for the customer's tier, a record for the invoice, two functions.
type Tier =
    | Standard
    | Member of years: int
    | Partner of rate: decimal

type Invoice = { Customer: string; Amount: decimal; Tier: Tier }

let discountRate tier =
    match tier with
    | Standard -> 0m
    | Member years when years >= 5 -> 0.10m
    | Member _ -> 0.05m
    | Partner rate -> rate

let total invoice =
    invoice.Amount * (1m - discountRate invoice.Tier)

let invoices =
    [ { Customer = "Asha"; Amount = 1200m; Tier = Standard }
      { Customer = "Ben"; Amount = 1200m; Tier = Member 2 }
      { Customer = "Chen"; Amount = 1200m; Tier = Member 7 }
      { Customer = "Dana"; Amount = 1200m; Tier = Partner 0.15m } ]

for invoice in invoices do
    printfn "%-6s %8.2f" invoice.Customer (total invoice)

// Copy and update: a new invoice with another tier; the old one does not change.
let renewed = { invoices[1] with Tier = Member 5 }
printfn "%s: %.2f before renewal, %.2f after" renewed.Customer (total invoices[1]) (total renewed)
discount.fsx, run, then loaded into F# Interactive Python · discount_session.py
from session import fsi, run

# Run the script, then load it into F# Interactive, which prints the type of everything the script defines.
run("""
dotnet fsi discount/discount.fsx
""")
fsi([
    '#load "discount/discount.fsx";;',
    "#quit;;",
], args=["--nologo"])

Output

$ dotnet fsi discount/discount.fsx
Asha    1200.00
Ben     1140.00
Chen    1080.00
Dana    1020.00
Ben: 1140.00 before renewal, 1080.00 after
$ dotnet fsi --nologo
> #load "discount/discount.fsx";;
[Loading discount/discount.fsx]
Asha    1200.00
Ben     1140.00
Chen    1080.00
Dana    1020.00
Ben: 1140.00 before renewal, 1080.00 after
module FSI_0002.Discount
type Tier =
  | Standard
  | Member of years: int
  | Partner of rate: decimal
type Invoice =
  {
    Customer: string
    Amount: decimal
    Tier: Tier
  }
val discountRate: tier: Tier -> decimal
val total: invoice: Invoice -> decimal
val invoices: Invoice list
val renewed: Invoice

> #quit;;

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

Both programs print the same five lines, from 1200.00 for Asha down to Ben’s 1080.00 after renewal. The second half of the session shows what the compiler worked out: discountRate takes a Tier and returns a decimal, and total takes an Invoice, yet no parameter in the script has a type written on it. That is type inference: The compiler working out the types of values and of a function's parameters from how they are used, so most F# code needs no type annotations and is still statically typed.: F# is as statically typed as C#, and the compiler finds most types from how the values are used.

The two programs are about the same length. What differs is what each language does when you write nothing special:

By default C# 14 F# 10
A local value can be assigned again is immutable; let mutable opts in
if, switch and match if is a statement, switch has a statement form everything is an expression with a value
Parameter types written on every method inferred, written where you choose
A fixed set of cases records under an abstract base a discriminated union: An F# type made of a fixed set of named cases, each with its own data, such as Standard | Member of years: int. Because the set is closed, a match over it is checked for missing cases.
null allowed for reference types, with warnings not a value of the types F# defines

The fourth row matters most for rules like this one. In C# 14 the compiler cannot know that no other class derives from Tier, so DiscountRate needs the _ => throw arm; add a fifth tier later and the program still compiles, then throws when it prices an invoice with the new tier. An F# union is closed: add a case and the compiler warns about every match that does not handle it, as the last lesson of this module shows. C# 15, a preview that comes with .NET 11, adds union types and closed hierarchies for the same reason.

Microsoft’s guide describes F# records and unions as non-null, immutable and comparable by default, and both languages copy a record: An F# type with named fields that is immutable and compared by value by default. { r with Field = v } makes a copy with one field changed and leaves r as it was. instead of changing it: with made a renewed invoice for Ben and left the first one as it was. A functional-first: A language whose defaults favour functions and immutable values: everything is an expression and values cannot change unless declared mutable, while classes and mutation stay available. language keeps classes, interfaces and mutation, and F# uses them for .NET interop and for state that must change, but you reach for them when you need them rather than by default.

Key idea

F# shares C#’s runtime and libraries but not its defaults: values are immutable, everything is an expression, types are inferred and unions are closed, so the compiler checks more of your rules for you.

Checkpoint

Check: F# defaults against C# defaults

  1. Question 1 of 2 discount.fsx writes no type on any parameter. What type did F# Interactive print for total?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: invoice: Invoice -> decimal

    Type inference finds the types from how values are used: the record fields name the Invoice type, and the arithmetic on decimals fixes the result. The program is just as statically typed as the C# one.

  2. Question 2 of 2 You add a fifth tier, Trial, to the Tier type of both programs and change nothing else. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: F# warns when it compiles that discountRate misses Trial; C# compiles, then throws when it prices a Trial invoice

    A discriminated union fixes its cases where it is defined, so every match over it can be checked for a missing case. In C# 14 a record hierarchy stays open, so a switch over it needs a discard arm that hides new cases.

Where F# earns its place in a .NET team, and what it costs

The features of the last section pay off in code that is mostly rules and data: pricing and eligibility rules, calculations, parsers, validation, and data checks and automation written as scripts. Microsoft’s F# documentation adds web development, where ASP.NET Core supports F# out of the box and community frameworks such as Giraffe and Falco build on it, and the browser, where the open-source Fable compiler turns F# into JavaScript. Code that mostly wires framework calls together gains least, because there is little logic for the types to check.

There are real costs. F#’s community is small, as the survey figure shows, so plan to train C# developers rather than expect to hire F# ones, and everyone who reads the code before it is merged must know the language. Microsoft’s F# strategy relies on the community for important libraries and tools, from the Ionide editor extension to the Fantomas formatter, so part of your tooling comes from projects that Microsoft does not maintain. And the boundary with C# needs care: the component design guidelines advise hiding union representations, avoiding curried parameters and replacing option results with the TryGetValue pattern in libraries that C# calls, because those F# constructs look unusual from C#.

There are four common ways to start, from a script on one laptop to a whole service. Answer the three questions about your own team and code:

Interactive · answer and compare Where would F# go in your .NET code? Answer three questions about your team and your code. The table marks the ways of using F# that fit, and what to watch for in the others.
Does anyone on the team already write F# well?
Will C# code call the new code directly?
What is the new code mostly?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four ways to use F# in a .NET team, against what decides between them
Way to use F# What the team must learn Fit with C# code Modelling rules and data Finding people
F# scripts What it changes: checks, data jobs and automation in .fsx files Good one person can start, and nothing new ships to production Good a script references any .NET package with #r Mixed unions and records help, but scripts stay small Good nobody new is needed
One F# library What it changes: one project in a C# solution Mixed everyone who changes or reads it must know F# Mixed C# calls it like any assembly; F# options, lists and unions need a C#-friendly surface Good closed unions and exhaustive match for the rules Mixed a few people must know F# well
A service in F# What it changes: a whole application written in F# Poor everyone on the team writes F# every day Good ASP.NET Core and NuGet packages work as they do from C# Good the whole domain in unions, records and functions Poor 1.3 % of 2025 survey respondents used F#, against 27.8 % for C#
Stay with C# What it changes: nothing new: C# records and switch expressions Good nothing new to learn Good one language everywhere Mixed C# 14 has no closed unions, so a switch over records needs a default arm Good the large pool of C# developers
How the answers decide
  • “Does anyone on the team already write F# well?” No, nobody yet: rules out A service in F# and adds a caveat to One F# library. A team that cannot yet read F# cannot maintain a whole F# service. Start with scripts, or with one library that two people learn to write first.
  • “Will C# code call the new code directly?” Yes, C# projects reference it: adds a caveat to One F# library and A service in F#. C# can call any public F# member, but F# options, lists, unions and function values look strange from C#: expose .NET types and methods at the boundary.
  • “What is the new code mostly?” Rules, calculations or parsing: adds a caveat to Stay with C#. A closed union makes the compiler warn about every match that misses a case. In C# 14 a switch over records needs a default arm, so a new case fails only when it runs.
  • “What is the new code mostly?” Glue between frameworks and screens: adds a caveat to One F# library and A service in F#. Code that mostly wires framework calls together gains little from unions and inference, and each F# file is one more language for the team to read.

In short: scripts cost almost nothing and teach the language; one F# library for the rules of a C# system is the usual first production step, with a C#-friendly surface; a whole service in F# needs a team that already reads F#; and plain C# stays the better choice for glue code or a team that cannot invest in a second language.

Checkpoint

Check: Where F# fits in a team

  1. One question A C# team maintains a pricing engine full of rules, and nobody on the team knows F# yet. What is a sensible first step?

    Choose one answer.

    Show the answer: One question

    Answer: Write checks and data jobs as F# scripts, then move one rules library to F# once two people read it well

    Start where the cost is lowest and the payoff clearest: scripts, then one library of rules with a C#-friendly surface. A whole service in F# needs a team that already reads F#.

Practise: the pieces, the versions and the fit

Test yourself on the pieces of an F# program, its version numbers, the defaults of the two languages and where F# belongs. The practice starts with whichever of these ideas your answers so far show to be weakest, and each choice you make is explained on the spot:

Practice that adapts to you

Practise this lesson

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

  1. Question 1 of 9 Which statement about an F# program and a C# program on .NET 10 is true?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: Both are IL in assemblies, run by the same runtime with the same libraries

    F# and C# share everything below the language: IL, assemblies, the runtime, the base class library and NuGet. FSharp.Core is the one F#-only piece.

  2. Question 2 of 9 To save space, someone deletes FSharp.Core.dll from the output folder of a built F# console program. What happens when it runs?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: It fails as soon as it needs FSharp.Core, because the file it was built against is missing

    FSharp.Core is a library like any other: the program references it, and the runtime must find FSharp.Core.dll beside the program when the first F# type or function is used.

  3. Question 3 of 9 What decides the .NET runtime that a compiled F# program runs on?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: The project's target framework, such as net10.0, and the newest patch of it installed

    Each version number has its own place: the SDK comes from what is installed and global.json, the language from the compiler or LangVersion, FSharp.Core from the package reference, the runtime from the target framework.

  4. Question 4 of 9 A program built for net10.0 with SDK 10.0.401 starts on a computer where the 10.0.12 and 10.0.13 runtimes are installed. Which runtime runs it?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: 10.0.13

    A program asks for a major and minor version, here 10.0, and runs on the newest patch of it, so security fixes apply without a rebuild.

  5. Question 5 of 9 In F#, what is the value of if 3 > 0 then "positive" else "not positive"?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: The string "positive"

    Everything in F# is an expression. An if returns the value of the branch it takes, which is why both branches must have the same type, and why match can compute a rate directly.

  6. Question 6 of 9 discount.fsx runs let renewed = { invoices[1] with Tier = Member 5 }. What happens to invoices[1]?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: Nothing; with builds a new invoice and the old one keeps its tier

    Immutability is F#'s default: you get a changed value by building a new one. C#'s with on records works the same way, but there it is something you choose rather than the default for every value.

  7. Question 7 of 9 Why does the C# DiscountRate need its _ => throw arm, while the F# discountRate needs no catch-all case?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: An F# union's cases are fixed where it is defined, so a match can be checked; C# 14 cannot know that no other class derives from Tier

    Exhaustiveness needs a closed set of cases. F# unions are closed; C# 14 records are not, and C# 15 previews union types and closed hierarchies to close that gap.

  8. Question 8 of 9 Which of these is the strongest case for writing a component in F#?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: A library of tax, discount and eligibility rules

    F# pays off where the code is mostly rules and data. Glue code that mostly calls a framework is better left in the team's main language.

  9. Question 9 of 9 Your F# library returns Customer option to its C# callers. What do the F# component design guidelines suggest instead?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: A method in the TryGetValue pattern, a bool result with an out parameter

    Use F# types freely inside the library and translate at the edge: the TryGetValue pattern, .NET collection interfaces and methods with tupled parameters are what C# callers expect.

Interview questions

Warm-up (fresher to 2–5 years): what is F#, and how does it relate to C# and .NET?

F# is a functional-first, statically typed language for .NET. Its compiler ships inside the .NET SDK and writes the same IL into the same kind of assemblies as the C# compiler, so F# code runs on the .NET runtime and uses the base class library and NuGet packages exactly as C# does; the one extra library is FSharp.Core, which holds F# lists, options and printf. A C# project can reference an F# assembly and the other way round. The differences are the defaults: values are immutable, if and match are expressions with values, types are inferred, and discriminated unions with exhaustive pattern matching model a closed set of cases. Classes, interfaces and mutation are still available for interop and for state.

Two harder questions follow, about introducing F# into a C# system and about the boundary between the two languages. Note your answer first, then open each first step:

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

  1. Mid to senior

    Your team maintains a large C# system whose pricing rules keep breaking when a new customer type is added. A colleague proposes moving the rules to F#. How would you decide, and if you go ahead, how would you introduce F# with the least risk?

    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

    You are writing an F# library that a C# web application will call. Inside, it uses discriminated unions, options and curried functions. What should its public API look like, and why?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • An F# program is a .NET program: the compiler in the .NET SDK writes the same IL as C#, the same runtime runs it, and FSharp.Core is the only extra library.
  • Four version numbers describe an F# setup, the SDK, the F# language, FSharp.Core and the runtime, and each is set in its own place, from global.json to the project’s target framework.
  • F# and C# differ in their defaults: immutable values, expressions everywhere, inferred types and closed unions that the compiler checks for missing cases.
  • F# pays off in code that is mostly rules and data, and costs training, teammates who can read it, community tooling and a careful boundary with C#; scripts or one library are the usual first steps.

Terms in this lesson

discriminated union
An F# type made of a fixed set of named cases, each with its own data, such as Standard | Member of years: int. Because the set is closed, a match over it is checked for missing cases.
FSharp.Core
F#'s own library: the list, option and Result types, printf, and the List, Seq and Option modules. Every F# program uses it, and a built F# program ships FSharp.Core.dll beside its own assembly.
functional-first
A language whose defaults favour functions and immutable values: everything is an expression and values cannot change unless declared mutable, while classes and mutation stay available.
IL
Intermediate language: the instructions that every .NET compiler, F# and C# alike, writes into assemblies. The .NET runtime compiles them to machine code just before each method first runs.
record
An F# type with named fields that is immutable and compared by value by default. { r with Field = v } makes a copy with one field changed and leaves r as it was.
type inference
The compiler working out the types of values and of a function's parameters from how they are used, so most F# code needs no type annotations and is still statically typed.

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.