I wrote the following function that checks the validity of bracketed expressions:
let matched str =
let rec matched' stack = function
| "" -> isEmpty stack
| str ->
match first str with
| '(' | '[' | '{' as p -> matched' (push p stack) (rest str)
| ')' -> matchClosing '(' stack str
| ']' -> matchClosing '[' stack str
| '}' -> matchClosing '{' stack str
| _ -> matched' stack (rest str)
and matchClosing expected stack s =
match peek stack with
| Some c when c = expected -> matched' (pop stack) (rest s)
| _ -> false
matched' [] str
If we substitute the implementation of matchClosing
into matched'
, we get a tail recursive function. Can the F# compiler recognize this and optimize away the recursive calls?
Was the show cancelled? AMC surprised fans in November 2021 when they announced that Kevin Can F--k Himself Season 2 would be the final season.
Kevin has, of course, moved on quickly in the wake of Allison's disappearance. He's got the beard of sadness, and he's roped Pete into waiting on him hand and foot. But Pete has had it — he takes his suitcase and tells Kevin no more because he's moving to Lorraine's condo in Florida.
"The angle [creator Valerie Armstrong] takes on telling this particular story has nothing to do with me. It has nothing to do with Kevin James.
AFAICT your example isn't complete which makes it harder to check. I complemented it somewhat and was able to compile it.
Using ILSpy
one sees that the mutual recursion is still in place:
// F#: | ')' -> matchClosing '(' stack str
case ')':
return Program.matchClosing@39('(', stack, str);
// F#: | matched' t (tail s)
return Program.matched'@28(t, s.Tail);
So while it should be technically possible to unpack two mutually tail recursive function into a loop it's not done.
When checking the IL code we see that the the calls are tagged with .tail
// F#: | matchClosing '(' stack str
IL_0083: tail. // Here
IL_0085: call bool Program::matchClosing@39(char, class [FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1<char>, valuetype Program/SubString)
// F#: | matched' t (tail s)
IL_002a: tail. // Here
IL_002c: call bool Program::'matched\'@28'(class [FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1<char>, valuetype Program/SubString)
The .NET Jitter is in release mode kind enough to consider .tail
flag
// As you can see when debugging the code in WinDbg
02410bdf e8fbd3176b call clr!JIT_TailCall (6d58dfdf)
We also see when we debug in WinDbg that the stack don't grow. Unfortunately when looking at clr!JIT_TailCall
it does a fair amount of work meaning while it doesn't consume stack it consumes clock cycles instead like noted here: How to eliminate time spent in JIT_TailCall for functions that are genuinely non-recursive
However in Debug mode (and at least older versions of Mono) .tail
flag is ignored
// As you can see when debugging the code in WinDbg (this is a normal call)
02f619c1 e8c2f4ffff call 02f60e88
We also see when we debug in WinDbg that the stack grow.
So the answer to your question should be:
.tail
attribute.tail
attributes and generates tail calls that don't grow the stack (but are ineffecient).tail
attributes are ignored and no tail calls are generated by the JIT:er and the stack will grow.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With