F# for Linux People
Originally published 2021-12-16 on my blog at carpenoctem.dev
Introduction
Everything you need to start hacking F# on Linux!
Table of Contents
- Why this page exists
- Installation & Initial Configuration
- .NET Versions
- Projects and Solutions
- Slow Startup Time
- Tools
- Package Management
- FSI - F# Interactive
- Standalone Executables
- Templates
- Git
- Vim
- Visual Studio Code
- GUI Development
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:
- Use
dotnet watch run
so that the application is run every time you save a change in a source file. - Do you experimental coding in FSI.
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.lock
contains the dependency tree as discussed earlier. - Within Projects will be a new
paket.references
file. This is a simple text file containing a list of packages used by that project. If you change this file, you will need to runpaket update
to 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.
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:
- F# scripts should have the
fsx
file extension. - The
#load "file.fsx"
syntax allows you to load other fsx files. - The
#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 fsi
and run it like any other script on your system. - Ctrl-D quits the REPL.
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.
Top comments (1)
Apologies to people who may follow twitter and have seen this already; I'm experimenting with cross posting on dev.to to get more readers. Anyways, please tell me what you think!