F# has a great dependency management tool called Paket. It’s easy to setup and use, whether you’re working with F# projects or F# scripts.
In this blog post, we’ll create a solution, and project from the ground up using Paket for dependency management. We’ll then convert our F# cli app into a .fsx
script.
You can find the completed code at this repository: https://github.com/karanveersp/paket-guide
Paket Overview
Paket is a dependency management tool similar to npm
in node, and pip
in python.
It can be used to reference NuGet
packages, files on GitHub, or any HTTP accessible file.
With a few cli commands (outlined below), you can easily add and update dependencies for your F# project. While dependencies for F# projects are easy enough to manage using NuGet and IDE features, managing dependencies for F# scripts is challenging.
That’s where I feel Paket really shines, and streamlines the process of acquiring and consuming libraries from .fsx
scripts.
You can find the official docs here: https://fsprojects.github.io/Paket/
Initializing a Project
We are going to create a solution and a project to compute and display the size of a given directory in gigabytes. It’s a simple enough program which doesn’t really need any external libraries to write.
However, I found a nice library we can use to output colorful text using printfn
. The library is located here: https://www.nuget.org/packages/BlackFox.ColoredPrintf/
Basically, we’ll output the result in one color, and any errors in red.
The following steps are pretty generic which can be referenced for initializing any Paket-based project.
-
Create a new directory and initialize a solution.
mkdir PaketGuide cd PaketGuide dotnet new sln
-
Install Paket as a local tool.
dotnet new tool-manifest dotnet tool install paket dotnet tool restore
-
Initialize Paket. This generates a file that will contain references to our dependencies.
dotnet paket init
-
Initialize a git repository, and create a
.gitignore
file with the following entries.git init
# Paket .paket/ paket-files/
-
Create a F# console project and include it in the solution.
dotnet new console -lang F# -o DirectorySizeCli dotnet sln add --project DirectorySizeCli\DirectorySizeCli.fsproj
Paket Files
There are three important files to be aware of to use Paket.
paket.dependencies
- Specifies dependencies and versions for entire codebase. Resides in solution root (same level as.sln
file).paket.references
- Specifies a subset of dependencies for a project in a solution. Resides in project root (same level as.fsproj
file).paket.lock
- A lock file generated by Paket. It contains all the versions of all transitive dependencies which can be used to get reproducible builds.
The paket.dependencies
and paket.references
files can be manually edited.
The paket.lock
file should be left alone and only edited by Paket.
These three files should be committed into source control.
Commands Cheat Sheet
The following table summarizes the most important commands.
Command | Description |
---|---|
dotnet paket init | Generate a new paket.dependencies file. |
dotnet paket add | Add a new dependency. By default, it is added to the solution, and then referenced from a project via a paket.references file. Creates a paket.dependencies file as well. |
dotnet paket install | Run after updating paket.dependencies file with new package references. Updates lock file, and refreshes all projects that specify paket dependencies to import references. |
dotnet paket update | Updates references to latest versions of all dependent packages. |
dotnet paket restore | Takes current paket.lock file and updates all projects to reference correction versions of NuGet packages. Should be called by your build script. |
dotnet paket outdated | List dependencies that have updates. |
dotnet paket generate-load-scripts --framework net6.0 | This command is used to generate include scripts which can be loaded in .fsx files or F# interactive. |
Directory Size App
We’re going to write a simple app to print the size of a given directory.
open System
open System.IO
open System.Linq
/// Converts bytes to gigabytes
let toGb numBytes = (float numBytes) * 1e-9
/// Returns the size of a directory in gigabytes.
let getDirectorySize dirPath =
DirectoryInfo(dirPath)
.EnumerateFiles("*", SearchOption.AllDirectories)
.Sum(fun fi -> fi.Length)
|> toGb
let dirPath = Environment.GetCommandLineArgs().[1]
try
dirPath
|> getDirectorySize
|> sprintf "The size is: %.3f gb"
|> printfn "%s"
with
| :? DirectoryNotFoundException as ex -> printfn $"{ex}{ex.StackTrace}"
The two functions at the start do our heavy lifting. We create a simple pipeline from the command line argument to print the directory size.
A try...with
block is added to detect invalid directories. If the provided directory isn’t found, the exception and stack trace are printed.
It’s a useful app but a bit boring! Some color ought to spice it up. Lets use Paket to add the following dependencies to our project.
dotnet paket add BlackFox.ColoredPrintf --version 1.0.5
dotnet paket add FSharp.Core --version 6.0.1
This will add an entries to the paket.dependencies
and paket.references
files.
You can remove the extra frameworks if you’d like to keep things minimalistic.
The paket.dependencies
file should look like this.
source https://api.nuget.org/v3/index.json
storage: none
framework: net6.0
nuget BlackFox.ColoredPrintf 1.0.5
nuget FSharp.Core 6.0.1
Run dotnet restore
inside your DirectorySizeCli
project directory.
We’re ready to dive into some color!
Lets create a new .fs
file for our Console
module.
If you’re using VSCode with the ionide extension as I am, then you can use it to add a new file above Program.fs
.
The ColoredPrintf
library allows specifying the foreground and background colors using a very concise syntax.
colorprintfn "$red[Hello!]"
The above function will print a red Hello!
in the terminal. If you want to change the background, it can be included with a semicolon.
colorprintfn "$red;yellow[Hello!]"
It produces output like the following.
We want some functions that will take a string and output a string in some conventional color that corresponds to the type of message. Include this in Console.fs
.
module Console
open BlackFox.ColoredPrintf
let complete = colorprintfn "$magenta[%s]"
let ok = colorprintfn "$green[%s]"
let info = colorprintfn "$cyan[%s]"
let warn = colorprintfn "$yellow[%s]"
let error = colorprintfn "$red[%s]"
Back in Program.fs
, open the new Console
module, and replace the printfn
function with the Console
counterparts.
open Console
//...
try
dirPath
|> getDirectorySize
|> sprintf "The size is: %.3f gb"
|> Console.info
with
| :? DirectoryNotFoundException as ex -> Console.error $"{ex}{ex.StackTrace}"
Run the code using a directory of your choosing as a command line argument.
dotnet run C:\users\karan\csharp
Now that cyan looks sweet!
Lets run a test for the exception case. What a glorious wall of red…
Dependencies in Scripts
Managing dependencies for F# and C# projects is straightforward using the above commands.
But what if you write some F# scripts (.fsx
files) which need to use external packages?
Those can be directly invoked using dotnet fsi myscript.fsx
and are often sufficient for simple tasks that don’t need the full project structure.
Paket offers a command to generate F# and C# include scripts that reference installed packages. These include scripts can be used in F# Interactive (FSI) or .fsx
files to load packages.
When the dotnet paket generate-load-scripts
command is run, it creates .fsx
files under .paket/load/
.
The generated load scripts reference DLLs from installed packages using #r
preprocessing directives.
Those are tedious to write, and Paket helps us out here.
The dotnet paket generate-load-scripts
command only works after packages have been restored. But we don’t want to keep re-running this command whenever we add a new package.
To generate load scripts when installing packages, place generate_load_scripts: true
at the top of the paket.dependencies
file.
generate_load_scripts: true
source https://api.nuget.org/v3/index.json
storage: none
framework: net6.0
nuget BlackFox.ColoredPrintf 1.0.5
nuget FSharp.Core 6.0.1
In a .fsx
file you can then reference the external package.
#load @".paket/load/BlackFox.ColoredPrintf.fsx"
open BlackFox.ColoredPrintf
Lets create two files in our solution root, Console.fsx
and DirSize.fsx
.
Add the following into Console.fsx
.
#load @".paket/load/BlackFox.ColoredPrintf.fsx"
module Console =
open BlackFox.ColoredPrintf
let complete = colorprintfn "$magenta[%s]"
let ok = colorprintfn "$green[%s]"
let info = colorprintfn "$cyan[%s]"
let warn = colorprintfn "$yellow[%s]"
let error = colorprintfn "$red[%s]"
DirSize.fsx
loads this Console.fsx
script. Note that our command line arg index is incremented by 1.
This is because FSI script arguments contain the fsi program itself, and the script as the first two arguments.
The command line arg is the third argument, so index 2
.
Example: fsi script.fsx arg1 arg2...
#load "Console.fsx"
open Console
open System
open System.IO
open System.Linq
/// Converts bytes to gigabytes
let toGb numBytes = (float numBytes) * 1e-9
/// Returns the size of a directory in gigabytes.
let getDirectorySize dirPath =
DirectoryInfo(dirPath)
.EnumerateFiles("*", SearchOption.AllDirectories)
.Sum(fun fi -> fi.Length)
|> toGb
let dirPath = Environment.GetCommandLineArgs().[2]
try
dirPath
|> getDirectorySize
|> sprintf "The size is: %.3f gb"
|> Console.info
with
| :? DirectoryNotFoundException as ex -> Console.error $"{ex}{ex.StackTrace}"
You can run the script using dotnet fsi
to make sure the output is the same as the CLI app.
dotnet fsi DirSize.fsx C:\users\karan\csharp
Conclusion
We’ve just built a project using Paket from scratch, and used a script to load a dependency.
I know that when I got started using F# scripts, importing libraries was incredibly awkward. Using pip
with Python was just smoother and easier, so I just gave up. Once I discovered Paket, I was able to take my F# scripts as far as any Python script.
This was a basic example using NuGet, but I’m eager to explore the ability to reference files on GitHub and how easy that makes sharing libraries. Great topic for a future post.
I hope that this blog post helps you become confident with using Paket and F#.
Happy scripting!