rule : Configuration -> Review.Rule.Rule
Reports recursive functions that are not tail-call optimized.
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.
optOutWithComment : String -> Configuration
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
n
The 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.
optInWithComment : String -> Configuration
Reports only the functions tagged with a comment.
config =
[ NoUnoptimizedRecursion.rule (NoUnoptimizedRecursion.optInWithComment "ENSURE TCO")
]
With the configuration above, the following function would be reported.
fun n =
-- ENSURE TCO
if condition n then
fun n * n
else
n
This 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 NoUnoptimizedRecursion
The 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
n
This 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 * n
This function won't be reported because it has not been tagged.
-- With opt-in configuration
config =
[ NoUnoptimizedRecursion.rule (NoUnoptimizedRecursion.optInWithComment "ENSURE TCO")
]
fun n =
fun n * n
To 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 apply tail-call optimization only when a recursive call (1) is a simple function application and (2) is the last operation that the function does in a branch.
(1) means that while recurse n = recurse (n - 1)
would be optimized, recurse n = recurse <| n - 1
would not. Even though you may consider <|
and |>
as syntactic sugar for function calls, the compiler doesn't (at least with regard to TCO).
As for (2), the locations where a recursive call may happen are:
and only if each of the above appeared at the root of the function or in one of the above locations themselves.
The compiler optimizes every recursive call that adheres to the rules above, and simply doesn't optimize the other branches which would call the function naively and add to the stack frame. It is therefore possible to have partially tail-call optimized functions.
Following is a list of likely situations that will be reported.
The result of this recursive call 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) * n
Hint: 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
factorialHelp (result * n)
and split the function into the one that will do recursive calls (above) and an "API-facing" function which will set the initial result value (below).
factorial : Int -> Int
factorial n =
factorialHelp n 1
Even 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
n
The fix here consists of converting the recursive calls to ones that don't use a pipe 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 xs
The 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
False
Calls from let functions won't be optimized.
fun n =
let
funHelp y =
fun (y - 1)
in
funHelp n
Note that recursive let functions can be optimized if they call themselves, but calling the parent function will cause the parent to not be optimized.