A Brief Introduction to F#

First, a simple F# program

Let’s look at our favorite starter program, but written for F#.

printfn "Hello world!\n"

That is the entire program. Refreshing, isn’t it?

What is F#?

F# is a functional programming language. Functional programming languages are likely very different in style than the programs with which you are accustomed. Even if you decide that functional programming is not for you, exposure to functional programming ideas will change the way you think about coding.

Functional programming encourages expressions over statements, immutable instead of mutable variables, and pure, first-class functions instead of side-effecting procedures. F# is also strongly typed unlike C, which is weakly typed and Python, which is dynamically typed. These differences sharply contrast with the typical style of programming you encounter in procedural languages like C, Java, or Python. The end result is that functional programs read more like mathematical statements than procedural programs. Let’s briefly touch on each of these concepts.

Immutable variables

In a language like Python or C, a variable can be declared and written to many times. E.g.,

x = 2
x += 1  # the value of x is now 3
x += 1  # the value of x is now 4
x += 1  # the value of x is now 5

In a functional programming language, a variable can only be written to once, when it is declared.

let x = 2
x += 1   // can't do this in F#; will not compile

You might be wondering how on earth you “update” data. You do it like this:

let x = 2
let y = x + 1

x and y are not the same variable.

Variables in F# are immutable, meaning that once they are declared, their values will never change.

If you’re like me, this idea probably has left you scratching your head. Good. The value of this style of programming will become apparent to you in time.

Expressions

In a language like Python or C, a line of code can either return a value or not. For example, in Python:

x = 2   # returns nothing; this is a statement
x + 1   # returns the value 3; this is an expression

In a functional language, all language constructs are expressions.

let x = 2  // returns a binding of the value 2 to the variable 'x'
x + 1      // returns the value 3

When a line of code returns nothing, we call it a statement. Since it is pointless to have a line of code that does nothing, a statement does something by changing the state of the machine. Changing the state of the machine independently of a return value is called a side effect. Side effects are either banned in functional languages (e.g., pure Lisp, Haskell) or strongly discouraged (e.g., SML, F#).

Pure, first-class functions

A pure function is a function that has no side effects. In F#, we usually write pure functions.

In C, you can do the following:

int i = 0;

void increment() {
	i++;
}
increment();  // i has the value 1

Notice that the function takes no arguments and returns no values and yet, it does something useful by altering (the PL word is mutating) the variable i. You cannot do this in a functional programming language because variables are immutable and functions are pure. Instead, you’d write something like:

let increment i = i + 1
let i = 0
let i' = increment i  // i has the value 0; i' has the value 1

i and i' are different variables.

Functions in F# also take a little squinting at before you recognize their form. It helps to rewrite the above program using parentheses and types:

let increment(i: int) : int = i + 1
let i: int = 0
let i': int = increment(i)

This is also a valid F# program, and if you find yourself struggling with syntax, I encourage you to write in this style instead.

Functions in F# are also first class, meaning that they can be treated as values in the language. What does that mean? Well, that means that you can assign them to variables. For instance,

let increment(i: int) : int = i + 1
let addone = increment
addone(3)  // returns 4

The type of the variable addone is a function (specifically, a function from int to int), and since it’s a function we can call it just like we’d call increment.

And since values and variables can be passed into functions, you can pass variables with function type into functions too:

let increment(i: int) : int = i + 1
let doer_thinger(f: int -> int, i: int) : int = f(i)
doer_thinger(increment, 3)  // returns 4

And, just for fun, let’s get rid of the unnecessary syntax so you can see how simple this program can look:

let increment i = i + 1
let doer_thinger f i = f i
doer_thinger increment 3  // returns 4

Strong types

F# is a strongly-typed programming language. A strongly-typed language is one that enforces data types strictly and consistently. That means that the following kinds of programs are not admissible in F#. For example, the Python program,

x = 1
x = "hi"

or the C program,

int x = -3;
unsigned y = x;

Even with all the warnings enabled, clang, doesn’t flinch: no errors or warnings are printed. Nonetheless, it doesn’t make sense to disregard the fact that an int is not an unsigned int, because assigning -3 to y dramatically changes the meaning of the value. y is very much not -3 anymore. Try running a little code experiment in C if you don’t know what happens.

Both of the above programs are considered type errors in F#, and the program will not compile. To convert from an integer to an unsigned integer, we need to convert explicitly in F#:

let x: int = -3
let y: uint32 = uint32 x

Strong types help you avoid easy-to-make but costly mistakes.

Other features

F# has many other features, such as garbage collection (like Java), lambda expressions, pattern matching, type inference, concurrency primitives, a large, mature standard library, object-orientation, inheritance, and many other features. Don’t worry if you don’t know what these words mean now– we will discuss these features throughout the remainder of the semester.

Microsoft .NET

F# is a part of an ecosystem of languages and tools developed by Microsoft called .NET (pronounced “dot net”). Programs written in .NET are almost entirely interoperable, meaning that different parts of the same program can be written in different languages. For instance, I routinely write software that makes use of C#, F#, and Visual Basic in the same program.

.NET is also portable. Unless you specifically seek to write platform-specific code, .NET code can be run anywhere the .NET Common Language Runtime (CLR) is available. This language architecture is similar to, and heavily inspired by, the technology behind the Java Virtual Machine (JVM). The .NET Core CLR is available on Windows, the macOS, and Linux. Additional platforms (like Android, iOS, and FreeBSD) are supported by the open source Mono project.

We will be using the .NET Core framework on Linux for this class. If you would like to install .NET Core on your own machine, you may do so by downloading the installer.

Modularity

One feature that we will address right away is F#’s strong support for modularity. Modules are a way of organizing code so that similarly named functions and variables in different parts of code do not conflict. In C, where libraries are imported by the C preprocessor essentially by pasting all the code from all the libraries into a single file and then compiling that, name conflicts are an annoying and commonplace occurence. In F# and other .NET languages, this is not a problem, because names are scoped, meaning that they only have meaning within certain boundaries.

F# has a variety of constructs available to scope names: solutions, projects, namespaces, and modules. For now, we will focus on projects.

A project is a unit of organization defined by .NET. A project contains a collection of source code files, all in the same language. A project is either a library, meaning that it must be called by another project, or an application, meaning that it has an entry point and can run by itself.

Creating the HelloWorld project

In order to “house” our hello world program, let’s generate an application project. Having an application packaged in this way makes it self-contained and easy to manage during the development process.

We create new F# projects using the dotnet command on the UNIX command line. Because dotnet creates a project in the existing directory, you should first create a directory for your project.

$ mkdir helloworld

Now cd into the directory and create the project.

$ cd helloworld
$ dotnet new console -lang "F#"

By default, this command will generate a Hello World program!

// Learn more about F# at http://fsharp.org

open System

[<EntryPoint>]
let main argv =
    printfn "Hello World from F#!"
    0 // return an integer exit code

There’s a little more boilerplate here, and it is mostly unnecessary. We will keep it around because it makes working with arguments a little easier.

F# is a “whitespace sensitive” language, like Python. That means that the scope of a function is delimited by indentation and not curly braces. Thus the last line in the above main function is the expression 0, which since it is the last line of the function, is the function’s return value. If you recall from our discussion of C, returning 0 is how we tell the operating system that everything went A-OK.

One last thing. Since every language construct in F# is an expression, printfn is an expression. However, it falls into an special class of side-effecting expressions. Input and output are inherently side-effecting, and so a functional language that does not allow at least some side effects is seriously restricted in terms of its expressiveness. Pure functional languages like Haskell have a clever but somewhat byzantine system for dealing with side effects, which is why the first “hello world” program in my “easy-to-read” Haskell book appears on page 154. Right after the section on “functors,” of course ¯\(ツ)


Compiling and running your project

Compile your project with:

$ dotnet build

You may also just run the project, and if it needs to be built, dotnet will build it for you before running it.

$ dotnet run

I personally prefer to run the build command separately because the run command hides compiler output. I like to see compiler output since it will tell me if it finds problems with my program. Unlike other languages you’ve used, F# has very good compiler output, although as you will see, it still has some warts here and there.

Code editors

You are welcome to use whatever code editor you wish on this assignment. Two in particular stand out for F#, however: Visual Studio Code and emacs. Both are installed on our lab machines. Note, however, that we will strictly manage our projects using the dotnet command line tool.

Visual Studio Code

Visual Studio Code works out of the box with F#, but a plugin called Ionide adds additional features like syntax highlighting and tooltips to your editor. To install Ionide, follow this tutorial on installing extensions.

Note: Ionide comes with a variety of build tools such as FAKE, Forge, Paket, and project scaffolds. Please do not use these tools for this class as they do not interoperate well with our class environment. Instead, please use the dotnet command line tool to compile and run your tool as discussed earler.

emacs

If you prefer emacs, you can add the fsharp-mode which adds syntax highlighting, tooltips, and a variety of other nice features. This is my preferred environment on lab machines.

Paste the following into ~/.local_emacs

;;; Initialize MELPA
(require 'package)
(add-to-list 'package-archives '("melpa" . "http://melpa.milkbox.net/packages/"))
(unless package-archive-contents (package-refresh-contents))
(package-initialize)

;;; Install fsharp-mode
(unless (package-installed-p 'fsharp-mode)
  (package-install 'fsharp-mode))

;;; Run fsharp-mode
(require 'fsharp-mode)

This will install both MELPA, which is an online package repository for emacs, and the fsharp-mode package. Note that MELPA has many other modes you can install if you like what you see. One downside to MELPA is that it adds a few seconds of startup time to emacs, but in my opinion, the delay is well worth the wait.

The next time you start emacs with F# code, you will see the new mode in action.

  • CSCI 334: Principles of Programming Languages, Fall 2018