-
Notifications
You must be signed in to change notification settings - Fork 666
Description
The Elm compiler could improve its support for lazy
operations with inner let
functions by emitting JS that doesn't reassign inner functions on every call to the outer function.
There is a related issue here that motivates this suggestion. Html.Lazy.lazy cannot optimize inner functions because the same function keeps getting assigned to new local variables:
The above issue includes an SSCCE for the particular case of Html.Lazy.
Here I'll discuss it in even more generic terms, using this repo branch as an example:
https://github.com/showell/elm-start/commits/hoist-js
Consider this Elm code:
three =
3
fac n =
List.product (List.range 1 n)
someMath a b c =
let
double x =
x * 2
timesThree x =
x * three
compute =
double a + timesThree b + fac c
in
compute
The functions double
and timesThree
are helper functions that are lexically inside someMath
, but they're not closing on a, b, or c. The Elm code is semantically equivalent to this:
three =
3
fac n =
List.product (List.range 1 n)
double x =
x * 2
timesThree x =
x * three
someMath a b c =
let
compute =
double a + timesThree b + fac c
in
compute
The compiler currently emits this for the first Elm example above:
var $author$project$Main$someMath = F3(
function (a, b, c) {
var timesThree = function (x) {
return x * $author$project$Main$three;
};
var _double = function (x) {
return x * 2;
};
var compute = (_double(a) + timesThree(b)) + $author$project$Main$fac(c);
return compute;
});
Obviously, the above JS computes the correct values, but it needlessly reassigns _double
and timesThree
every time you call someMath
, and that undermines any possible optimizations in Html.Lazy
(or, more precisely, _VirtualDom_diffHelp
).
I think Elm could help Html.Lazy
by emitting this instead:
var $author$project$Main$someMath2 = (function() {
var timesThree = function (x) {
return x * $author$project$Main$three;
};
var _double = function (x) {
return x * 2;
};
return F3(
function (a, b, c) {
var compute = (_double(a) + timesThree(b)) + $author$project$Main$fac(c);
return compute;
});
}());
It's a two-step change here:
- Wrap someMath2 with
(function () { ...}())
. - Promote
timesThree
and_double
by one scope. (They're still not top-level, so no collision.)
Obviously we leave any function like compute
that uses a/b/c
exactly where the compiler currently emits it.
Performance
For normal functions, the two semantically equivalent JS functions run at about the same speed. I've run benchmarks on Chrome/FF, and the results are inconclusive, except that either version runs in under a microsecond. The benchmarks are a bit noisy even if you have identical functions. You can run them off my branch:
https://github.com/showell/elm-start/commits/hoist-js
(Remember, don't rebuild index.html
, because I manually modified it for demonstration purposes.)
The bigger performance implication here, as mentioned earlier, is that in the second JS version, we only assign timesThree
and _double
one time. This example is obviously too contrived for Html.Lazy.lazy
, but the idea here is that any inner function that you wrap in lazy
would be short circuited as long as it doesn't close outer vars.
Details
In order to decide which functions go inside F3(function(a,b,c) {...})
block, it probably makes sense to walk the graph this way:
- first get all direct callers of a/b/c
- keep expanding list of inside-F3 functions with any callers (or callers of callers)
The tough case looks something like this:
someMath a b c =
let
double x =
x * 2
timesThree x =
x * three
compute =
double a + timesThree b + fac c
sneakyFunctionIndirectlyDependingOnABC =
compute
in
sneakyFunctionIndirectlyDependingOnABC