Originally published 2021-12-16 on my blog at carpenoctem.dev
Everything you need to start hacking F# on Linux!
- Why this page exists
- Installation & Initial Configuration
- .NET Versions
- Projects and Solutions
- Slow Startup Time
- Package Management
- FSI – F# Interactive
- Standalone Executables
- Visual Studio Code
- GUI Development
People learning the F# language today are blessed with excellent books, blogs, quality official online documentation, and other resources. However, these resources tend to assume that the student is either using Windows, familiar with .NET development with C#, or using a particular IDE/Editor.
Often, something that “just works” on Windows with Visual Studios may take some creativity to get working on Linux. Sometimes (though not often) it doesn’t work at all.
The goal of this article is to fill that gap by documenting my own experience of learning F# as a Linux-centric developer who has not programmed on Windows or .NET for 15+ years. It will not cover the language itself, but rather the tooling, ecosystem and things that confused me along the way.
To install F#, you need to install the official .NET SDK from Microsoft, which includes F#. Don’t worry, it is open source under the MIT license, and it runs beautifully on Linux.
Thankfully, installing the .NET SDK is trivial: Microsoft maintains official package repositories for Ubuntu, Debian, CentOS/RHEL, Fedora, OpenSUSE, SLES, and Alpine. Arch Linux has a community maintained package. There is also manual installer for distros not listed here. Furthermore, note that if you use VS Code, you likely already have the correct repository.
They even support ARM, so get your Raspberry Pi’s ready!
Once you have configured one of these official repositories, you’ll need to install a packaged named
dotnet-sdk-6.0 or similar. On Ubuntu, it’s just:
sudo apt install dotnet-sdk-6.0
There is also
dotnet-runtime-6.0, which allows you to run .NET applications but not build them. Useful for servers and docker images. (There is also way to build standalone binaries which do not even require the runtime. See the Standalone Executable section below).
That’s it! You should have the
dotnet command line tool installed on your system. You won’t need to run any other sudo commands.
dotnet tool is your one-stop-shop for managing your .NET installation, installing packages, creating projects, and so on. It is similar to npm for node. However,
dotnet handles multiple versions of the SDK and runtime seamlessly, so you do not need a separate ‘version manager’ like nvm, rvm, perlbrew, or virtualenv.
After installation, put something similar to this in your
.zshrc, or other shell initialization file:
export DOTNET_CLI_TELEMETRY_OPTOUT=1 if [ -d "$HOME/.dotnet/tools" ]; then export PATH=$HOME/.dotnet/tools:$PATH fi
The first line prevents the
dotnet command line tool from sending Microsoft anonymized usage information. No, it is not cool that this is opt-out instead of opt-in, but at least it is supposedly anonymized, and not hidden or obfuscated.
The rest sets up your path to include the
~/.dotnet/tools directory, where various tools you install via
dotnet tool install are located. More on this later.
No doubt you’ve heard of the open source implementation of the .NET Framework started by Miguel de Icaza in 2004.
Mono still exists and is not deprecated. In fact, Mono is used by the Unity gaming engine. Xamarin, the .NET-based platform for developing iOS and Android applications, also uses Mono (although they may be switching to the official .NET soon). Mono will also likely be used indefinitely by pre-existing free software such as Tomboy.
However, you should use the official .NET SDK from Microsoft for F#. The official .NET SDK is more complete and up-to-date, especially for F# developers. Furthermore, the official SDK dominates F# developer mindshare, meaning that third party F# libraries will likely be written for the official SDK.
If you just want to start hacking F#, all you need to know is:
.NET 6 is the current version of the .NET platform, and F# 6 is the current version of the F# language.
However, eventually you will want to know some of the history of .NET, because libraries and projects you find online will target various older versions, and you need to understand what’s going on. Come back to this section when you do.
Click here for a brief and probably wrong history of .NET versions
In the beginning, 2001 to be specific, there was .NET Framework (yes, ‘Framework’ is part of the name). It was proprietary and Windows-only, and remains so to this day, though some parts were open sourced.
In 2014, Microsoft released .NET Core as a separate, alternative implementation of .NET. It was cross-platform and open source under the MIT license. It proved immensely popular and revitalized interest in .NET. There were several versions of .NET Core, with 3.1 released in December 2019.
Around this time, the decision was made to consolidate .NET Core and .NET Framework. In November 2020, .NET Core was renamed .NET, and MS announced .NET Framework would no longer be developed. The first version of .NET was 5, not 4, to avoid confusion with the existing .NET Framework 4.x.
(Yes, it’s just “.NET”, with no suffix or prefix. This has made it difficult to differentiate whether one is talking about .NET in general (which may include Framework), or more specifically the recent releases from Microsoft 🤷🏼).
And that’s how you end up with “.NET 6”, the current version.
Minor Caveats 1. Although .NET Framework is no longer actively developed and version 4.8 will be its final version, it will continue to exists indefinitely because the last versions are installed by default on Windows 10 and various versions Windows Server. You may encounter older code, or code written by Windows-only developers targeting these versions.
Minor Caveat 2. There’s also something called .NET Standard. Unlike the others, .NET Standard is merely a specification, and not a software package you can download and install. It seems to be an earlier attempt to unify the different frameworks. Specifically, if you can build a .NET library that targets .NET Standard, it will run on both .NET Framework and .NET Core and .NET. With the consolidation of the various versions, the .NET Standard specification was deprecated. However, if you find a project targeting .NET Standard, it should work on current versions unmodified.
In .NET, a Project is basically a compile-able unit of source code. An executable console application Project might be created with:
dotnet new console -lang 'F#' -o YourFirstApp
And a library might be created with:
dotnet new classlib -lang "F#" -o MyFirstLib
However, in the world of .NET, there is a higher level of organization called the Solution. Solutions contain Projects, and Projects within Solutions can reference each other. This makes it easy share libraries between different executables. Also, in .NET, your tests should exist as a separate project.
Here’s an example of creating a Solution with a console application referencing a library:
# Create the solution dotnet new sln -o MySolution cd MySolution dotnet new classlib -lang "F#" -o src/MyLib dotnet new console -lang "F#" -o src/App # Adding projects to a solution dotnet sln add src/MyLib/MyLib.fsproj dotnet sln add src/App/App.fsproj # Reference the library from the console app dotnet add src/App/App.fsproj reference src/MyLib/MyLib.fsproj dotnet run --project App
If you are acustomed to interpretted languages such as Python, you will notice that
dotnet run seems very slow… a simple Hello World application will take over 2 seconds to launch! Don’t worry, compiled applications will start up much quicker, but it is quite annoying during development.
Unfortunately, there is no way to reduce startup time significantly.
Two possible remedies are:
dotnet watch runso that the application is run every time you save a change in a source file.
- Do you experimental coding in FSI.
dotnet cli tool can be used to install various tools. You can either install them globally (in
~/.dotnet/tools) or locally, within a project or solution. Global installations are more conveninent during development (less typing), but local installations make more sense when you are using CI/CD. It is ok to have a tool installed both locally and globally.
Regardless, one tool you’ll definitely want to configure is the Paket package management software:
dotnet tool install paket --global
Assuming you added
~/.dotnet/tools to your
$PATH as mentioned above, you should be able to run
paket now in your shell.
Installing locally in a solution or project involves an extra step:
dotnet new tool-manifest dotnet tool install paket
To run the locally installed tool, you’ll need to run it as
dotnet paket. There is no need to mess with your
$PATH in this case. Be sure to also add the newly generated manifest file
.config/dotnet-tools.json to git.
.NET has a public repository of packages called NuGet. It is akin to pip for Python, npm for Node.js, CPAN for Perl, etc.
NuGet packages are installed at the Project level (as opposed to the Solution level) with a command like:
dotnet add package Giraffe
Then you can reference any module/namespace provided by the package with
One important note, from an open source perspective: unlike repositories for other languages, packages on NuGet may not be FOSS. For example, IronPDF is completely closed-source and proprietary, yet it is distributed via NuGet. Therefore, please check out the package’s license carefully before using a random package off of NuGet!
Paket is an alternative dependency manager for .NET, written in F#. It can use NuGet packages, as well as point directly to Github repos and URLs. See their FAQ for why you may want to use Paket over the native package management built into
Note: All examples in this section will assume you’ve installed Paket globally (see the Tools section). If you want to use a local paket, change all calls of
paket below to
Paket can be configured at the Solution level or the Project level. Let’s start with a solution:
dotnet new sln -o PaketTest1 cd PaketTest1 dotnet new console -o App1 -lang 'F#' dotnet sln add App1/App1.fsproj paket init
paket init creates a
paket.dependencies file (which you should add to your git repo). After initialization, the first thing you must do is open
paket.dependencies and fix the
framework line to point to the correct version, if necessary. For example, with Paket 6.2.1, the
init command creates the following:
source https://api.nuget.org/v3/index.json storage: none framework: net5.0, netstandard2.0, netstandard2.1 # WRONG!
You need to change the framework line to
source https://api.nuget.org/v3/index.json storage: none framework: net6.0 # CORRECTED
I have no idea why Paket does not specify the correct framework by default. It might be that Paket is not updated for .NET 6.0 at the time of writing, though I had similar problems during .NET 5.0.
After fixing the dependencies file, you must install the
FSharp.Core, which includes the standard libraries for F#:
paket add FSharp.Core
FSharp.Core is not installed by default because Paket can be used for C# applications as well. You can install any other NuGet package with the
add subcommand, shown below with an optional version number:
paket add Suave --version 2.5.6
After installing these packages, the
paket.dependencies file will look like:
source https://api.nuget.org/v3/index.json storage: none framework: net6.0 nuget FSharp.Core nuget Suave 2.5.6
You can edit this file directly, but make sure to run
paket update to tell Paket of your changes. You will also notice a few new files:
paket.lockcontains the dependency tree as discussed earlier.
- Within Projects will be a new
paket.referencesfile. This is a simple text file containing a list of packages used by that project. If you change this file, you will need to run
paket updateto propagate the changes to your .fsproj file.
Paket directly in Projects. Paket can also be initialized in a bare Project, without a solution:
dotnet new console -o PaketTest2 -lang 'F#' cd PaketTest2 paket init ### FIX paket.dependencies as described above paket add FSharp.Core paket add Suave dotnet run
In this case, the dependencies, lock, and reference files will all be created in the same directory.
F# comes with a REPL called FSI or F# interactive, which can be launched with
$ dotnet fsi Microsoft (R) F# Interactive version 220.127.116.11 for F# 6.0 Copyright (c) Microsoft Corporation. All Rights Reserved. For help type #help;; > printfn "Hello, %s" "World";;
The Official Doc is adequate so I won’t go into too much more.
A couple things to know:
- F# scripts should have the
#load "file.fsx"syntax allows you to load other fsx files.
#r "..."syntax allows you to load packages.
;;is used to terminate statements, or groups of statements.
- You can use the shebang
#!/usr/bin/env -S dotnet fsiand run it like any other script on your system.
- Ctrl-D quits the REPL.
NuGet packages can be loaded during an FSI session like this:
#r "nuget: Suave";; // and then use it as usual: open Suave;;
For the same reason you may want to use Paket in regular F# code (for example, version consistency across multiple scripts), you may want to use it within FSX scripts. To be honest, this was not easy to figure out on Linux, and in fact, my problems with getting Paket working on Linux is what prompted me to write this entire article.
First, you need to get the package
FSharp.DependencyManager.Paket onto your system. The easiest way to do that is in FSI:
#r "nuget: FSharp.DependencyManager.Paket";;
Now, there will be a cached copy of the package in the
~/.nuget/packages directory. We need to pass this to the
--compilertool option of fsi (you will need to adjust it for your unix username and version of paket):
dotnet fsi --compilertool:"/home/YOURUSERNAME/.nuget/packages/fsharp.dependencymanager.paket/6.2.1/lib/netstandard2.0"
I recommend having an alias like below, and updating it whenever you update paket:
alias fsi='dotnet fsi --compilertool:"/home/YOURUSERNAME/.nuget/packages/fsharp.dependencymanager.paket/6.2.1/lib/netstandard2.0"'
Now, if you run FSI within a Solution or Project, you will be able to load the package according to the versions in
paket.lock, assuring version consistency between multiple Projects and FSX scripts:
#r "paket: nuget Suave";;
Warning – There is an bug that prevents multiple users on your machine from loading NuGet and Paket packages in this way.
The cause of this bug is that the packages are stored in
/tmp/script-packages with the permission 775, preventing other users (not in the same group) from creating new subdirectories. To workaround this, simply remove these directories (or maybe permission them correctly) if switching users.
F# applications can be compiled into standalone, self-contained binaries. They can be distributed just like statically compiled applications written in C, Rust, or Go. Of course, these binaries will tend to be large because they include the .NET runtime (a Hello World application comes in at around 65M). This may be a consideration if the target environment is constrained (maybe an embedded device) or if you want to create dozens of of individual applications.
In order to build self-contained applications, add the lines highlighted below:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net5.0</TargetFramework> <!-- FROM HERE.... --> <PublishSingleFile>true</PublishSingleFile> <SelfContained>true</SelfContained> <RuntimeIdentifier>linux-x64</RuntimeIdentifier> <PublishReadyToRun>true</PublishReadyToRun> <!-- TO HERE --> </PropertyGroup> </Project>
And then run:
Your binary will be available in
For more information, see here
dotnet CLI tool uses Templates to initialize new projects and other components. They are akin to various project scafolding systems like
create-react-app for React.
To see a list of templates installed on your machine, run
dotnet new --list:
Template Name Short Name Language Tags -------------------------------------------- ------------------- ---------- --------------------------------------------------- Console Application console [C#],F#,VB Common/Console Class library classlib [C#],F#,VB Common/Library Gtk Application gtkapp [C#] Gtk/GUI App Gtk Dialog gtkdialog [C#] Gtk/UI Gtk Widget gtkwidget [C#] Gtk/UI Gtk Window gtkwindow [C#] Gtk/UI MSTest Test Project mstest [C#],F#,VB Test/MSTest NUnit 3 Test Item nunit-test [C#],F#,VB Test/NUnit NUnit 3 Test Project nunit [C#],F#,VB Test/NUnit xUnit Test Project xunit [C#],F#,VB Test/xUnit MVC ViewImports viewimports [C#] Web/ASP.NET Razor Component razorcomponent [C#] Web/ASP.NET MVC ViewStart viewstart [C#] Web/ASP.NET Razor Page page [C#] Web/ASP.NET Blazor Server App blazorserver [C#] Web/Blazor Blazor WebAssembly App blazorwasm [C#] Web/Blazor/WebAssembly ASP.NET Core Empty web [C#],F# Web/Empty ASP.NET Core Web App (Model-View-Controller) mvc [C#],F# Web/MVC ASP.NET Core Web App webapp [C#] Web/MVC/Razor Pages Razor Class Library razorclasslib [C#] Web/Razor/Library ... and a whole lot more
The “Short Name” is what you pass to
dotnet new. The Language column specifies the default language of the template and the availability of other languages. To create a Class Library, therefore, you would need to run
dotnet new classlib --lang 'F#' -o MyClassLib, because otherwise it would default to C#.
Also note that most of the templates that come default are for C# only. That’s ok, since the core use cases (
classlib, and testing) are covered, and the F# community has developed templates for other use cases.
In order to install, say, the Expecto testing framework template, you would run:
dotnet new -i "Expecto.Template::*"
The template list (
dotnet new --list) will show you that the Short Name for this template is unsurprisingly ‘expecto’, so you would run something like the following to create your testing project:
dotnet new expecto -o AwesomeTestProj
Note that you do not need to specify
--lang 'F#' here because it F# is the default (and only) language for this template.
When you install a template using
--install, they are installed in
Under the hood, Templates are just specially tagged NuGet packages. To find out the underlying package for a Template, run
dotnet new -u.
The following lines may be useful in your .gitignore:
If you are a Vim/NeoVim user, you will be happy to know that F# support is surprisingly good. With the help of the F# Language Server plugin Ionide-Vim, you can get access to contextual code completion (called ‘Intellisense’ in Visual Studios), diagnostics, and much more.
On NeoVim, the built-in LSP client works without modification. On Vim, you will need LanguageClient-neovim.
Visual Studio Code, unsurprisingly, has excellent support for F# through Ionide. Simply
Ctrl-Shift-X to the extension management screen to install.
You can also impress the kids by using FSI through a “Notebook” interface with the .NET Interactive Notebooks extension. After installation, press
Ctrl-Shift-P and select “.NET Interface: Create new blank notebook”, choose “Create as
.ipynb” then “F#”.
TODO: Figure out how to get VS Code to recognize Paket
Your only real choice for GUI development with F#/.NET on Linux is GTK#. (I’ve gotten feedback on Twitter that this statement may be a bit harsh, will update when I dive a bit deeper into other options).
Despite investing heavily in Open Source for the .NET itself, Microsoft has never seriously supported GUI development on Linux. You could run old WinForm applications using Mono, and there are some efforts to run UWP applications on Linux. However, development tools for libraries are largely these tied to Visual Studios and Windows.