I think it’s unlikely that PureScript would ever get dynamic loading as part of the core language. For a general purpose language, I think it’s a bit of an odd feature, since it’s not that useful outside of SPAs. That said, I think it would be possible to implement a lazy loading solution into PS as some sort of corefn/backend pass or even just on the output JS itself in whatever bundler you are using. It may not look like the most elegant thing, but I’m sure it could be fairly robust. The key bit is “just” that you need to use require.ensure or dynamic import which of course we cannot surface in PS directly.
As an example, say we have this module:
module LazyTest where
import Prelude
import Effect.Random (random)
import Math (round)
import ModuleA as A
import ModuleB as B
import Module.Lazy (load)
main = do
n <- round <$> random
if n > 0
then load "ModuleA"
(\e -> Console.error e)
(\_ -> A.doThis)
else load "ModuleB"
(\e -> Console.error e)
(\_ -> B.doThat)
Here there are two modules we want to conditionally load (ModuleA, and ModuleB). A key bit is that we don’t really need first class modules, and we still use standard import syntax. The function load would be something like
foreign import load :: forall sym.
String ->
(Error -> Effect Unit) ->
(Unit -> Effect Unit) ->
Effect Unit
That is, opaque magic. And indeed the foreign implementation would be to just call the success handler. How does that give us lazy loading? It doesn’t as is, but it’s something that works with the existing compiler toolchain, and you’d get a working application if you did this, but without actually lazy loading anything.
In a separate corefn/backend pass you could replace these load calls with require.ensure, and you could replace any reference to the provided module with the dyamically imported package.
So as is the compiler might codegen the above code like:
var Prelude = require("../Prelude/index.js");
var Effect_Random = require("../Effect.Random/index.js");
var Math = require("../Math/index.js");
var ModuleA = require("../ModuleA/index.js");
var ModuleB = require("../ModuleB/index.js");
var Module_Lazy = require("../Module.Lazy/index.js");
main = function __do() {
var n = ...
if (n > 0) {
return Module_Lazy.load("ModuleA")(...)(function (__unused) {
return function () {
return ModuleA.doThis;
};
});
} else {
...
}
};
That is, ModuleA and ModuleB are still statically referenced, so we don’t get anything (but it works!). If your corefn/backend pass could then translate this form into
var Prelude = require("../Prelude/index.js");
var Effect_Random = require("../Effect.Random/index.js");
var Math = require("../Math/index.js");
var Module_Lazy = require("../Module.Lazy/index.js");
main = function __do() {
var n = ...
if (n > 0) {
return function () {
require.ensure([], function (require) {
var ModuleA = require("../ModuleA/index.js");
ModuleA.doThis();
});
};
} else {
...
}
};
Here we’ve remove the ModuleA and ModuleB imports from the top level, and just moved them under the require.ensure call. You would need to do some validation, like the lazily imported modules can only be referenced under the load callback, and load must be called in a fully saturated form. This is kind of like what the compiler does with various Eff/Effect inlining. The foreign implementation is something that works (but maybe not ideal), and then when fully saturated it can replace it with a faster inline version.