Fable is not running initialization code of modules in the correct order?

Created on 27 Aug 2018  ·  24Comments  ·  Source: fable-compiler/Fable

Description

It seems like fable it not running the initialization blocks of modules in the correct order?

Workaround

Add dummy functions.

Related information

  • Fable version (dotnet fable --version):
$ dotnet fable --version
1.3.17
  • Operating system: Windows 7


Previous assumption

Description

Im my scenario I had a project with

Logging.fs
../shared/Logging.fs

compiling fine but not working in the browser (can't remember the exact error but it was very specific to the code in the files). I might have had clashing member names in those modules as well.

Let me know, but it should be straightforward to reproduce.

All 24 comments

Most of Elmish apps have many files with the same name and module (State, Rest, etc) so it's unlikely that this is causing the problem. Could you please provide more details?

Yes give me a bit of time. I'm pretty sure I'm encountering something fishy here. I also had some problems with initialization order. I hope I can provide a repro showing both. Opened this to check if this is something well known or unsupported.

Ok I think I have reproduced what I have encountered today:

It seems to have nothing to do with name clashing (I might have been a bit confused by accident). Basically you can reproduce:

image

If you uncomment Client.fs line 16 it works, which doesn't make any sense whatsoever.

I'm changing the title accordingly.

Thanks for giving more details about this @matthid. It seems the problem here is how Fable translates references between files into JS imports. Because of this, if a file is not referenced by another ultimately leading to the main file (last one in the projects) it won't make into the JS bundle. In your case, if you don't call anything from Logging.fs, the whole file will be erased so the window?LogManager assignment won't take place.

This is necessary for proper integration with JS tooling and take advantage of important features like incremental compilation and dead code removal with Webpack. But I understand it may be annoying for devs used to how F# initializes modules in .NET. We had some discussion about this in #1439 but I'm still reluctant to try alternatives because it will be an important change on how Fable works to support a pattern (static module initialization) that I'm not personally very interested in promoting.

My recommendation is to add an init function to the modules that need it, and explicitly call it from the main file.

While definitely not understanding all the nuances of how fable works, I can only report that tree shaking was not the problem (or at least not obvious to me). In fact in my real application we had a "init" funtion called from the entry point, but apparently to late. I think if you move the dummy function lower or into a function it will stop working again.

Or maybe I did not understand what you tried to explain :)

Is there any documentation on where the differences are or how fable works I can consult (besides the source code)? This is definitely a subtle difference...

Or maybe in other words: Currently, I don't even understand why the workaround works when looking at the generated javascript. At least I'd like to understand what's going on so my future me can debug such issues faster ;)

So my suggestion would be instead of this:

module Foo // in Foo.fs

do foo()

// ------------------------------------

module App // in App.fs

do somethingThatDependsOnFoo()

do this:

module Foo

let init() =
    foo()

// ------------------------------------

module App

let init() =
    Foo.init()
    somethingThatDependsOnFoo()

do init() // We can safely do actions in the main file

About documentation, this is the page about compatibility with .NET. I try not to make it very long because people don't read otherwise and also it's difficult to keep it sync as Fable evolves. But of course if there's anything we can add to improve it, PRs are very welcome :)

Yes I understand, however this makes our use-case a bit awkward because goal was to have some static logger instances at the top of every module. with this init function we would probably have to use mutables in every module. Instead We probably go with my suggested workaround.

Also this doesn't really clarify the rules regarding loading initialization, which I feel are important to clarify in order to explain and understand stuff like this.

Well, a dummy function is the same as an empty function so either is fine :)

What I don't understand is why you do this:

let [<Global>] LogManager : ILogManager = jsNative

do window?LogManager <-
    { new ILogManager with
        member x.Create name =
            new Logger(name) :> ILogger }

instead of just this, which would solve your problem:

let LogManager =
    { new ILogManager with
        member x.Create name =
            new Logger(name) :> ILogger }

It's because you have to expose LogManager to JS code? In any case, in the current code the first line (let [<Global>] LogManager : ILogManager = jsNative) is doing nothing. This is used to declare something that is already global (because of the environment or because it's already made global by a JS lib) but in your case you're creating a global value with the same name (by attaching it to the window object) right after that.

It's because you have to expose LogManager to JS code?

yes we use that global in other projects as well (without setting it). And this seems to be the easiest approach... The idea is to have “uniform” code. Other projects work fine because at that point everything is initialized properly.

And disclaimer: I’m just getting started with the javascript fable stuff, usually I’m learning fast but it’s quite possible that I’m just doing this stuff the wrong way and try to get with the head through the wall ;)

Are these projects referencing each other or each one generates its own bundle? If the projects reference to each I would just made LogManager accessible to other projects as you would do in a normal F# solution.

They generate their own bundles and are loaded dynamically.

Please be careful with that approach and only expose interfaces and primitive types this way. If the bundles happen to be compiled with different versions of Fable there may be mismatches in the implementation of F# types, etc. Interfaces are more or less guaranteed to stay the same because they're the main way to interact with JS.

Yes thanks for the pointers! In fact we probably need a way to "contribute" back to JavaScript eventually. It is a bit one-sided at the moment ;)

Just one note. Initially I tried:

let [<Global>] LogManager : ILogManager =
    { new ILogManager with
        member x.Create name =
            new Logger(name) :> ILogger }

Which I would assume should have the same effect as my code but it doesn't

Please have a look at the docs: http://fable.io/docs/interacting.html#importing-javascript-code

The Import and Global attributes are intended to import external code. Global tells Fable that value is supposed to have been declared globally somewhere, so Fable will ignore the expression after = and compile LogManager as a global identifier.

If you don't mind, we can close this issue as the situation with initialization is more or less intended. For discussion we already have #1439

Do we have an actual explanation why this doesn't work? The code is not dead code as I did call methods. Also it is not related to having multiple projects (as everything is a single project in my example).
So, I'm not really after a fix but we should clarify what happens here?

Or maybe I just don't understand the related issue :)

Global is only for binding global scope API from JavaScript. You can't use it to export a variable in the global scope.

There is 2 things to understand about [<Global>] attribute

When used on declaration

let [<Global>] LogManager : ILogManager = jsNative

Fable generate nothing.

When used to access API (sorry I don't really have a better name)

LogManager.Log("echo")

Fable generate:

LogManager.Log("echo")

This is exactly what we do to access console.log, document.querySelector, etc.

This is the only two things to remember.


:warning: :warning: :warning: :warning:

The next link, is a "demo" using the REPL and try to explain the relation between the F# and JavaScript code. I find it complex to explain by text, and I am not sure if will help you or no understand what's happening. Because it also need to understand how JavaScript scope works.

I wanted to put this disclaimer so you know it's not exactly needed to understand the following code:
Demo repl

So what you are trying to tell me is that when I use LogManager from another module it doesn't "Count" as module access?

I think it now makes sense, thanks

We can now close this if you are OK with this behavior. Personally, I think it's kind of "strange" to be able to access members of the modules (even if they are marked as global) without the module being initialized.

Was this page helpful?
0 / 5 - 0 ratings