F# for Linux People

2021-12-16

Introduction

Everything you need to start hacking F# on Linux!

Table of Contents

Why this page exists

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.

Installation & Initial Configuration

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.

The 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

After installation, put something similar to this in your .bash_profile, .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.

What About Mono?

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.

.NET Versions

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.

Projects and Solutions

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

Slow Startup Time

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:

Tools

The 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.

Package Management

.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 open:

open Giraffe

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

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 dotnet.

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 dotnet paket.

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 net6.0:

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 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.

FSI - F# Interactive

F# comes with a REPL called FSI or F# interactive, which can be launched with dotnet fsi.

$ dotnet fsi                                                                             

Microsoft (R) F# Interactive version 12.0.0.0 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:

Using NuGet with FSI

NuGet packages can be loaded during an FSI session like this:

#r "nuget: Suave";;

// and then use it as usual:
open Suave;; 
Using Paket with FSI

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/nuget and /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.

Standalone Executables

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:

dotnet publish

Your binary will be available in bin/Debug/net5.0/linux-x64/publish/.

For more information, see here

Templates

The 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 (console, 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 ~/.templateengine.

Under the hood, Templates are just specially tagged NuGet packages. To find out the underlying package for a Template, run dotnet new -u.

Git

The following lines may be useful in your .gitignore:

[Dd]ebug/
[Rr]elease/
x64/
[Bb]in/
[Oo]bj/
.paket/
paket-files/

Vim

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

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

GUI Development

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.