Tail-call optimization makes Elm code more performant and helps prevent stack overflows.
Since this optimization is done silently and under specific circumstances, it is unfortunately relatively easy to not notice when the optimization is not being applied. You can find the reasons why a function would not be optimized below.
Configuration for NoUnoptimizedRecursion.
Use optOutWithComment or optInWithComment to configure this rule.
You can use comments to tag functions as to be checked or ignored, depending on the configuration option you chose.
This comment has to appear on the line after the = that follows the declaration of your function. Note that this
comment only needs to contain the tag that you're choosing and that it is case-sensitive.
The same will apply for functions defined in a let expression, since they can be tail-call optimized as well.
Reports recursive functions by default, opt out functions tagged with a comment.
config =
[ NoUnoptimizedRecursion.rule (NoUnoptimizedRecursion.optOutWithComment "IGNORE TCO")
]With the configuration above, the following function would not be reported.
fun n =
-- elm-review: IGNORE TCO
if condition n then
fun n * n
else
nThe reasons for allowing to opt-out is because sometimes recursive functions are simply not translatable to
tail-call optimized ones, for instance the ones that need to recurse over multiple elements (fun left + fun right).
I recommend to not default to ignoring a reported issue, and instead to discuss with your colleagues how to best solve the error when you encounter it or when you see them ignore an error.
I recommend to use this configuration option as your permanent configuration once you have fixed or opted-out of every function.
Reports only the functions tagged with a comment.
config =
[ NoUnoptimizedRecursion.rule (NoUnoptimizedRecursion.optInWithComment "CHECK TCO")
]With the configuration above, the following function would be reported.
fun n =
-- CHECK TCO
if condition n then
fun n * n
else
nThis rule is useful for both application maintainers and package authors to detect locations where performance could be improved and where stack overflows can happen.
You should not enable this rule if you currently do not want to invest your time into thinking about performance.
You can try this rule out by running the following command:
elm-review --template jfmengels/elm-review-performance/example --rules NoUnoptimizedRecursionThe rule uses optOutWithComment "IGNORE TCO" as its configuration.
This function won't be reported because it is tail-call optimized.
fun n =
if condition n then
fun (n - 1)
else
nThis function won't be reported because it has been tagged as ignored.
-- With opt-out configuration
config =
[ NoUnoptimizedRecursion.rule (NoUnoptimizedRecursion.optOutWithComment "IGNORE TCO")
]
fun n =
-- elm-review: IGNORE TCO
fun n * nThis function won't be reported because it has not been tagged.
-- With opt-in configuration
config =
[ NoUnoptimizedRecursion.rule (NoUnoptimizedRecursion.optInWithComment "CHECK TCO")
]
fun n =
fun n * nTo understand when a function would not get tail-call optimized, it is important to understand when it would be optimized.
The Elm compiler is able to do tail-call elimination only when all the recursive calls are the last operation that the function would do in a branch. Any recursive calls happening in other locations de-optimizes the function.
Here are the locations when a recursive call may happen in:
and only if each of the above appeared at the root of the function or in one of the above locations themselves.
Following is a list of likely situations that will be reported.
The result of this function gets multiplied by n, making the recursive call not the last thing to happen in this branch.
factorial : Int -> Int
factorial n =
if n <= 1 then
1
else
factorial (n - 1) * nHint: When you need to apply an operation on the result of a recursive call, what you can do is to add an argument holding the result value and apply the operations on it instead.
factorialHelp : Int -> Int -> Int
factorialHelp n result =
if n <= 1 then
result
else
factorial (result * n)and split the function into the one that will do recursive calls (above) and a "API-facing" function which will set the initial result value (below).
factorial : Int -> Int
factorial n =
factorialHelp n 1Even though you may consider these operators as syntactic sugar for function calls, the compiler doesn't and
the following won't be optimized. The compiler doesn't special-case these functions and considers them as operators just
like (*) in the example above.
fun n =
if condition n then
fun <| n - 1
else
n
fun n =
if condition n then
(n - 1)
|> fun
else
nThe fix here consists of converting the recursive calls to ones that don't use the operator.
The following won't be optimized.
isPrefixOf : List a -> List a -> Bool
isPrefixOf prefix list =
case ( prefix, list ) of
( [], _ ) ->
True
( _ :: _, [] ) ->
False
( p :: ps, x :: xs ) ->
p == x && isPrefixOf ps xsThe fix here is consists of using if expressions instead.
isPrefixOf : List a -> List a -> Bool
isPrefixOf prefix list =
case ( prefix, list ) of
( [], _ ) ->
True
( _ :: _, [] ) ->
False
( p :: ps, x :: xs ) ->
if p == x then
isPrefixOf ps xs
else
FalseCalls from let functions won't be optimized.
fun n =
let
funHelp y =
fun (y - 1)
in
funHelp nNote that recursive let functions can be optimized if they call themselves, but calling the parent function will cause the parent to not be optimized.
Reports recursive functions that are not tail-call optimized.