Overview of F# expressions
Control flows, lets, dos, and more
In this post we'll look at the different kinds of expressions that are available in F# and some general tips for using them.
Is everything really an expression?
You might be wondering how "everything is an expression" actually works in practice.
Let's start with some basic expression examples that should be familiar:
No problems there. Those are obviously expressions.
But here are some more complex things which are also expressions. That is, each of these returns a value that can be used for something else.
In other languages, these might be statements, but in F# they really do return values, as you can see by binding a value to the result:
What kinds of expressions are there?
There are lots of diffent kinds of expressions in F#, about 50 currently. Most of them are trivial and obvious, such as literals, operators, function application, "dotting into", and so on.
The more interesting and high-level ones can be grouped as follows:
Lambda expressions
"Control flow" expressions, including:
The match expression (with the
match..with
syntax)Expressions related to imperative control flow, such as if-then-else, loops
Exception-related expressions
"let" and "use" expressions
Computation expressions such as
async {..}
Expressions related to object-oriented code, including casts, interfaces, etc
So, in upcoming posts in this series, we will focus on "control flow" expressions and "let" expressions.
"Control flow" expressions
In imperative languages, control flow expressions like if-then-else, for-in-do, and match-with are normally implemented as statements with side-effects, In F#, they are all implemented as just another type of expression.
In fact, it is not even helpful to think of "control flow" in a functional language; the concept doesn't really exist. Better to just think of the program as a giant expression containing sub-expressions, some of which are evaluated and some of which are not. If you can get your head around this way of thinking, you have a good start on thinking functionally.
There will be some upcoming posts on these different types of control flow expressions:
"let" bindings as expressions
What about let x=something
? In the examples above we saw:
General tips for using expressions
But before we cover the important expression types in details, here are some tips for using expressions in general.
Multiple expressions on one line
Normally, each expression is put on a new line. But you can use a semicolon to separate expressions on one line if you need to. Along with its use as a separator for list and record elements, this is one of the few times where a semicolon is used in F#.
The rule about requiring unit values until the last expression still applies, of course:
Understanding expression evaluation order
In F#, expressions are evaluated from the "inside out" -- that is, as soon as a complete subexpression is "seen", it is evaluated.
Have a look at the following code and try to guess what will happen, then evaluate the code and see.
What happens is that both "true" and "false" are printed, even though the test function will never actually evaluate the "else" branch. Why? Because the (printfn "false")
expression is evaluated immediately, regardless of how the test function will be using it.
This style of evaluation is called "eager". It has the advantage that it is easy to understand, but it does mean that it can be inefficient on occasion.
The alternative style of evaluation is called "lazy", whereby expressions are only evaluated when they are needed. The Haskell language follows this approach, so a similar example in Haskell would only print "true".
In F#, there are a number of techniques to force expressions not to be evaluated immediately. The simplest it to wrap it in a function that only gets evaluated on demand:
The problem with this is that now the "true" function might be evaluated twice by mistake, when we only wanted to evaluate it once!
So, the preferred way for expressions not to be evaluated immediately is to use the Lazy<>
wrapper.
The final result value f
is also a lazy value, and can be passed around without being evaluated until you are finally ready to get the result.
If you never need the result, and never call Force()
, then the wrapped value will never be evaluated.
There will much more on laziness in an upcoming series on performance.
Last updated
Was this helpful?