Binding with let, use, and do

How to use them

As we've have already seen, there are no "variables" in F#. Instead there are values.

And we have also seen that keywords such as let, use, and do act as bindings -- associating an identifier with a value or function expression.

In this post we'll look at these bindings in more detail.

"let" bindings ##

The let binding is straightforward, it has the general form:

let aName = someExpression

But there are two uses of let that are subtly different. One is to define a named expression at a the top level of a module*, and the other is to define a local name used in the context of some expression. This is somewhat analogous to the difference between "top level" method names and "local" variable names in C#.

* and in a later series, when we talk about OO features, classes can have top level let bindings too.

Here's an example of both types:

module MyModule = 

    let topLevelName = 
        let nestedName1 = someExpression
        let nestedName2 = someOtherExpression
        finalExpression

The top level name is a definition, which is part of the module, and you can access it with a fully qualified name such as MyModule.topLevelName. It's the equivalent of a class method, in a sense.

But the nested names are completely inaccessible to anyone -- they are only valid within the context of the top level name binding.

Patterns in "let" bindings

We have already seen examples of how bindings can use patterns directly

And in function definitions the binding includes parameters as well:

The details of the various pattern bindings depends on the type being bound, and will be discussed further in later posts on pattern matching.

Nested "let" bindings as expressions

We have emphasized that an expression is composed from smaller expressions. But what about a nested let?

How can "let" be an expression? What does it return?

The answer that a nested "let" can never be used in isolation -- it must always be part of a larger code block, so that it can be interpreted as:

That is, every time you see the symbol "nestedName" in the second expression (called the body expression), substitute it with the first expression.

So for example, the expression:

really means:

When the substitutions are performed, the last line becomes:

In a sense, the nested names are just "macros" or "placeholders" that disappear when the expression is compiled. And therefore you should be able to see that the nested lets have no effect on the expression as whole. So, for example, the type of an expression containing nested lets is just the type of the final body expression.

If you understand how nested let bindings work, then certain errors become understandable. For example, if there is nothing for a nested "let" to be "in", the entire expression is not complete. In the example below, there is nothing following the let line, which is an error:

And you cannot have multiple expression results, because you cannot have multiple body expressions. Anything evaluated before the final body expression must be a "do" expression (see below), and return unit.

In a case like this, you must pipe the results into "ignore".

"use" bindings ##

The use keyword serves the same purpose as let -- it binds the result of an expression to a named value.

The key difference is that is also automatically disposes the value when it goes out of scope.

Obviously, this means that use only applies in nested situations. You cannot have a top level use and the compiler will warn you if you try.

To see how a proper use binding works, first let's create a helper function that creates an IDisposable on the fly.

Now let's test it with a nested use binding:

We can see that "done" is printed, and then immediately after that, myResource goes out of scope, its Dispose is called, and "hello disposed" is also printed.

On the other hand, if we test it using the regular let binding, we don't get the same effect.

In this case, we see that "done" is printed, but Dispose is never called.

"Use" only works with IDisposables

Note that "use" bindings only work with types that implement IDisposable, and the compiler will complain otherwise:

Don't return "use'd" values

It is important to realize that the value is disposed as soon as it goes out of scope in the expression where it was declared. If you attempt to return the value for use by another function, the return value will be invalid.

The following example shows how not to do it:

If you need to work with a disposable "outside" the function that created it, probably the best way is to use a callback.

The function then would work as follows:

  • create the disposable.

  • evaluate the callback with the disposable

  • call Dispose on the disposable

Here's an example:

This approach guarantees that the same function that creates the disposable also disposes of it and there is no chance of a leak.

Another possible way is to not use a use binding on creation, but use a let binding instead, and make the caller responsible for disposing.

Here's an example:

Personally, I don't like this approach, because it is not symmetrical and separates the create from the dispose, which could lead to resource leaks.

The "using" function

The preferred approach to sharing a disposable, shown above, used a callback function.

There is a built-in using function that works in the same way. It takes two parameters:

  • the first is an expression that creates the resource

  • the second is a function that uses the resource, taking it as a parameter

Here's our earlier example rewritten with the using function:

In practice, the using function is not used that often, because it is so easy to make your own custom version of it, as we saw earlier.

Misusing "use"

One trick in F# is to appropriate the use keyword to do any kind of "stop" or "revert" functionality automatically.

The way to do this is:

  • Create an extension method for some type

  • In that method, start the behavior you want but then return an IDisposable that stops the behavior.

For example, here is an extension method that starts a timer and then returns an IDisposable that stops it.

So now in the calling code, we create the timer and bind it with use. When the timer value goes out of scope, it will stop automatically!

This same approach can be used for other common pairs of operations, such as:

  • opening/connecting and then closing/disconnecting a resource (which is what IDisposable is supposed to be used for anyway, but your target type might not have implemented it)

  • registering and then deregistering an event handler (instead of using WeakReference)

  • in a UI, showing a splash screen at the start of a block of code, and then automatically closing it at the end of the block

I wouldn't recommend this approach generally, because it does hide what is going on, but on occasion it can be quite useful.

"do" bindings ##

Sometimes we might want to execute code independently of a function or value definition. This can be useful in module initialization, class initialization and so on.

That is, rather than having "let x = do something" we just the "do something" on its own. This is analogous to a statement in an imperative language.

You can do this by prefixing the code with "do":

In many situations, the do keyword can be omitted:

But in both cases, the expression must return unit. If it does not, you will get a compiler error.

As always, you can force a non-unit result to be discarded by piping the results into "ignore".

You will also see the "do" keyword used in loops in the same way.

Note that although you can sometimes omit it, it is considered good practice to always have an explicit "do", as it acts as documentation that you do not want a result, only the side-effects.

"do" for module initialization

Just like let, do can be used both in a nested context, and at the top level in a module or class.

When used at the module level, the do expression is evaluated once only, when the module is first loaded.

This is somewhat analogous to a static class constructor in C#, except that if there are multiple modules, the order of initialization is fixed and they are initialized in order of declaration.

let! and use! and do!

When you see let!, use! and do! (that is, with exclamation marks) and they are part of a curly brace {..} block, then they are being used as part of a "computation expression". The exact meaning of let!, use! and do! in this context depends on the computation expression itself. Understanding computation expressions in general will have to wait for a later series.

The most common type of computation expression you will run into are asynchronous workflows, indicated by a async{..} block. In this context, it means they are being used to wait for an async operation to finish, and only then bind to the result value.

Here are some examples we saw earlier in a post from the "why use F#?" series:

Attributes on let and do bindings

If they are at the top-level in a module, let and do bindings can have attributes. F# attributes use the syntax [<MyAttribute>].

Here are some examples in C# and then the same code in F#:

Let's have a brief look at three attribute examples:

  • The EntryPoint attribute used to indicate the "main" function.

  • The various AssemblyInfo attributes.

  • The DllImport attribute for interacting with unmanaged code.

The EntryPoint attribute

The special EntryPoint attribute is used to mark the entry point of a standalone app, just as in C#, the static void Main method is.

Here's the familiar C# version:

And here's the F# equivalent:

Just as in C#, the args are an array of strings. But unlike C#, where the static Main method can be void, the F# function must return an int.

Also, a big gotcha is that the function that has this attribute must be the very last function in the last file in the project! Otherwise you get this error:

Why is the F# compiler so fussy? In C#, the class can go anywhere.

One analogy that might help is this: in some sense, the whole application is a single huge expression bound to main, where main is an expression that contains subexpressions that contain other subexpressions.

Now in F# projects, there are no forward references allowed. That is, expressions that refer to other expressions must be declared after them. And so logically, the highest, most top-level function of them all, main, must come last of all.

The AssemblyInfo attributes

In a C# project, there is an AssemblyInfo.cs file that contains all the assembly level attributes.

In F#, the equivalent way to do this is with a dummy module which contains a do expression annotated with these attributes.

The DllImport attribute

Another occasionally useful attribute is the DllImport attribute. Here's a C# example.

It works the same way in F# as in C#. One thing to note is that the extern declaration ... puts the types before the parameters, C-style.

Interop with unmanaged code is a big topic which will need its own series.

Last updated

Was this helpful?