I'm going to start a series of posts as I learn F#, working through the excellent book
Expert F#, which was
published last month (and I wholeheartedly recommend it over
Fundamentals of F#, by the way). The premise of these posts is that the reader and I have advanced C# skills, but we're new to F# and functional programming. The caveat is that since I'm new to F#, I'll make mistakes, but I'll come back and fix them as soon as I know better.
What is F#? From the
download page for version 1.9.3.7:
F# is a variant of the ML programming language for .NET and has a core language similar to OCaml. F# is a mixed functional/imperative/object-oriented programming language excellent for medium/advanced programmers and for teaching. It also can be used to access hundreds of .NET libraries, and the F# code can be accessed from C# and other .NET languages.
Functional ExpressionsF# gets its name and notoriety from the fact that it is, among other things, a functional language, so let's start off with functional code. Within an
fsi.exe interpreter session, I'm typing the blue:
> let add x y = x + y;;
val add : int -> int -> int
Everything in blue up to the semicolons is an expression which is evaluated. The semicolons just tell the interpreter to go ahead and evaluate everything typed up to that point, I won't be continuing the expression on the next line. Here the
let expression evaluates to a function,
add, which maps two integers to their sum.
Next, the line of feedback from the interpreter displays the results of the evaluation -- hey, we've created a value, named
add, that is a function of a certain type. The type of the function looks odd at first, but let's keep moving and try it out:
> add 3 4;;
val it : int = 7
The expression
add 3 4 evaluates to an integer value of
7. The name
it is just a placeholder for the unnamed value (
7) that resulted from the evaluation.
Have you noticed that the type
int was assumed by the interpreter for
x and
y? That's what F# does, it
infers the types of an expression based on what the expression does and how it does it. While it may seem arbitrary that
x and
y are inferred as integers here rather than as floats or whatnot, the rules of type inference are well-defined (this one is directly inherited from
OCaml, F#'s daddy). Type inference has some serious selling points:
values are strongly typed without having to be explicitly typed. In some cases, when a type cannot be unambiguously inferred, F# requires an explicit type specifier -- this is the price for having the best of both worlds.
Now why is the type of add
int -> int -> int? This means that
add is a function which maps an integer to a function which maps an integer to an integer. That's a mouthful, but what does it mean? Don't try this in C#:
> add 1;;
val it : (int -> int) = (fun:it@11)
Look, no error! We can apply just
one parameter (
x) to
add, and that expression (everything in blue) evaluates to a function which maps an integer to an integer. Don Syme, F#'s creator, calls such a result a
residual function. Let's package this up in a named function,
inc, that we can use later:
> let inc = add 1;;
val inc : (int -> int)
> inc 4;;
val it : int = 5
So
inc is a function which maps one integer to another -- by evaluating, in effect,
add 1 y. Now we can better understand the type of
add:
int -> int -> intThis is the type of a function which maps one integer (the left
int) to a function of (on the right) type
int -> int, that is, to a function which, like
inc, maps one integer to another.
...With Side-EffectsF# is not a pure functional language, it is also a fully-featured imperative and OO language. I'm led to believe F# could even be used as a total replacement for C# without ever using it's functional aspects. Here's an expression:
> printfn "Hello!";;
Hello!
val it : unit = ()
First, note that this is an expression,
not a statement -- there are no statements in F#, and this is a clue as to why some things do not work as the C# brain expects. When this expression is evaluated, it results in a side-effect, the printing of
Hello! to the console. The type of the expression is, we are told,
unit, which is just F# for
void. Such imperative code can show up in a function too. Let's try this:
> let hello = printfn "Hello!";;
val hello : unit
Hello!
Oops! I hoped to be defining a parameterless function, but it turns out
hello is not a function, we can see this from its type, the hapless
unit. Interestingly, the side-effect of printing
Hello! to the console already occurred during the evaluation of the
let expression.
A commentor has kindly pointed out that to define a function that takes no parameters, we can indicate one "empty tuple" parameter:
> let hola () = printfn "Hola!";;
val hola : unit -> unit
> hola ();;
Hola!
This is what I was after, a function which maps
unit to
unit but has a side-effect. The empty parens
() is not an empty parameter list, that's just what it looks like to the C-derived-language eye. It is actually a special parameter, the empty tuple. I'll discuss tuples in part 2.
MixedLet's define a more interesting function with side-effects:
> let addandtell x y = printfn "The number is %d" (add x y);;
val addandtell : int -> int -> unit
> addandtell 8 9;;
The number is 17
val it : unit = ()
Function
addandtell maps from two integers to
unit,
and it has the side-effect of printing a message with their sum out to the console. This time the side-effect from
printfn happens exactly when sub-expression
add x y evaluates to an
int -- which happens when the
8 and
9 are supplied to
addandtell. If we were to next supply
addandtell with
10 and
11, then
add x y would re-evaluate, causing the
printfn sub-expression to re-evaluate and have a new side-effect: printing
The number is 21 to the console.
As we did earlier with
add, we can apply just one parameter to
addandtell, and then use the residual function.
> let incandtell = addandtell 1;;
val incandtell : int -> unit
The sub-expression
addandtell 1 is a residual function with a side-effect. The side-effect won't occur until the
add "inside" of
addandtell gets all of the parameters it is waiting for and can evaluate to the integer which
printfn takes. Let's make it happen:
> incandtell 13;;
The number is 14
val it : unit = ()
And there's the side-effect, the printout of
The number is 14.
Don't take the "waiting for" metaphor here literally. There's no blocked thread here. A residual function is produced by supplying some-but-not-all of the parameters to a function, so in a logical sense the original function is "waiting for" the rest of its parameters.
NextIn part 2 I'll look at some of F#'s special data types, and do some real work with recursive functions.
Labels: F#, software