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.
- Files listed here must be sorted in topological order (no cyclic dependencies are allowed).
- Any file listed here can be used without an import statement.
- Whenever a file is moved to another directory,
fsproj.xml
is the only place you will need to update - 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>\
Top comments (0)