Open showell opened 4 years ago
Here is example code that computes shows how you might compute which let
functions are truly local, i.e. bound to the outer function's arguments (either directly or indirectly):
https://github.com/showell/elm-start/blob/truly-local-let-functions/src/CallGraph.elm
I assume the compiler could do something similar without incurring too much expense. The main logic is just streaming a bunch of list/set/graph helpers together:
getTrueLocals : Ast -> Set String
getTrueLocals ast =
let
localFuncNames =
ast.lets
|> List.map .name
|> Set.fromList
-- does our let function directly depend on any
-- of the outerArgs?
isDirectDep letF =
Set.intersect letF.deps ast.outerArgs
|> Set.isEmpty
|> not
-- exclude toplevel deps; they're just noise
localDeps letF =
Set.intersect
letF.deps
localFuncNames
directLocals =
ast.lets
|> List.filter isDirectDep
|> List.map .name
|> Set.fromList
depGraph =
ast.lets
|> List.map
(\letF ->
( letF.name
, localDeps letF
)
)
|> Dict.fromList
reversedGraph =
depGraph
|> reverseGraph
allTrueLocals =
getAllDescendants reversedGraph directLocals
in
allTrueLocals
The Elm compiler could improve its support for
lazy
operations with innerlet
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:
https://github.com/elm/html/issues/201
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:
The functions
double
andtimesThree
are helper functions that are lexically insidesomeMath
, but they're not closing on a, b, or c. The Elm code is semantically equivalent to this:The compiler currently emits this for the first Elm example above:
Obviously, the above JS computes the correct values, but it needlessly reassigns
_double
andtimesThree
every time you callsomeMath
, and that undermines any possible optimizations inHtml.Lazy
(or, more precisely,_VirtualDom_diffHelp
).I think Elm could help
Html.Lazy
by emitting this instead:It's a two-step change here:
(function () { ...}())
.timesThree
and_double
by one scope. (They're still not top-level, so no collision.)Obviously we leave any function like
compute
that usesa/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 forHtml.Lazy.lazy
, but the idea here is that any inner function that you wrap inlazy
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:The tough case looks something like this: