To get started on F#, this article covers some necessary tools and basics to get you started on writing simple CLI programs.
From my experience, one of the most important activities for learning F# or any new programming language, is actually to just get simple programs to run from scratch and by yourself.
- What tools do I need to have?
- How do I get a project template? How do projects, packages, etc. work?
- How does standard I/O work for CLI programs?
- Building, running, testing, publishing?
It sounds basic when put in those terms, but between sessions of assignments or other work you might do to study more and more features of F#, I strongly encourage you to take a step back and practice these basics frequently.
Code judges are nice as tools for assignments in courses and competitions, and you should absolutely use them for practice and for fun. They are not a substitute for your ability to build and run a program by yourself.
This article is about getting started with coding, building and running F# regularly; in a way where you will be your own judge of things.
Run the code that you write. Test sample inputs yourself. Reason about inputs and outputs.
1. Developer environment preparation
1.1. Getting the .NET SDK
The scenario we consider is F# as used for .NET and ASP.NET Core programming. For this purpose, obtain the Source Development Kit (SDK) for the major version of .NET, 10.0 at present. Link to download page here.🔗 The language is generally very stable as versions go, but I have encountered older example code projects where slight updates to deprecated syntax are welcomed.
There are other convenient ways to install .NET that I know of:
- Windows users should consider using the WinGet package manager.🔗
- For Linux users, check your package managers. I.e. APT package manager users can do
apt search dotnet-sdkto find a reasonably current version.
The standard keyword for .NET from terminal is dotnet. For example to check available SDK versions,
dotnet --list-sdks
should show something like this with .NET 10:
10.0.109 [/usr/lib/dotnet/sdk]
Using the terminal for handling anything related to SDKs is my personal preference. I find it to be the easiest and most reliable way of managing the setup for any given project. More on this topic in section 4.
You may need to use an earlier or later version of .NET depending on what you are working on. The major versions of .NET are on a relatively short release cycle.
1.2. Selecting an editor or IDE
For an editor or IDE, I want to highlight two options that I have found to work well:
- Obtain a student license for JetBrains Rider. This IDE supports F# projects out of the box and has many useful additional (convenient, not essential) features.
- Use Visual Studio Code with the .NET Extension Pack. This will get you the static analysis and highlighting tools to support your coding.
If you are training programming or especially if you are learning this for a course, make sure to disable any generative AI features right away. Stick to the classic static analysis and syntax highlighting for feedback as you code.
There are many more options to explore, including editors that are truly open source or are lighter for a server to run if working remotely over SSH.
2. Console app project setup
2.1. Project templates
We want to get started with using project templates right away.
The .NET SDK comes with a number of project templates already installed, including some for F# development, with more available to install online. The dotnet new command is for searching through available templates and creating new projects.
The default language for .NET projects is C#. Pass -lang F# to get the F# version of a template instead.
For a console app project, first create an empty directory with the project name and get a terminal window in this directory. This is the command for the project template,
dotnet new console -lang F#
and you should see something like this:
The template "Console App" was created successfully.
Processing post-creation actions...
Restoring /home/rono/source/first-fsharp/first-fsharp.fsproj:
Restore succeeded.
2.2. Project structure
I find it worthwhile to go over what was just created part-by-part.
The obj directory
This directory gets populated with metadata to reference any project dependencies, when dotnet restore is invoked. It is also used for caching intermediate artifacts from building the project.
The bin directory
This directory will contain any builds; the results of invoking dotnet build or dotnet publish.
The project file first-fsharp.fsproj
This file defines a .NET project. Its source, dependencies, properties and more.
Most importantly for any F# project, it specifies which source files to include and what order to compile them in. Our initial template just has an item group with our one source file:
...
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>
...
With multiple source files, a module should be declared within each source file. These should be compiled by order of module dependency. Each source file must be placed after source files for modules that it opens. The “runnable” source file is usually last, as it should be able to open the other modules.
Note that for Kattis problems, the submitted
.fssource files are ordered alphabetically.
I also want to do a followup on projects with multiple source files at some point. In particular to cover how F# signature files work and tricks for an exam situation.
The source file program.fs
The initial F# source file with a Hello World program:
// For more information see https://aka.ms/fsharp-console-apps
printfn "Hello from F#"
For now, just check that it runs by invoking dotnet run and checking the output.
3. F# crash course for CLI apps
Now for some advice on how to actually start writing console apps in F#. There are definitely levels of skill to this, and I want to help you with initial ideas for how to practice.
My overall goal is that you should be able to take this text and get started writing one-pagers for open.kattis.com🔗 problems. I have been using Kattis myself for all sorts of programming practice for the past three years.
Importantly, the Kattis judge requires you to write a full working program that accepts standard input and output correctly, however small the problem. Doing this yourself is not only a significant part of the work on each problem, learning how to do it also tends to give you a lot of information about programming languages.
3.1. Program entry and arguments
Initially with the template, our F# program consists of top-level statements only. The program entry point🔗 is implicit.
For reference, let us first consider a simple “add two numbers” program with explicit entry point:
[<EntryPoint>]
let main argv =
let a = int argv[0]
let b = int argv[1]
printfn "%d" (a+b)
0
This is a function of type string array -> int. The input is the command line arguments (like in a Java or C# program) and the function returns an exit code (like in a C/C++ program).
The exact same program can be written with an implicit entry point instead:
let argv = System.Environment.GetCommandLineArgs()
let a = int argv[1]
let b = int argv[2]
printfn "%d" (a+b)
exit 0
- I added an explicit exit code statement for reference. Implicitly, the program exits with code 0 unless the exit is due to an unhandled exception.
- Notice the different argument indices. Try printing out
argv[0]for yourself.
It is simple to test either of these examples. E.g.:
dotnet run 2 2
3.2. Standard input and output
For this section, I want to stick to the basics. Entire lectures are out there on parser libraries for F# or the efficiency of writing your own buffered input tokenizer for particularly parsing-heavy problems. This is about when you are just starting out. As more advanced solutions become appropriate, you will be able to find them.
In terms of applications, input parsing really favors F# with its capacity for elegant higher-order behavior, inline operators and error handling. Working by examples, we will just cover the basics for getting started on Kattis problems:
- Reading lines from standard input (
System.Console.Readline) into an F# program. - Basic string tokenization and parsing primitive types in F#.
Here I have the “add two numbers” program from before, but using the standard input instead:
open System
let line1 = Console.ReadLine()
let tokens = line1.Split(" ")
let a = int tokens[0]
let b = int tokens[1]
printfn "%d" (a+b)
Reading lines from standard input
Console.Readline is a unit -> string function. It is invoked with the empty argument () and returns a string. This is an impure function with a side effect: it reads the string to return from standard input. The expression Console.Readline() therefore is of type string.
Unlike with C#, I have to open the System namespace explicitly if I want to avoid typing System for resources such as Console.
For more advanced use cases: standard input, output and error are also explicitly accessible as
TextReadertype properties of Console. These can also be opened as streams for combination with buffers or other reader types.
Note the \(2^{31}-1\) character limit on strings. That would be one case for doing something more advanced.
String methods
While there are some purely functional operators in the String module🔗 of F#, many of the important string methods are accessed as F# object programming members, and so I write line1.Split(" ") as if I were invoking an instance method.
What I have shown so far are fairly graceful examples for functional programming, of interfacing between C# and F# programming within .NET development.
In the documentation for .NET, i.e. for the String class🔗, you can switch to see F# code with the drop-down menu just above the title to the right of the breadcrumb. Unfortunately I have no way to link in a way that sets this by default.
Parsing to primitive types
The simplest conversion from string to primitive data types comes from casting and conversion🔗 operators, int being one of them. This uses the System.Int32.Parse function of type string -> int for the conversion.
Importantly, we only want to use this function when we are sure that the string converts into an integer, else an exception will be thrown. Likewise for the other casting and conversion operators.
With untrusted input or other cases of uncertainty, it becomes necessary to build more advanced logic around either handling the exception, or using System.Int32.TryParse and handling its return values correctly.
Writing text to standard output
Finally I want to present printfn and printf: an F# specific version of format printing to standard output with compile-time type checking. Think of “stdio.h” from C or “fmt” from Go. Most of the same formatting tricks apply: “%d” for integers, “%f” for floating point values. There is also a catch-all “%A” to print the default to-string of an argument.
Important variants of printfn and printf are:
sprintfwhich returns the output as a string.fprintfandfprintfnfor using other writers than standard output. Try using it for writing to standard error or a file writer.
Handling end-of-file
Most Kattis problems make parsing relatively easy by offering dimension parameters or other easy queues about how much to parse. Still, not all problems are that easy in practice. Two non-trivial parsing examples I want to highlight:
- What if there can be multiple spaces between tokens?
- What if the only queue to stop parsing is the end-of-file (EOF) marker?
To handle multiple spaces between tokens, the easiest way I find is to pass the relevant string split option like this:
let solve (l1:string) =
let tokens = l1.Split(" ", StringSplitOptions.RemoveEmptyEntries)
let r, s = int tokens[0], float tokens[1]
printfn "%d" (velocity r s)
// Assume a "velocity" function further up the page :)
When Console.Readline() encounters the EOF marker, no more lines are expected and null is returned instead of a string. There are several ways to use either a recursive function or type of loop to parse until the EOF marker.
Within F#, null should really only ever be used in the interface with other .NET libraries and systems. More about that here.🔗
Here I have an FP course friendly tail-recursive function:
[<TailCall>]
let rec readLines () =
let line = Console.ReadLine()
match line with
| null -> ()
| _ ->
solve line
readLines ()
// Invoke the function to form a program entry point.
readLines ()
When null is obtained instead of a line, the function returns unit and recursion ends. Otherwise it calls the function to handle the parsed line, followed by a recursive call.
To test a program written for this type of input, EOF can be sent in terminal by hitting Ctrl+D.
The
[<TailCall>]attribute is a little trick for .NET 8.0 onwards. Read more here.🔗 Completely optional, it tells the compiler to issue a warning if the function is non-tail recursive.
4. Building, running and testing
While there are ways to configure a run button within many IDEs and editors, I prefer using the terminal for building and testing projects. This way:
- I am sure that the project builds easily and reliably, with just the source code and SDK.
- It becomes easier to onboard other students or a developer team, not to mention customers and clients, on how the project builds.
- I have a solid foundation for scripting workflows. Essential to handle the DevOps part of a larger project.
- I have a reliable way to ensure that the code compiles. Double checking and triple checking exam hand-ins or F# examples for a web article is hard enough. 😉
The most important two commands are:
dotnet restoreis the command to resolve project dependencies. Any packages on NuGet are downloaded and referenced if needed.dotnet buildis the command to build the project. This will also rundotnet restoreunless specified.
Within these, we have various options to set whether to build a “Debug” or “Release” version, whether or not the build should be self-contained, what the build target is, etc.
Further commands are provided for convenience:
dotnet runwill restore the project, build the program in the “Debug” version and run it. Ideal for when we are just trying to learn some F#.dotnet testwill restore, build and run a test project. This is for projects using xUnit, NUnit or similar testing tools.dotnet publishbuilds the project, but it builds the “Release” version. This is important for stress-testing and especially for load testing an F# program when the continuation passing style (CPS) is used.
F# Interactive
I want to mention F# Interactive or dotnet fsi in terminal. It is a tool that many explorers and students of F# will find by themselves.
When used correctly, F# Interactive lets us use both our own F# source code and external libraries interactively. It is just like how Python can be typed into terminal.
When not used correctly, I find that some students will waste their time repeatedly copy-pasting entire modules of source code from multiple files into F# Interactive. Their F# work also gets disrupted by this practice; F# Interactive requires terminating statements with double semi-colon.
If the goal is some sort of test driven development, you should strongly consider just writing and running simple F# programs and save the hassle of reloading code into F# Interactive so many times.
If you want to use F# Interactive as part of your routine, you should do it well. Get familiar with the #load command for obtaining your modules, the #r command for DLL dependencies, and #quit;; for getting out. Make a small script that is easier to copy and paste, like this:
#r "Assignment3/bin/Debug/net9.0/FsLexYacc.Runtime.dll"
#load "Assignment3/tools/Absyn.fs"
#load "Assignment3/tools/ExprPar.fs"
#load "Assignment3/tools/ExprLex.fs"
#load "Assignment3/tools/Parse.fs"
#load "Assignment3/tools/Expr.fs";;
The dotnet fsi command can also be used with arguments to do the same, making it even easier to save time through scripting.