question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Module `do` is often not evaluated, static initializers

See original GitHub issue

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:

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”:

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

From the spec:

image

Issue Analytics

  • State:open
  • Created 10 months ago
  • Reactions:2
  • Comments:6 (4 by maintainers)

github_iconTop GitHub Comments

2reactions
kspeakmancommented, Nov 23, 2022

Classes have static do for this purpose. Could modules have static do? This would add no new syntax and would have the same behavior (executed on first access of type). The existing do 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).

1reaction
kspeakmancommented, Nov 21, 2022

@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 module do, what is the interop story supposed to be with statically configured libraries?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Initialization of a static variable in the scope of a function or ...
I found in Rust reference that static items' initialization are evaluated at compile time. I wonder what exactly happens at runtime when the ......
Read more >
C++ - Initialization of Static Variables
Dynamic initialization happens at runtime for variables that can't be evaluated at compile time. Here, static variables are initialized every ...
Read more >
Provide a way to gather static initializers · Issue #369 · dart- ...
I have no idea when exactly _initialize is evaluated, if it is preemptively or lazily evaluated, I just know that it is working...
Read more >
Why does my explicit init crash?
The default memberwise initializer does not evaluate variable-initialization expressions when explicit values are passed in.
Read more >
Static initialization blocks - JavaScript - MDN Web Docs
Static initialization blocks are declared within a class. It contains statements to be evaluated during class initialization.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found