Module `do` is often not evaluated, static initializers
See original GitHub issueThis 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:
Use a
do
binding when you want to execute code independently of a function or value definition. The expression in ado
binding must returnunit
. Code in a top-leveldo
binding is executed when the module is initialized. The keyworddo
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”:
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:
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).
Issue Analytics
- State:
- Created 10 months ago
- Reactions:2
- Comments:6 (4 by maintainers)
Top GitHub Comments
Classes have
static do
for this purpose. Could modules havestatic do
? This would add no new syntax and would have the same behavior (executed on first access of type). The existingdo
behavior could remain the same.A module
static do
feels semantically redundant. But maybe I’m just thinking that because I know they compile to static classes (implying all members are static).@abelbraaksma Thanks so much for tracking this down.
My use case is integrating with a .NET library (Dapper) that requires a static, one-time configuration. The initialization adds support for F# Option types.
Because my library’s module
do
would never run, I had to resort to a low-level form of initialization that has to be called in every function on the module. Because I can’t predict which function will be called first.It seems at first that user-facing initialization is an alternative solution. That is, requiring the user to call
initialize
before calling other library fns. This still requires the same low-level initialization as before. Because a library may be used both directly and transitively by other libraries. (And I do use the referenced lib this way.) Both would have to call the initialization.Note: User-facing initialization is preferable when startup has a significant cost. So that the user doesn’t pay it unexpectedly, such as in a performance-critical section. Here, that is not the case.
My use case exactly fits a static constructor. But I want to use idiomatic F# with modules (which compile to static classes) rather than classes. I thought module
do
would suffice. But in libraries,do
code gets optimized away unless objects in it are referenced by other compiling code. So it can’t be used to initialize static dependencies as a static constructor can. Nor even the library’s own configuration in some cases. Why can’t it be so? If not moduledo
, what is the interop story supposed to be with statically configured libraries?