dotnet / dotnet/fsharp

Module `do` is often not evaluated, static initializers

Open
#14,362 10 comments 2 reactions 0 assignees View on GitHub
Area-Compiler-CodeGen Feature Improvement Needs-RFC Theme-Simple-F#
Dominant language
F#
Stars
4.3k
Forks
876
Avg merge
4d 22h
Merged PRs (30d)
144

Description

This is a known issue, or feature so to say. The language spec (screenshot below) has an extensive list of reasons when _static initialisers_ aren't evaluated. The spec doesn't specifically mention `module do` there, but it applies.

The [docs say about this just the following](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/functions/do-bindings):

> Use a `do` binding when you want to execute code independently of a function or value definition. The expression in a `do` binding must return `unit`. Code in a top-level `do` binding is executed when the module is initialized. The keyword `do` is optional.

This isn't very clear and arguably not true. The common idea (as often stated in online posts) is that the `do` binding is executed "on first access", basically similarly to how `static do` works in classes. This is incorrect.

If you manage to find the section in the F# spec and you disentangle it, it turns out that the `do` is _only_ executed if it's either in the same file as `main` (or a script file), or if it has a referential dependency on mutable state (i.e., a `let mutable`), or a normal `let` that happens to be bound to something not "constant-like".

### Repro steps

There are many subtleties to this, but as a "simplest example":

```f#
module Test =
do printfn "Hello world"

let f() = printfn "Doing something interesting"
```

In another file, or reference the above through a project reference, call `Test.f()`. You will notice that `Hello world` is never printed.

### Expected behavior

Module `do` gets executed on first access to the module.

### Actual behavior

Module `do` does not get executed ever. However, as soon as you try to reproduce it by dumping it in a script file or copying it to FSI, you will see that it does get executed.

### Known workarounds

Force a referential dependency on a mutual. This is far from trivial and has led to many discussions and hard-to-diagnose bugs.

Now the `do` will get executed:

```f#
module Test =
let mutable __force = 42
do printfn "Hello world"

let f() =
__force <- __force + 1
printfn "Doing something interesting"
```

### Related information

Perhaps we can improve by issuing a warning when initializing code is not being executed? Or, conversely, add an opt-in attribute that would add the `module do` to the static constructor of the module, as opposed to the `StartupCode$File` cctor (which contains the static fields, forcing the initialization only when there's some mutable state or non-trivial let binding).

[From the spec](https://fsharp.org/specs/language-spec/4.1/FSharpSpec-4.1-latest.pdf):

![image](https://user-images.githubusercontent.com/16015770/203073194-ceefb1e9-65ce-4643-b288-01e207aeaddf.png)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.