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:
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:
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 let
s have no effect on the expression as whole. So, for example, the type of an expression containing nested let
s 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:
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.
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?