F# for Fun and Profit
  • Introduction
  • Getting started
    • Contents of the book
    • "Why use F#?" in one page
    • Installing and using F#
    • F# syntax in 60 seconds
    • Learning F#
    • Troubleshooting F#
    • Low-risk ways to use F# at work
      • Twenty six low-risk ways to use F# at work
      • Using F# for development and devops scripts
      • Using F# for testing
      • Using F# for database related tasks
      • Other interesting ways of using F# at work
  • Why use F#?
    • The "Why use F#?" Series
      • Introduction to the 'Why use F#' series
      • F# syntax in 60 seconds
      • Comparing F# with C#: A simple sum
      • Comparing F# with C#: Sorting
      • Comparing F# with C#: Downloading a web page
      • Four Key Concepts
      • Conciseness
      • Type inference
      • Low overhead type definitions
      • Using functions to extract boilerplate code
      • Using functions as building blocks
      • Pattern matching for conciseness
      • Convenience
      • Out-of-the-box behavior for types
      • Functions as interfaces
      • Partial Application
      • Active patterns
      • Correctness
      • Immutability
      • Exhaustive pattern matching
      • Using the type system to ensure correct code
      • Worked example: Designing for correctness
      • Concurrency
      • Asynchronous programming
      • Messages and Agents
      • Functional Reactive Programming
      • Completeness
      • Seamless interoperation with .NET libraries
      • Anything C# can do...
      • Why use F#: Conclusion
  • Thinking Functionally
    • The "Thinking Functionally" Series
      • Thinking Functionally: Introduction
      • Mathematical functions
      • Function Values and Simple Values
      • How types work with functions
      • Currying
      • Partial application
      • Function associativity and composition
      • Defining functions
      • Function signatures
      • Organizing functions
      • Attaching functions to types
      • Worked example: A stack based calculator
  • Understanding F# ###
    • The "Expressions and syntax" Series
      • Expressions and syntax: Introduction
      • Expressions vs. statements
      • Overview of F# expressions
      • Binding with let, use, and do
      • F# syntax: indentation and verbosity
      • Parameter and value naming conventions
      • Control flow expressions
      • Exceptions
      • Match expressions
      • Formatted text using printf
      • Worked example: Parsing command line arguments
      • Worked example: Roman numerals
    • The "Understanding F# types" Series
      • Understanding F# types: Introduction
      • Overview of types in F#
      • Type abbreviations
      • Tuples
      • Records
      • Discriminated Unions
      • The Option type
      • Enum types
      • Built-in .NET types
      • Units of measure
      • Understanding type inference
    • Choosing between collection functions
    • The "Object-oriented programming in F#" Series
      • Object-oriented programming in F#: Introduction
      • Classes
      • Inheritance and abstract classes
      • Interfaces
      • Object expressions
    • The "Computation Expressions" Series
      • Computation expressions: Introduction
      • Understanding continuations
      • Introducing 'bind'
      • Computation expressions and wrapper types
      • More on wrapper types
      • Implementing a builder: Zero and Yield
      • Implementing a builder: Combine
      • Implementing a builder: Delay and Run
      • Implementing a builder: Overloading
      • Implementing a builder: Adding laziness
      • Implementing a builder: The rest of the standard methods
    • Organizing modules in a project
    • The "Dependency cycles" Series
      • Cyclic dependencies are evil
      • Refactoring to remove cyclic dependencies
      • Cycles and modularity in the wild
    • The "Porting from C#" Series
      • Porting from C# to F#: Introduction
      • Getting started with direct porting
  • Functional Design ###
    • The "Designing with types" Series
      • Designing with types: Introduction
      • Single case union types
      • Making illegal states unrepresentable
      • Discovering new concepts
      • Making state explicit
      • Constrained strings
      • Non-string types
      • Designing with types: Conclusion
    • Algebraic type sizes and domain modelling
    • Thirteen ways of looking at a turtle
      • Thirteen ways of looking at a turtle (part 2)
      • Thirteen ways of looking at a turtle - addendum
  • Functional Patterns ###
    • How to design and code a complete program
    • A functional approach to error handling (Railway oriented programming)
      • Railway oriented programming: Carbonated edition
    • The "Understanding monoids" Series
      • Monoids without tears
      • Monoids in practice
      • Working with non-monoids
    • The "Understanding Parser Combinators" Series
      • Understanding Parser Combinators
      • Building a useful set of parser combinators
      • Improving the parser library
      • Writing a JSON parser from scratch
    • The "Handling State" Series
      • Dr Frankenfunctor and the Monadster
      • Completing the body of the Monadster
      • Refactoring the Monadster
    • The "Map and Bind and Apply, Oh my!" Series
      • Understanding map and apply
      • Understanding bind
      • Using the core functions in practice
      • Understanding traverse and sequence
      • Using map, apply, bind and sequence in practice
      • Reinventing the Reader monad
      • Map and Bind and Apply, a summary
    • The "Recursive types and folds" Series
      • Introduction to recursive types
      • Catamorphism examples
      • Introducing Folds
      • Understanding Folds
      • Generic recursive types
      • Trees in the real world
    • The "A functional approach to authorization" Series
      • A functional approach to authorization
      • Constraining capabilities based on identity and role
      • Using types as access tokens
  • Testing
    • An introduction to property-based testing
    • Choosing properties for property-based testing
  • Examples and Walkthroughs
    • Worked example: Designing for correctness
    • Worked example: A stack based calculator
    • Worked example: Parsing command line arguments
    • Worked example: Roman numerals
    • Commentary on 'Roman Numerals Kata with Commentary'
    • Calculator Walkthrough: Part 1
      • Calculator Walkthrough: Part 2
      • Calculator Walkthrough: Part 3
      • Calculator Walkthrough: Part 4
    • Enterprise Tic-Tac-Toe
      • Enterprise Tic-Tac-Toe, part 2
    • Writing a JSON parser from scratch
  • Other
    • Ten reasons not to use a statically typed functional programming language
    • Why I won't be writing a monad tutorial
    • Is your programming language unreasonable?
    • We don't need no stinking UML diagrams
    • Introvert and extrovert programming languages
    • Swapping type-safety for high performance using compiler directives
Powered by GitBook
On this page
  • Is everything really an expression?
  • What kinds of expressions are there?
  • "Control flow" expressions
  • "let" bindings as expressions
  • General tips for using expressions
  • Multiple expressions on one line
  • Understanding expression evaluation order

Was this helpful?

  1. Understanding F# ###
  2. The "Expressions and syntax" Series

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:

1                            // literal
[1;2;3]                      // list expression
-2                           // prefix operator    
2 + 2                        // infix operator    
"string".Length              // dot lookup
printf "hello"               // function application

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.

fun () -> 1                  // lambda expression

match 1 with                 // match expression
    | 1 -> "a"
    | _ -> "b"

if true then "a" else "b"    // if-then-else

for i in [1..10]             // for loop
  do printf "%i" i

try                          // exception handling
  let result = 1 / 0
  printfn "%i" result
with
  | e -> 
     printfn "%s" e.Message


let n=1 in n+2               // let expression

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:

let x1 = fun () -> 1                  

let x2 = match 1 with                 
         | 1 -> "a"
         | _ -> "b"

let x3 = if true then "a" else "b"    

let x4 = for i in [1..10]             
          do printf "%i" i

let x5 = try                          
            let result = 1 / 0
            printfn "%i" result
         with
            | e -> 
                printfn "%s" e.Message


let x6 = let n=1 in n+2

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:

let x5 = let n=1 in n+2

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#.

let f x =                           // one expression per line
      printfn "x=%i" x
      x + 1

let f x = printfn "x=%i" x; x + 1   // all on same line with ";"

The rule about requiring unit values until the last expression still applies, of course:

let x = 1;2              // error: "1;" should be a unit expression
let x = ignore 1;2       // ok
let x = printf "hello";2 // ok

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.

// create a clone of if-then-else
let test b t f = if b then t else f

// call it with two different choices
test true (printfn "true") (printfn "false")

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:

// create a clone of if-then-else that accepts functions rather than simple values
let test b t f = if b then t() else f()

// call it with two different functions
test true (fun () -> printfn "true") (fun () -> printfn "false")

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.

// create a clone of if-then-else with no restrictions...
let test b t f = if b then t else f

// ...but call it with lazy values
let f = test true (lazy (printfn "true")) (lazy (printfn "false"))

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.

f.Force()     // use Force() to force the evaluation of a lazy value

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.

PreviousExpressions vs. statementsNextBinding with let, use, and do

Last updated 5 years ago

Was this helpful?

We have already discussed lambdas in the series, and as noted earlier, computation expressions and object-oriented expressions will be left to later series.

How can "let" be an expression? The reason will be discussed in the next post on .

"thinking functionally"
The match expression
Imperative control flow: if-then-else and for loops
Exceptions
"let", "use" and "do"