F# for Linux People
Tags: fsharp, dotnet, linux
Published: 2022-01-18 02:10:40

# F# for Linux People

<small>Originally published 2021-12-16 on my blog at [carpenoctem.dev](https://carpenoctem.dev)</small>

## Introduction

Everything you need to start hacking F# on Linux!

## Table of Contents

* [Why this page exists](#why)
* [Installation & Initial Configuration](#installation)
* [.NET Versions](#versions)
* [Projects and Solutions](#projects-and-solutions)
* [Slow Startup Time](#startup)
* [Tools](#tools)
* [Package Management](#packages)
* [FSI - F# Interactive ](#fsi)
* [Standalone Executables](#standalone)
* [Templates](#templates)
* [Git](#git)
* [Vim](#vim)
* [Visual Studio Code](#vscode)
* [GUI Development](#gui)

## <a name="why">Why this page exists</a>

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. 

## <a name="installation">Installation & Initial Configuration</a>

To install F#, you need to install the [official .NET SDK from Microsoft](https://docs.microsoft.com/en-us/dotnet/core/install/linux), 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](https://wiki.archlinux.org/title/.NET).  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](#standalone) 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](https://docs.unity3d.com/Manual/overview-of-dot-net-in-unity.html).  [Xamarin](https://dotnet.microsoft.com/en-us/apps/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](https://wiki.gnome.org/Apps/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.

## <a name="versions">.NET Versions</a>

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.

<details>
<summary>Click here for a brief and probably wrong history of .NET versions</summary>

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.

<small>(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 🤷🏼).</small>

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

## <a name="projects-and-solutions">Projects and Solutions</a>

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

## <a name="startup">Slow Startup Time</a>

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. 

## <a name="tools">Tools</a>

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. 


## <a name="packages">Package Management</a>

.NET has a public repository of packages called [NuGet](https://www.nuget.org/).  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](https://www.nuget.org/packages/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](https://fsprojects.github.io/Paket/faq.html#I-do-not-understand-why-I-need-Paket-to-manage-my-packages-Why-can-t-I-just-use-NuGet-exe-and-packages-config) 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](#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 run `paket 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.

## <a name="fsi">FSI - F# Interactive </a>

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](https://docs.microsoft.com/en-us/dotnet/fsharp/tools/fsharp-interactive/) 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.

## <a name="standalone">Standalone Executables</a>

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](https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file#publish-a-single-file-app---sample-project-file)

## <a name="templates">Templates</a>

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](https://github.com/dotnet/templating/wiki/Available-templates-for-dotnet-new#f-templates). 

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

## <a name="git">Git</a>

The following lines may be useful in your .gitignore:

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

## <a name="vim">Vim</a>

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](https://github.com/ionide/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](https://github.com/autozimu/LanguageClient-neovim).

## <a name="vscode">Visual Studio Code</a>

Visual Studio Code, unsurprisingly, has excellent support for F# through [Ionide](https://ionide.io/Editors/Code/getting_started.html).  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](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.dotnet-interactive-vscode) 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*

## <a name="gui">GUI Development</a>

Your only real choice for GUI development with F#/.NET on Linux is [GTK#](https://github.com/GtkSharp/GtkSharp).  (*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.