From C# to F#, Part 1: Expressions with side-effects
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# 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 -> int
This 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-Effects
F# 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.
Mixed
Let'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.
Next
In part 2 I'll look at some of F#'s special data types, and do some real work with recursive functions.
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 Expressions
F# 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 -> int
This 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-Effects
F# 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.
Mixed
Let'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.
Next
In part 2 I'll look at some of F#'s special data types, and do some real work with recursive functions.
1 Comments:
At 3:02 PM , Anonymous said...
If you want to make 'hello' a function, you need to define it as taking an empty tuple as its parameter:
let hello() = printfn "Hello!";;
This will make hello's type
unit -> unit
And to call it you do
hello();; (* rather than just hello;; *)
Post a Comment
Subscribe to Post Comments [Atom]
<< Home