03 November 2017

An introduction followed by 15 lectures on the functional aspects of F#

(by Martin Elsman)

F# is a programming language aimed at executing programs on the .NET platform. We will explore how to develop F# programs that run under Mono, an open-source implementation of Microsoft’s Common Intermediate Runtime (CLR). We will see that it is perfectly possible, with certain restrictions, to develop portable programs and code that runs on a variety of platforms, including MacOS, Microsoft Windows, Linux, and, last but not least, a large variety of mobile platforms out there.

We will, essentially, be using a number of open-source tools. The most essential tool is the terminal program, which allows the developer to navigate around the file system and to execute commands, which includes compiling and executing F# programs.

Next, we will assume that the user has installed an editor on the machine. Whereas, many different editors exist, we will recommend gedit, which is available on all platforms (on MacOS, it may be installed using Homebrew; just type brew install gedit).

Finally, the user needs to install Mono, which, under MacOS, also can be installed using Homebrew; just type brew install mono in a terminal.

After having installed these tools, we are ready to run the fsharpi program in the terminal. In your terminal window, simply write

$ fsharpi

The fsharpi program will launch with a greeting and will be ready to receive commands:

F# Interactive for F# 4.1
Freely distributed under the Apache 2.0 Open Source License

For help type #help;;

>

Expressions and Types

The fsharpi program accepts commands as input, but it is also possible to type in an F# expression. Here is an example:

> 4+2*8;;
val it : int = 20

Notice that expressions and commands need to be terminated with ;; to indicate that fsharpi can start processing the input. After fsharpi receives the input, a number of things happen. First, the input is analysed and it is determined that the input corresponds to an expression that has type int. Then the expression is compiled into CIL bytecode, and finally, the bytecode is executed and the result, together with its type int, is printed in the feedback message val it : int = 20. Notice here the distinction between analysing and compiling the expression and the process of executing the generated bytecode, resulting in a final value; the expression is evaluated only if the expression can be given a type (in this case int). Finally, notice also that the resulting value is bound to a so-called variable it, which the user can refer to later if the result is needed for another computation:

> (it+3)*2;;
val it : int = 46

From the above interaction, we see that F# remembers the value of the variable it when calculating the value of the new expression, but we also see that the variable it is now replaced with a new binding, which associates the variable it with the value 46. We shall see later that a programmer can easily bind values to new variables and that the programmer may choose names for variables almost arbitrarily. Notice also that the programmer may use parentheses to overrule the usual precedence rules of mathematics. To exit the F# interpreter, type the command #q;;.

F# has support for a large number of built-in types, including integers (int), double-precision floats (float), strings (string), and booleans (bool). As we shall see later, it is also possible for the developer to declare his or her own types. F# also has a number of built-in operations on values of the various types; we have seen how the operation + operates on two integers, resulting in a new integer. The fact that + behaves like this can be specified by saying that + has the (function) type int -> int -> int. In short, we shall often use the following syntax to specify this property:

val (+) : int -> int -> int

Similarly, other operations on integers are available as well:

val (-) : int -> int -> int
val (/) : int -> int -> int
val abs : int -> int
val max : int -> int -> int
val min : int -> int -> int

Exercise 1: Play around with various expressions in the fsharpi program and evaluate the result of computing the number of minutes it takes for a train to run from Copenhagen to Aarhus given that it runs on average 120km/hour and that the distance between Copenhagen and Aarhus is 290km.

Of course, it is possible to program with values other than integers, as indicated by the fact that F# supports other types than int. For instance, F# supports programming with 64-bit double-precision floating-point values, which have type float:

> sin(4.9+float(2*8));;
val it : float = 0.8871575287

The value resulting from evaluating the expression is determined to have type float and, again, the result of evaluating the expression is bound to the variable it. We also see that it is possible to use the float(...) function to convert an integer value into a value of type float. However, we can also see that F# is somewhat rigid in this respect; it does not allow you to mix integers and floats in operations without you using the float function:

> 4.9+8;;
  4.9+8;;
  ----^

...(2,5): error FS0001: The type 'int' does not match the type 'float'

Exercise 2: The formula for calculating the body-mass-index for a person is identical to the weight of the person (in kilos) divided by the height (in meters) squared. Calculate the body mass index of a person of height 205cm and weight 89 kilos.

Variables

As mentioned, it is possible in F# for a programmer to bind a value to an identifier specified by the programmer. Such a binding is introduced with a so-called let construct:

> let myhousenumber = 24;;
val myhousenumber : int = 24
> let nexthousenumber = myhousenumber + 1;;
val nexthousenumber : int = 25
> let pi = 3.14;;
val pi : float = 3.14
> let height = sin pi;;
val height : float = 0.001592652916

As we can see, it is straightforward to create new bindings and to use the values associated to the introduced variables. In the above code, notice the use of the sine-function:

val sin : float -> float

Other functions (besides most of those available on integers) are available on floats as well. Such functions include cos, tan, exp, and so on.

Strings

Another essential data type available in F# is the string type:

> let hello = "Hello";;
val hello : string = "Hello"

> let mystring = hello + " World";;
val mystring : string = "Hello World"

Here we see that string constants are sequences of characters and that string constants are enclosed in double quotes (“). Moreover, the operator + is overloaded to also work for strings:

val (+) : string -> string -> string

The effect of “adding” to strings, which is also called catenation, is to create a new string that contains the argument strings put together in sequence. A number of other operations are available on strings, but to access most of them, it is required to access the, so-called, module containing string operations. Modules are referred to by name and, essentially (and for now), a module contains a number of variable bindings. The module containing string-operations is the module String and to access functionality within a module, we use the so-called dot-notation, which allows us, for instance, to get information about the length of a string:

> let mylen = String.length mystring;;
val mylen : int = 11

We see that the result of applying the String.length function to a concrete string is an integer, which we can also see by inspecting the function:

> String.length;;
val it : (string -> int) = <fun:it@24>

Whereas we can see the type of the function (it has type string -> int), we get no information about the function itself; that is, we cannot see its implementation.

Booleans and Conditional Expressions

The type bool represents the set of boolean values containing precisely the values true and false. Boolean values are, for instance, resulting from comparing values for equality and inequality:

> 2 < 3;;
val it : bool = true

> "hi" = "hello";;
val it : bool = false

Operations on booleans include boolean conjunction (&&) and boolean disjunction (||), which implements boolean “and” and boolean “or”, respectively. It is also possible to negate boolean values, using the function not:

val (&&) : bool -> bool -> bool
val (||) : bool -> bool -> bool
val not  : bool -> bool

Here is an example of a boolean expression:

> 2 < 3 && 5 >= 5;;
val it : bool = true

Notice that because the comparison operators (here < and >=) bind strong than &&, parentheses are not needed in this example.

Booleans may also be used for controlling program evaluation paths through the use of conditional expressions, also called if-then-else expressions:

let age = 17;;
val age : int = 17

> if age >= 18 then "you may drive a car" else "driving is not ok";;
val it : string = "driving is not ok"

Functions

We have already seen the use of some built-in functions, including functions for arithmetic (sine, addition, multiplication), for comparison testing, and for performing string operations.

In F#, it is straightforward to define your own functions built on top of previously defined functions or built in functions. In fact, the “F” in F# is really stands for the term functional.

Defining a function can be done by simply extending a let-expression to be parametric in an argument:

> let legal age =
    if age >= 18 then "you may drive a car"
    else "driving is not ok";;
val legal : age:int -> string

In this example, we define a function legal, which takes an integer as argument and produces a string as a result. This example is also the first example demonstrating that code may span several lines of text; once the first line is entered, fsharpi awaits further input and only when the ;; end-sequence appear will fsharpi attempt to make sense of the input. Once the function is defined, we can apply the function to different arguments and observe that the function behaves differently on different input:

> legal 14;;
val it : string = "driving is not ok"

> legal 28;;
val it : string = "you may drive a car"

Functions may take multiple parameters as input as exemplified in the following code that declares a function for determining the volume of a cylinder, given that the variable pi is bound already:

> let volume r h = h*pi*r*r;;
val volume : r:float -> h:float -> float

It is straightforward to call the function volume as can be seen in the following code:

> let vol1 = volume 1.0 10.0;;
val vol1 : float = 31.4

Exercise 3: Write a function bmi of type float -> float -> float that takes as input the weight and the height of a person and returns the body mass index for the person. You will probably need to apply a so-called type constraint to one of the function parameters in order not to have F# infer that the type of the function is int -> int -> int. The way to do this is to put parentheses around one of the arguments and specify that it should be of type float (as in (w:float)). When applying the bmi function to a weight of 83kg and a height of 2.05m, you should obtain a body mass index of approximately 19.75, which is considered slightly below normal.

Exercise 4: Using your newly defined function, create a new function bmi_msg that, as the bmi function takes a weight and a height as arguments, but now tests (using two conditional expressions) whether the person has a body mass index below or above normal. The result of the function should be a string telling the user if the body mass index is above normal, below normal, or within normal (a body mass index in the range 20-25 is considered normal). Test your function on various inputs.

Pairs and Tuples

It is often advantageous to think of a type as a set of values. In this way we can think of the type int as the set of all integers and the type bool as the set {true,false} containing the two possible boolean values. Understanding types as sets opens up the possibility for extending the universe of types. In this section, we shall consider product types. As an example, the product type of int and float, written int*float, is the type representing the set of pairs where the first component is an integer and the second component is a floating point value. In F#, pair values are simply constructed using a ,:

> let p = 34, 2.7;;
val p : int * float = (34, 2.7)

To access the components of a pair, we can use the functions fst and snd:

val fst : 'a * 'b -> 'a
val snd : 'a * 'b -> 'b

Here we see the first instance of so-called generic types, which are used in the types of the functions fst and snd to indicate that the functions can be applied to pairs of any type.

An important feature here is that the product type is just as good as any other type meaning that functions can be declared to take pairs as arguments and return pairs as results just as well as they can accept integers and return strings. As an example, here is a function that, given a radius value, returns a pair containing the circumference and the area of a circle:

> let circle_props r = (2.0*pi*r, pi*r*r);;
val circle_props : r:float -> float * float

Now that the function is declared, we can apply it to a particular radius and extract the area value:

> let area = snd(circle_props 5.0);;
val area : float = 78.5

Recursive Functions

Let us consider how we could write a function that adds up the numbers between 1 and 100. Clearly, we do not want to write up an expression 1+2+3+...+100. Even if this approach would work for summing up the values between 1 and 100, what if someone asked us to add up the numbers between 1 and 1000? Instead, we somehow need a way to repeat computations.

There are different kinds of programming languages that emphasize different programming paradigms. One paradigm is the imperative programming paradigm, which emphasizes the use of destructive updates, which, together with looping constructs, opens a possibility for arbitrary computing, which, for instance could be to sum up a range of numbers. Here we will follow another paradigm, which is the functional programming paradigm, a paradigm that celebrates immutability whenever possible and which features a number of reasoning principles that allow programmers to better specify and reason about properties of the program. An essential part of functional programming has to do with recursive functions, which are simply functions that call themselves.

Now consider again the task of writing a function that sums the integers between 0 and 100. Here is a recursive F# function that does the job for integers between 0 and a positive given argument n:

> let rec sum n = if n <= 0 then 0
                  else n + sum (n-1);;
val sum : n:int -> int

Notice the special keyword rec appearing after the let keyword. The rec keyword specifies that it is ok for the function to call itself inside the function’s own body. Now, let us look at the function in more detail. If the function is called with an argument of 0, clearly, the function will return 0 as a result, which we can easily check:

> sum 0;;
val it : int = 0

However, if the function is called with the argument 1, the immediate result is that the else-branch is taken. What this branch will do is to call sum recursively on n-1 and because n is 1, we have that sum is called with an argument of 0. After this call returns, 1 is added to the result of the call and the resulting value, which is 1, is returned.

Exercise 5: Write a recursive function called fac that takes an integer n as argument and computes the value 1*2*...*n. Test your function on a number of input values.

Exercise 6: Write a recursive function called power that takes two arguments, a float a and an integer n, and computes the value a^n. That is, it should compute the value 1.0*a*a*...*a, with n multiplications. Test your function on a number of input values. Notice that calling the function with the arguments 2.0 and 0 should result in the value 1.0 and calling the function with the arguments 3.0 and 4 should result in the value produced by the expression 1.0*3.0*3.0*3.0*3.0.

Working with Programs

Until now, we have only played around in the F# interpreter fsharpi. We shall now see how we can construct a self-contained program that, when executed, will run our F# code. To do so we will need to have our F# source program stored in a file somewhere on the machine. Using the terminal program, first create a directory mycode in your home directory on your machine:

$ cd ~
$ mkdir mycode
$ cd mycode

If you have not already installed gedit (or are using another text editor), now is the time to do so. The gedit program can be installed using Howebrew; just execute the command brew install gedit.

Once installed, open gedit from the terminal program:

$ gedit &

You can now type in your program in the gedit editor and save the program using the “Save As…” functionality in the menu. As an example, type in the following code in the editor, select F# as a language under “View->Highlight Mode…”, and save the file under the name sum.fs using the “Save As…” functionality:

let rec sum n = if n <= 0 then 0
                else n + sum (n-1);;

do printfn "%A" (sum 0)
do printfn "%A" (sum 10)

You are now ready to compile your first program using the F# compiler fsharpc. Again, from your terminal prompt, execute the following command:

$ fsharpc sum.fs

The command will generate a file called sum.exe, which can be executed using the mono program:

$ mono sum.exe
0
55

Notice the effects of the last two lines of the program. These lines print the results of evaluating the expressions sum 0 and sum 10 to the output. In each of the lines, the function printfn is evaluated to have an effect, which is to print a value to the program’s standard output. The function also prints the special new line character \n, which has the effect that the terminal program will show the two numbers on separate lines. The first argument to the printfn function is a so-called format string, which, in this case, specifies that the value should be printed using F#’s internal value formatter.

Reading Input Lines

We have seen how we can write a program that can write to the standard output, but we have not seen how we can get a program to read input from a user (or another program). Luckily, F# features a function for doing just this. The function is called System.Console.ReadLine and its type is unit -> string, where unit is a built-in type containing only one value, namely ().

We can now write a program, say sumN.fs, that asks the user to type an integer n and prints the value sum n:

let rec sum n = if n <= 0 then 0
                else n + sum (n-1);;

do printfn "Input a number, please:"
let n : string = System.Console.ReadLine()

do printfn "Sum(0..%s) = %A" n (sum (int(n)))

Notice the use of the : string type annotation in the let-binding for n; this annotation allows us to express our intention that n should contain a string. If we, for instance, had forgotten to give System.Console.ReadLine the unit` value as argument, the F# compiler would complain.

Here is the result of compiling the program and executing it with the number 11 as input:

$ fsharpc --nologo sumN.fs
$ mono sumN.exe
Input a number, please:
11
Sum(0..11) = 66

Exercise 7: Use the body mass index calculation function bmi_msg from Exercise 4 and combine it with code that reads the user’s weight and height from the terminal input and reports a message on the terminal output as a result.

Imperative Programming with Mutable Variables

We shall now see how we can use so-called mutable variables for storing values that change during the evaluation of a program. Whereas recursion is often better for encoding repetition, we shall also introduce the concepts of while-loops and for-loops.

Here is a program, called multableN.fs that outputs a multiplication table on the terminal output:

do printfn "Enter an integer:"
let x = System.Console.ReadLine()
do printfn "Table:"
let mutable i = 1
while i <= 10 do
  printfn "%A" (i*(int(x)))
  i <- i + 1

When the program executes, the body of the while-loop, which consists of the two indented lines of code, is executed 10 times and within the body, the variable i takes on different values for each iteration, caused by the statement i <- i + 1. Here is an example interaction with the program:

$ fsharpc --nologo multableN.fs
$ mono multableN.exe
bash-3.2$ mono multableN.exe
Enter an integer:
3
Table:
3
6
9
12
15
18
21
24
27
30

Exercise 8: Write a program christmas that reads an integer n and prints out a Christmas tree of height n. The program may use the function printf to print a string without printing a newline character. Here is what the program should output when the number 3 is given as input:

$ mono christmas.exe
Enter an integer:
3
   *
  ***
 *****

More F# Topics

Programming in F#, and functional programming in general, has much more to it than what we have seen here. In the referenced material below, we cover a number of topics, including programming with lists and arrays, recursion in depth, type-full programming with records and discriminated unions, and, finally, the topic of higher-order functional programming (to come). All of the examples given in the slides can be compiled and executed under Mono (you need to copy the code from the slides, though). Some of the examples utilize the ImgUtil library, which is comprised by an img_util.fsi file (an interface file) and the implementation file img_util.fs. For details about how to compile and use the library, please consult the README file.

Please be aware that the slides are all in danish; they have been developed as part of the introductory programming course for the Computer Science BSc degree at the Department of Computer Science, University of Copenhagen.

  1. Programming with Lists. We introduce the reader to list programming and demonstrate how a programmer may use the built-in list functions to construct larger functions.

  2. Programming with Lists (continued) and Arrays. We continue with the introduction of list programming and demonstrate also how programmers may use arrays of different dimensions to solve various tasks.

  3. Recursion. We illustrate the concept of recursion by introducing a number of basic functions on integers and lists.

  4. Recursion and Sorting. We illustrate recursive programming through the definition of a number of sorting routines over lists of integers. The sorting routines include bubble sort, insertion sort, selection sort, merge sort and quick sort.

  5. Recursion, Games, and Drawing. We demonstrate how we can implement functionality (rules) that allow for a human to play the “Towers of Hanoi” game in the F# shell. We then present the Hanoi algorithm, which allows the computer to play (and win) the game for any number of pegs, by applying a recursive algorithm. We also show how we can use recursion to draw simple fractals on a canvas using the ImgUtil library.

  6. Types and Pattern Matching. We present the concept of types in more details and show how F# allows the programmer to declare, even generic, type abbreviations and how a programmer may use the concept of pattern-matching to, for instance, define functions over lists.

  7. Discriminated Unions and a Turtle EDSL. We present the concept of discriminated unions and show how a programmer may implement a small Turtle EDSL (Embedded Domain Specific Language), which allows for specifying simple turtle movements and drawings. We show how the recursive features of a host language (in this case F#) can be used to generate instructions for drawing complex fractals. We also demonstrate how the drawings can be effectuated by writing an interpreter for the turtle EDSL that converts instructions into bitmap lines, which can be shown on a canvas.

  8. Stacks, Queues, and Recursion Revisited. We give a solution to the classical maximum segment sum problem, using recursion. We also define interfaces for stacks and queues and provide efficient implementations of both.

  9. Search Trees and Catenable Strings. We look at different tree structures and provide definitions of trees that are good for binary searching and definition of trees that are good for implementing catenable strings, that is, strings that can be catenated in O(1) time.

  10. Tree Traversals. We classify a number of different ways to traverse (binary) trees, including depth-first tree traversals (preorder, postorder, and inorder) and breath-first traversal, and provide F# implementations of them all.

  11. Expression Trees and Symbolic Differentiation. We show how we can define the concept of expression trees (of one variable) in F# using a simple, recursively defined, discriminated union type. We demonstrate how we can generate LaTeX code for such expressions and how we can completely define, using high-school math rules, how such expressions are differentiated. An expression simplifier makes for prettier printing of results.

  12. Higher-Order Functions. We revisit the definition of functions in F# and give a proper foundation in terms of the notion of closures. We give examples of how higher-order functions allow for functions to receive functions as parameters and how functions can also be returned as the result of calling a function. We define the notions of currying and partial applications, and we give an extensive example of how functions can be used to define so-called functional images.

  13. Exceptions. We introduce the concept of exceptions and present the difference between exception values and the concept of raising an exception. We describe how exceptions can be handled at different levels in a program and give an alternative model for programming with errors based on Yoda’s statement that “there is no try; either you do or you don’t”.

  14. Input and Output. In this lecture, we describe how F# programs may interact with the console (e.g., the terminal) through command-line arguments and by reading from stdin and by writing to stdout and stderr. We also demonstrate how programs may read and write to files and how we can perform HTTP requests to servers on the internet asking for web-page content. As an example, we demonstrate how we can apply the concept of regular expressions to extract the FX rate from a web-site and use it in an F# application.

  15. Parsing with Higher-Order Functions. We present the concepts of lexing and parsing and demonstrate how we can make use of the concept of higher-order functions to define the notion of parser combinators. We demonstrate how the parser combinators can be used to define parsers for expression trees and turtle-graphics commands that each will convert strings of characters to manageable F# data structures.

License

Creative Commons License
Getting Started with FSharp by Martin Elsman is licensed under a Creative Commons Attribution-NoDerivatives 4.0 International License.



blog comments powered by Disqus