loading...

How to Setup a minimal F# Project

ducaale profile image Mohamed Dahir ・4 min read

In a previous post, I highlighted how to use F# without setting up a .NET project. This time, we will look into the process of setting up a minimal F# project, adding unit-tests, and finally publishing it.

Let's start by scaffolding an F# project using dotnet new command

dotnet new console -lang "F#" -o hello-world

You should see a new folder named hello-world in the directory where you just ran the command

hello-world
├── obj
├── hello-world.fsproj
└── Program.fs

let's take a quick look at Program.fs

// Learn more about F# at http://docs.microsoft.com/dotnet/fsharp

open System

// Define a function to construct a message to print
let from whom =
    sprintf "from %s" whom

[<EntryPoint>]
let main argv =
    let message = from "F#" // Call the function
    printfn "Hello world %s" message
    0 // return an integer exit code

To run our project, we use dotnet run which will invoke main function in Program.fs

dotnet run
# Hello world from F#

Importing files

To acquaint ourselves with how files and modules work in F#, let's create a second file.

// Utils.fs

module Utils

let permute list =
  let rec inserts e = function
    | [] -> [[e]]
    | x::xs as list -> (e::list)::[for xs' in inserts e xs -> x::xs']

  match List.length list with
  | 0 -> []
  | _ -> List.fold (fun accum x -> List.collect (inserts x) accum) [[]] list

Update Program.fs to use our Utils module

[<EntryPoint>]
let main argv =
    let input = argv.[0] |> List.ofSeq

    let listToStr = List.toArray >> System.String

    input
    |> Utils.permute 
    |> List.iter (listToStr >> printfn "%s")

    0 // return an integer exit code

Run the program again

dotnet run -- 123
# error FS0039: The value, namespace, type or module 'Utils' is not defined.
# The build failed. Fix the build errors and run again.

It seems that our build system is unable to resolve the newly created file which in turn led to Utils module not being recognized. Before we fix this, let's take a quick look at hello-world.fsproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <RootNamespace>hello_world</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>

</Project>

If you look carefully, you might notice that ItemGroup tag references Program.fs. F# build system uses this list to resolve files it needs to compile.

  1. Files listed here must be sorted in topological order (no cyclic dependencies are allowed).
  2. Any file listed here can be used without an import statement.
  3. Whenever a file is moved to another directory, fsproj.xml is the only place you will need to update
  4. There are tools to automate the process of populating fsproj.xml.

With that being said, let's update fsproj.xml to reference Utils.fs

  <ItemGroup>
    <Compile Include="Utils.fs" />
    <Compile Include="Program.fs" />
  </ItemGroup>

Running the program again should work without any errors

dotnet run -- 123
# 321
# 231
# 213
# 312
# 132
# 123

Adding tests

We are going to use NUnit for our testing which means we need to install it along with some other packages

dotnet add package NUnit
dotnet add package NUnit3TestAdapter
dotnet add package Microsoft.NET.Test.Sdk

Check if your fsproj.xml references the new packages that were installed

  <ItemGroup>
    <Compile Include="Utils.fs" />
    <Compile Include="Program.fs" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
    <PackageReference Include="NUnit" Version="3.12.0" />
    <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
  </ItemGroup>

And now it's time to write some unit tests. In F#, tests are normal functions annotated with the special Test attribute. This means were are free to write our unit-tests anywhere as long it is annotated properly. In this case, I chose to mimic Rust which stores its unit-tests in a nested module inside the same file that is being tested while storing integration-tests inside a tests folder.

// Utils.fs

module Utils

let permute list =
    // code omitted


module Tests =
    open NUnit.Framework

    [<Test>]
    let ``When an empty list is permuted expect to get empty lists`` () =
        let permutationCount = [] |> permute |> List.length
        Assert.AreEqual(0, permutationCount)

    [<Test>]
    let ``When [1;2;3] is permuted expect to get 6 lists`` () =
        let permutationCount = [1;2;3] |> permute |> List.length
        Assert.AreEqual(6, permutationCount)

We can use the following command to run our tests

dotnet test
# Starting test execution, please wait...
#
# A total of 1 test files matched the specified pattern.
#
# Test Run Successful.
# Total tests: 2
#      Passed: 2
# Total time: 3.3699 Seconds

Building a shareable executable

We can use dotnet publish to build an executable that can be shared with the world. We are using self-contained flag to bundle .NET runtime with our executable. This will let us run our program in any environment regardless of the presence of .NET runtime or not. Note that this comes at expense of file size.

# Remember to customize -r flag according to your platform https://docs.microsoft.com/en-us/dotnet/core/rid-catalog
dotnet publish -c Release -r win10-x64 --self-contained

You can now publish the folder that is created at hello-world\bin\Release\<dotnet version>\<rid>\

References

Posted on by:

Discussion

markdown guide