Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Module do is often not evaluated, static initializers #14362

Open
abelbraaksma opened this issue Nov 21, 2022 · 10 comments
Open

Module do is often not evaluated, static initializers #14362

abelbraaksma opened this issue Nov 21, 2022 · 10 comments
Labels
Feature Improvement Needs-RFC Theme-Simple-F# A cross-community initiative called "Simple F#", keeping people in the sweet spot of the language.
Milestone

Comments

@abelbraaksma
Copy link
Contributor

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

@github-actions github-actions bot added this to the Backlog milestone Nov 21, 2022
@abelbraaksma abelbraaksma changed the title Module do is often not evaluated Module do is often not evaluated, static initializers Nov 21, 2022
@kerams
Copy link
Contributor

kerams commented Nov 21, 2022

A recent conversation here #13905

@abelbraaksma
Copy link
Contributor Author

abelbraaksma commented Nov 21, 2022

@kerams, thanks. There's also this 100-message long discussion with @kspeakman with the conclusion here (remains available for a month or so), related to this code that should be possible with module do.

Then there's this SO question by @natalie-o-perret and this old issue: #890.

I know it is not the first time this is raised, but I feel like we should find some low-hanging fruity way of making this less hurtful. Maybe including updating the docs and perhaps adding a "don't use module do in x y z scenarios" in the F# Coding Guidelines.

@kspeakman
Copy link

@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?

@T-Gro T-Gro added Theme-Simple-F# A cross-community initiative called "Simple F#", keeping people in the sweet spot of the language. Feature Improvement and removed Bug labels Nov 22, 2022
@T-Gro
Copy link
Member

T-Gro commented Nov 22, 2022

Woule a solution be to treat it on par with static constructors of regular types?

What I am afraid is that we would need to guard around this in optimizations, preventing inlining in particular.

@T-Gro
Copy link
Member

T-Gro commented Nov 22, 2022

Right now (AFAICS) the generation is driven by "doesSomething" at https://github.com/dotnet/fsharp/blob/main/src/Compiler/CodeGen/IlxGen.fs#L10241

The obviously bad thing is that a refactoring property of "moving modules around" is not stable.
The same for trying out a module in FSI vs. having in production code.

    // The code generation for the initialization is now complete and the IL code is in topCode.
    // Make a .cctor and/or main method to contain the code. This initializes all modules.
    //   Library file (mainInfoOpt = None) : optional .cctor if topCode has initialization effect
    //   Final file, explicit entry point (mainInfoOpt = Some _, GetExplicitEntryPointInfo() = Some) : main + optional .cctor if topCode has initialization effect
    //   Final file, implicit entry point (mainInfoOpt = Some _, GetExplicitEntryPointInfo() = None) : main + initialize + optional .cctor calling initialize
    let doesSomething = CheckCodeDoesSomething topCode.Code

@kspeakman
Copy link

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

@abelbraaksma
Copy link
Contributor Author

I don't think this should become a language suggestion (@0101 marking this Needs RFC). As @T-Gro's comments go, there's not really a feasible way to force-evaluate anything that goes into a module-do in a library. When it's an executable with an entry point, we have a clear moment where these dos get executed, same for FSI.

An alternative solution would be to allow ModuleInit attributes, like C# now does, which should provide a place for initializing library code as soon as it is resolved.

I'd rather change this one into a warning, as clearly this surprises even seasoned programmers and we won't be able to fix the existing behavior: code in the wild may rely on it.

@T-Gro
Copy link
Member

T-Gro commented May 24, 2024

I like the ModuleIntializerAttribute proposal for library code, leaning on the runtime to ensure an exactly-once execution.

https://github.com/dotnet/runtime/blob/main/docs/design/specs/Ecma-335-Augments.md#module-initializer

@abelbraaksma
Copy link
Contributor Author

abelbraaksma commented May 24, 2024

@T-Gro, yes. Keep in mind that module in ECMA-335 is not the same as module in F#, but still, I think it is a viable way for allowing ways to execute this particular run-once use-case for libraries, and to guarantee is runs.

I'll turn it into a language suggestion... Hmm, there was a language suggestion in the past, but it was turned down. Perhaps we can see if there's room for now? Original proposal: fsharp/fslang-suggestions#1024

@T-Gro
Copy link
Member

T-Gro commented May 27, 2024

The original suggestion did not have motivating examples where the existing F# features do not suffice and lead to unexpected (by the programmer) missed calls. This issue has it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature Improvement Needs-RFC Theme-Simple-F# A cross-community initiative called "Simple F#", keeping people in the sweet spot of the language.
Projects
Status: New
Development

No branches or pull requests

5 participants