Tail Recursion Explained - Computerphile

  Рет қаралды 167,521

Computerphile

Computerphile

4 жыл бұрын

Improve the efficiency of recursive code by re-writing it to be tail recursive. Professor Graham Hutton explains.
EXTRA BITS: • EXTRA BITS: Tail Recur...
/ computerphile
/ computer_phile
This video was filmed and edited by Sean Riley.
Computer Science at the University of Nottingham: bit.ly/nottscomputer
Computerphile is a sister project to Brady Haran's Numberphile. More at www.bradyharan.com

Пікірлер: 536
@daverotors
@daverotors 4 жыл бұрын
It's important to note that the language/compiler will need to support tail call optimisation, otherwise it's not helping as much: For example with the accumulator, a "naive" language implementation would still keep all the stack frames around until the end, and return the just-received value back up the stack. Only if the language supports TCO will it recognize the tail call and replace (overwrite) the current stack frame for the next call - which is where the optimisation helps to reduce memory usage.
@Henrix1998
@Henrix1998 4 жыл бұрын
Thanks, I was thinking about it the whole video. Constant memory just made no sense in that case
@TheAguydude
@TheAguydude 4 жыл бұрын
If the language *does* support tail call optimization, the general rule of thumb is that if the function has exactly one recursive call which is at the end of the function, then the compiler can optimize that call. For example, most compilers supporting tail call optimization can optimize a factorial function without the need for hand-tuning shown in the video. However, most compilers would requiring such hand-tuning before they could tail-call optimize the Fibonacci function. All that being said, functions where a tail call optimization is possible can often be translated into a loop in a very straightforward manner; in some sense that's what a tail call optimization usually ends up doing.
@ConstantlyDamaged
@ConstantlyDamaged 4 жыл бұрын
As someone getting into Elixir, thanks for pointing this out. This is literally how functional programs (like Elixir) have to do loops. For Python enthusiasts: don't do this (just use a loop you lazy sod).
@Lightn0x
@Lightn0x 4 жыл бұрын
It might also depend on the settings of your compiler. Both gcc and Visual Studio will have tail recursion enabled or disabled according to the optimization level set.
@ChocolateMilkCultLeader
@ChocolateMilkCultLeader 4 жыл бұрын
Thanks for this
@frognik79
@frognik79 4 жыл бұрын
No stack frames were harmed in the making of this video.
@pedrohenriquedadaltdequeir4859
@pedrohenriquedadaltdequeir4859 2 жыл бұрын
lolz
@lycan2494
@lycan2494 Жыл бұрын
cringe
@krishna9438
@krishna9438 4 жыл бұрын
Dear Computerphile, would it be possible for you to enable CC (the auto-generated one is enough) for us deaf viewers? I would really appreciate it. Thank you.
@FantasticW7World
@FantasticW7World 4 жыл бұрын
They could also enable community Translation
@HazhMcMoor
@HazhMcMoor 3 жыл бұрын
Yeah I'm pretty sure some ESL also will appreciate English sub especially complicated topics like this.
@haskellhutt
@haskellhutt 3 жыл бұрын
We are in the process of putting the subtitles together.
@krishna9438
@krishna9438 3 жыл бұрын
Graham Hutton Great😃
@haskellhutt
@haskellhutt 3 жыл бұрын
Subtitles now up!
@TomGalonska
@TomGalonska 4 жыл бұрын
Be aware that tail recursion in languages with lazy evaluation can actually make it slower or more memory-heavy. Let's take factorial as an example: with lazy evaluation, in the expression go (n-1) (a*n) the (n-1) would be evaluated because it's immediately needed in the next call of the function (to decide if this is the simple case go 1 a or the case go n a). The (a*n) isn't needed immediately, so it's not going to be evaluated. And the and you get sth. similar to the "primitive" fac function: go 3 1 = g (3-1) (1*3) = g 2 (1*3) = g (2-1) ((1*3)*2) = g 1 ((1*3)*2) = (1*3) * 2 = 6
@qzbnyv
@qzbnyv 4 жыл бұрын
Tom Galonska This is of course why in Haskell we use the {-# LANGUAGE BangPatterns #-} language pragma and put exclamation marks on the go function’s arguments (like ‘go !n !a = ...’) so that they are strictly evaluated. :)
@qzbnyv
@qzbnyv 4 жыл бұрын
Tom Galonska (or in the case of the fib function, `go !n (!a, !b)`)
@TomGalonska
@TomGalonska 4 жыл бұрын
@@qzbnyv I know ;) But with that you also have to be careful, because lazy evaluation is sometimes what makes Haskell so powerful (and yes, i know that Haskell technically is "non-strict", but i don't quite understand the difference :P)
@stensoft
@stensoft 4 жыл бұрын
I wonder, can't the compiler figure out from the definition that it will always need to evaluate the parameters and therefore can use strict evaluation?
@pedrovasconcelos8260
@pedrovasconcelos8260 4 жыл бұрын
@@qzbnyv True, but for these simple examples the GHC compiler can figure out that the results are used eventually when optimizations turned on (strictness analysis). Nonetheless, I agree that is it preferable to write code that does not depend on compiler cleverness to achive expected asymptotic efficiency.
@Number_055
@Number_055 4 жыл бұрын
As noted by avawn , your chosen language will need to support tail call optimisation to get the most of this technique, HOWEVER if your language does not support tail call optimisation, you can still exploit the efficiency of tail recursion. Since, by definition, tail recursive functions make the recursive call last, you can convert them to a while loop with minimal changes. All you generally need to do is define the parameters before the loop, then replace the recursive call with code that edits the parameters. The following is a demonstration in python, which does not support TCO: #Factorial n = 4 a = 1 while (n > 0): a = a*n n -= 1 print(a) #Fibonacci n = 4 a = 0 b = 1 while (n > 0): temp = a a = b b = temp + b n -= 1 print(a)
@JNCressey
@JNCressey 3 жыл бұрын
Or you can: import tail_recursion; my_function = tail_recursion.tail_call_optimise(my_function) Then the only thing you need to do is implement some tail_recursion.py that does that or find someone who's already done it.
@andreimiga8101
@andreimiga8101 3 жыл бұрын
In imperative languages, a while loop is almost always more efficient than its recursive counterpart (if written properly, of course), because there aren't any function calls and no new stack frames are created. A while loop defeats the whole idea of recursion. Tail recursion tries to be more efficient while still keeping the recursive nature of the function. The 2 shouldn't be compared.
@jeetadityachatterjee6995
@jeetadityachatterjee6995 3 жыл бұрын
While this is "practical" and " efficient" its boring! Even if there is probably a faster way and I am having fun (side or school project) I'll try and move away from loops and try and do everything functionally (maybe I just want too use scheme)
@felixfourcolor
@felixfourcolor 2 ай бұрын
Tip: you don't need a temp variable, you can do a, b = b, a+b
@randomelectronicsanddispla1765
@randomelectronicsanddispla1765 4 жыл бұрын
I'm surprised you didn't mention that it saves overloading the stack, it turns a "call, call, call,...., return, return, return" into a "call, while,..., loop, return"
@agmessier
@agmessier 4 жыл бұрын
I think it's important in this case to discuss how the stack normally gets used, and how tail recursion causes the current stack to get discarded, effectively allowing the program to forget it made recursive call. An important aspect of the optimization is the ability to return the result directly to the original caller and not have to traverse back down the stack.
@casperes0912
@casperes0912 4 жыл бұрын
This omits some of the cool compiler optimisations this allows you, like not setting up new stack space for the new call, and just reusing the context of the current call
@JamesTM
@JamesTM 4 жыл бұрын
So, am I wrong, or is this just a fancy name for redefining a recursive function as an iterable one?
@retropaganda8442
@retropaganda8442 4 жыл бұрын
Don't be ashamed to say goto.
@JoJoModding
@JoJoModding 4 жыл бұрын
@@JamesTM Yes the compiler will end up producing something that basically is a loop. But all you used was recursion.
@casperes0912
@casperes0912 4 жыл бұрын
James Tanner It’s actually quite fun - If you play around with different levels of compiler optimisation, no optimisation and -O will generally produce slower code than doing the loop yourself, but -O2 will be roughly the same as -O2 on a loop implementation, and -O3 the recursive one will actually be faster! (Experimentation done with a Collatz algorithm)
@JamesTM
@JamesTM 4 жыл бұрын
@@casperes0912 I'll admit, almost all my programming these days is in non-compiled languages. So I don't really get to enjoy the benefits of compile-time optimizations.
@duxoakende
@duxoakende 4 жыл бұрын
You guys are amazing. This channel and Numberphile have really given me so much inspiration to work in math and computer science. I probably owe my career to this channel. Thank you.
@ThugTheFerret
@ThugTheFerret 4 жыл бұрын
I couldn't agree more. Im a CS student and this channel is really inspiring to keep doing it!
@beziko
@beziko 3 жыл бұрын
I'm a professional developer and their videos are absolutely great for me too
@snensnurnberg8891
@snensnurnberg8891 4 жыл бұрын
Thank you Mr.Graham ! Really enjoyed this Lesson.
@Kartaal
@Kartaal 4 жыл бұрын
Nice little video explaining tail recursion with accumulators. I think it'd be really great to have one covering tail recursion with continuations though. Mostly to fill in the gap of "tail recursion is an optimised version of recursion" being not true for all cases. I just got done with a university course in F# so not that useful for me personally but I have always liked how accessible Computerphile videos are. Covering continuations in a similar manner would probably be really helpful to a lot of viewers.
@Qbe_Root
@Qbe_Root 4 жыл бұрын
In the tail-recursive Fibonacci example, there’s no need for 1 to be a base case, since go 1 (a, b) = go 0 (b, a + b) = b anyway
@Qbe_Root
@Qbe_Root 4 жыл бұрын
@@devrim-oguz Yeah, it would just take the recursive case one more time, then land in the base case for 0
@mannyc6649
@mannyc6649 4 жыл бұрын
What confused me about you comment is that there should always be two initial conditions for the Fibonacci sequence, no matter the implementation. But in this case the information about the second initial condition is implicit when you define fib n = go n (0,1)
@randomnobody660
@randomnobody660 4 жыл бұрын
I'm not sure why you would use recursion for Fibonacci sequence if memory usage is what you are concerned about thou. The sequence has a closed form expression.
@stensoft
@stensoft 4 жыл бұрын
​@@mannyc6649 The two initial conditions are the two values in the accumulator: (0,1). Recursion termination is not an initial condition and therefore needs to be there only once.
@TheHuesSciTech
@TheHuesSciTech 4 жыл бұрын
@@randomnobody660 These are just examples. Of course you'd use the closed form expression for Fibonacci in real life; but I suspect it's hard to find a simple-to-follow example to illustrate tail recursion that doesn't have a closed-form expression.
@NonTwinBrothers
@NonTwinBrothers 4 жыл бұрын
This was a pretty fun watch, thanks for sharing!
@Pedritox0953
@Pedritox0953 3 жыл бұрын
Love this video with Prof Hutton !!
@bradH2049
@bradH2049 Ай бұрын
The explanation was very clear and easy to understand. Thank you for making this world a better place.
@deefdragon
@deefdragon 4 жыл бұрын
THANK YOU FOR EXPLAINING THIS!!!
@warnercooler4488
@warnercooler4488 3 жыл бұрын
This was so helpful. Thank you so much!
@scheimong
@scheimong 4 жыл бұрын
I learned tail recursion in my uni's SML computing fundamentals course and knew how it worked, but never understood why it's called "tail recursion". Now I do so thanks.
@kurisu-makise
@kurisu-makise 4 жыл бұрын
Because the recursion occurs on the tail call :D
@andreasimonecosta
@andreasimonecosta 4 жыл бұрын
I'd like to see prof. Graham Hutton explaining Continuation Passing Style, it could be a nice next step after Tail Recursion. 😄😄😄
@pedrohenriquedadaltdequeir4859
@pedrohenriquedadaltdequeir4859 2 жыл бұрын
Thank you so much!!! I've been learning Haskell and this is super useful!!!! Loved to learn it :D
@Semtx552
@Semtx552 4 жыл бұрын
I keep finding great litle tools here that might help me write better scripts. Thanks!
@tifahefendijagaming9606
@tifahefendijagaming9606 4 жыл бұрын
Same
@georgichelenkov4360
@georgichelenkov4360 4 жыл бұрын
Thanks for the great content!
@thomasdtrain
@thomasdtrain 4 жыл бұрын
This was actually pretty interesting. Thanks.
@venil82
@venil82 4 жыл бұрын
My favourite Haskell professor 👨‍🏫
@mikehosken4328
@mikehosken4328 4 жыл бұрын
I remember doing both of these problems when I was 10. It takes about 4 lines of Apple Basic. Didn’t take long for it to overflow but I’m pretty sure they were efficient. Factorial was a for loop irc
@TheHuesSciTech
@TheHuesSciTech 4 жыл бұрын
Yes, tail recursion can easily be transformed into an iteration -- these are just simple examples to illustrate the idea of tail recursion, not applications where you'd actually use it in real life.
@Korudo
@Korudo 4 жыл бұрын
I love this. Thank you!
@nab-rk4ob
@nab-rk4ob 3 жыл бұрын
I missed this one. Thanks for teaching, me.
@DrGreenGiant
@DrGreenGiant 3 жыл бұрын
This is very elegant
@robertbrummayer4908
@robertbrummayer4908 2 жыл бұрын
Your videos are excellent
@nilp0inter2
@nilp0inter2 4 жыл бұрын
As always, thank you! these videos are a great resource. I hope he write more books soon.
@tomeru2377
@tomeru2377 3 жыл бұрын
Hello, thank you for your great videos, the videos contain a lot of technical details i notice that most of the videos are without subtitles, it will be great if you can add at least english subs
@yan-amar
@yan-amar 3 жыл бұрын
Thanks very much this was just what I was looking for.
@siderealdeveloper4954
@siderealdeveloper4954 3 жыл бұрын
What a nice explanation sir ♥ really appreciate
@sogerc1
@sogerc1 3 жыл бұрын
So I had to try it, I wrote two perl scripts implementing the two factorial algorithms, but the RSS value reported by ps is the same for both (2008 for fac(100)).
@leoncruz9757
@leoncruz9757 4 жыл бұрын
CPython: I have no idea of what you are talking about...
@themikead99
@themikead99 4 жыл бұрын
How did they come up with this? I'm interested in the history of this now, how did they figure it out? It's extremely clever.
@giga-chicken
@giga-chicken 3 жыл бұрын
Defining tail recursion also give you the opportunity to do other things just once instead of every recursive level, like asserting that n is an integer greater than zero.
@IslamWaleed-gg4mc
@IslamWaleed-gg4mc 3 ай бұрын
more than amazing video, thank you so much
@MrBlaq
@MrBlaq 4 жыл бұрын
Excellent vid
@ShadSterling
@ShadSterling 4 жыл бұрын
I would have preferred that you include how a tail call is different on the stack, or is there a part 2 coming?
@0LoneTech
@0LoneTech 3 жыл бұрын
It's different on the stack if tail call optimization is performed. That's because it can outright replace the current stack frame, because the return value of the two calls is the same. As a bonus, tail recursion means we know the two stack frames have the same shape, and can replace the call with a local branch. Thus it's easier than reshuffling the arguments for calling a different function.
@oktavarium
@oktavarium 4 жыл бұрын
Pretty satisfactorial!
@eyemotif
@eyemotif 4 жыл бұрын
my time with f# has made me write all my recursive functions with the fac/go format
@petemagnuson7357
@petemagnuson7357 4 жыл бұрын
TIL I've been using this for a while. A bunch of programming games don't have a stack or anything, so you need to implement this sort of pseudo-loop to get recursion to work right.
@durian7551
@durian7551 4 жыл бұрын
I'm not a pro on this, but what is the difference between the go function and a for loop? The two examples the prof gave here can be replaced by a for loop doing essentially the same thing as the go function. Is it true that every tail recursion can be replaced by a for loop? Can anyone give an example that must involve a go function? I'm thinking about something like this: accumulator = initial value for (i=n, i>=0, i -= 1): accumulator = foo(i, accumulator) return accumulator where foo is non-recursive
@sheeplessknight8732
@sheeplessknight8732 4 жыл бұрын
I coded a tail recursive function literally a week ago and I had no clue this was what it was called!!!
@RainDog222222
@RainDog222222 4 жыл бұрын
remove literally from that sentence and it is precisely the same, only more efficient!
@Belioyt
@Belioyt 4 жыл бұрын
Sheepless Knight, did you study computer science?
@skoto8219
@skoto8219 4 жыл бұрын
RainDog222222 Why did you include “precisely” in that sentence? It’s already implied in “the same”.
@dross6206
@dross6206 4 жыл бұрын
skoto “Precisely the same” -> === “The same” -> ==
@dibblethwaite
@dibblethwaite 4 жыл бұрын
@@RainDog222222 That is veritably true. :)
@emm7494
@emm7494 Жыл бұрын
So the go function is overloaded to return a value for the second base case but how do we reach the first base case where parameter n==0 as n reduces in the function call.
@lkbriones9315
@lkbriones9315 4 жыл бұрын
Thank you! I’m coding this one later
@JamesTM
@JamesTM 4 жыл бұрын
In languages with stack frames, won't this kind of recursion still build up memory usage by adding more frames to the stack as it goes? Also, I notice that it has a single passed "state" as it moves along. So, it seems to me that, by definition, you could rewrite any tail-recursive expression as an iterative expression in a fixed memory space. Am I missing something? Is there a case where that's not true, or where the recursion would be more efficient and/or inherently desirable?
@framegrace1
@framegrace1 4 жыл бұрын
Functional languages recognize you are using tail recursion and don't use the stack. They just do a loop with whatever is inside the recursive call. Don't try this with ruby, python, java or go, for example. You will get an stack overflow. gcc can detect tail recursion usagfe in c++ and also optimize it, but not sure if does that automatically.
@CryZe92
@CryZe92 4 жыл бұрын
An optimizer will notice that the last instruction of the function would be another function call, which it can turn into a jump instruction instead. This then gets rid of the entire stack frame and effectively turns the recursion into a loop. That's often only an optimization though, not a guarantee. Though with keywords such as `become` it can be guaranteed at the language level.
@dealloc
@dealloc 4 жыл бұрын
This is not necessarily a limitation in stack frame-based languages, as it's possible to add tail call optimization. One example is ECMAScript 6, which includes specification about TCO. V8 (Google's engine used in Chrome to run JavaScript) did have an unstable implementation of this at some point but was removed due to drawbacks like losing information about the stack during debugging and breaking software which relies on stack frames to collect telemetry. There are solutions to this, but ECMAScript is in a unique position in that the point of the specification should always be backwards compatible.
@RamHomier
@RamHomier 3 жыл бұрын
The go function in the end if just a fancy iteration? It seems to do the exact same thing as the following pseudo code, or am I missing something. answer = 1 for i in [1 to 5]: answer *= i
@TheDarkOne629
@TheDarkOne629 2 жыл бұрын
That's a thing with functional languages. They are very, very different from imperative languages you might be used to. (It took me 7 months to "get" functional languages and I am not embaressed to say it...) Take a look at continuation-passing-style for functional languages. It is a technique for compilers. It lends itself well to tail-recursion, but not to loops. SSA-form for imperative languages is better for loops. There are some great talks on it on LLVM conferences. Tldr: Haskell != C. Thanks a lot for aaking. :D
@akritishrestha4318
@akritishrestha4318 2 жыл бұрын
what will be the time complexity and auxiliary space of a tail recursive function ? since in modern compilers it is not considered to be recursive.
@DerrickJolicoeur
@DerrickJolicoeur 4 жыл бұрын
Can someone clarify something for me: I think tail recursion still uses *some* memory due to building up a stack. Am I wrong or do modern languages abstract this and actively eliminate the stack when it recognizes properly formatted tail recursion? It makes sense in theory that nothing is really being remembered outside the ever-changing arguments, but I can't help but feel if the stack really does grow and consume *some* resources, that any infinitely large recursion will ultimately consume all the resources.
@Broan13
@Broan13 4 жыл бұрын
How would it be building up a stack? At least in the examples he showed, there is no stack involved, but effectively building in result variables into the call of the function. It is effectively like using a while loop but in the function definition - while input not = base case, do this other thing. (Nevermind, I don't actually know how these things are implemented. After reading a few other comments, I need to do some reading.)
@n00dle_king
@n00dle_king 4 жыл бұрын
It’s language dependent. Languages that support tail recreation recognize it and drop the unused stack frames. Python for example doesn’t support tail recursion so if you tried this you’d hit the stack limit of like 500, but in a language that does support it you could write an infinite loop using tail recursion and you’ll never run out of memory.
@diego1694
@diego1694 4 жыл бұрын
gcc and clang are able to eliminate recursion from these functions, tail recursion or not. Try it in godbolt.org.
@123xqp
@123xqp 4 жыл бұрын
As written, it will use some stack to a depth proportional to N. Rewriting using tail recursion means that when you reach the basse case, there's nothing else to do and you can just return. You (or rather, the compiler) can the rewrite this to use a more efficient loop. The problem with the simple recursive Fibonacci is that it calculates the same values over and over again, and this work doubles each time you move from F(n) to F(n+1)
@1vader
@1vader 4 жыл бұрын
In some languages the compiler or runtime will perform so-called tail call elimination where the tail-recursive call will reuse the current stack frame. This is done in pretty much all functional languages and also in some imperative languages although it's not that common in the latter. For example Java, Python, or Rust don't do tail recursion, at least not by default or not yet, in part also because these languages allow you to inspect the stack trace which will get messed up by tail call elimination.
@Pedritox0953
@Pedritox0953 4 жыл бұрын
I love yo6r videos Prof Graham
@levmatta
@levmatta 4 жыл бұрын
Please talk about have the compiler and runtime deal with this in functional And imperative languages
@Kepler57
@Kepler57 Жыл бұрын
I cant discribe how your lesson is helpful. Thank you so much. Where is the button for 1000000000000000000000000000000000000000 likes?
@NeelSandellISAWESOME
@NeelSandellISAWESOME 3 жыл бұрын
This is really cool. I have seen this "sliding window" concept in a lot of other programming things.
@abbykr999
@abbykr999 4 жыл бұрын
I wrote such recursion for fibonacci series on my own, I had no idea it had a name!
@Necarion1
@Necarion1 4 жыл бұрын
Can someone explain why this form of tail recursion (at least the ones demonstrated in the video) aren't completely equivalent to a for-loop? The Fibonacci example in particular seemed like it was just counting down from n to 0, which you could do with a while or for loop. When might you want to use tail recursion instead of loops, if the structure is so similar? I'm asking seriously; my background in CS is pretty limited!
@retropaganda8442
@retropaganda8442 4 жыл бұрын
This is why teaching assembler still makes sense. This kind of problem is better explained at machine level.
@brettbreet
@brettbreet 4 жыл бұрын
Nevermind that 1 million factorial has more than 5 million digits in it!
@1vader
@1vader 4 жыл бұрын
Python or any other language with arbitrary precision integers can still calculate and display it. I just tried it out and I can do 100,000! without problems in Python but 1,000,000! takes a bit too long to calculate so I couldn't be bothered to wait until it finishes. However, I see no reason why it wouldn't work.
@haskellhutt
@haskellhutt 3 жыл бұрын
I tried it out, and calculating that 1 million factorial has 5.5M digits takes around two minutes using the tail recursive version of the function :-)
@lycan2494
@lycan2494 Жыл бұрын
@@haskellhutt nbod ycares
@123FireSnake
@123FireSnake 4 жыл бұрын
i my opinion the most important thing to know about recursion is that every recursion can be written in a loop (additionally that loop optimization is a thing^^)
@TheAguydude
@TheAguydude 4 жыл бұрын
True, but in many cases translating a recursion into a loop requires you to store intermediate data into your own stack. Mind you, this is often necessary for deep recursions to avoid a stack overflow exception.
@johnlister
@johnlister 4 жыл бұрын
I would say "Most useful recursive functions". However, I would give you two counterexamples: --Tree traversal. You can do it yourself, but you have to maintain a stack yourself with your history. --Ackerman's function. (not that you'll ever need it...)
@arturgasparyan2523
@arturgasparyan2523 4 жыл бұрын
​@@johnlister You can make Ackerman's with a loop and a stack
@johnlister
@johnlister 4 жыл бұрын
@@arturgasparyan2523 You absolutely can. Any time there's recursion, you can turn it into loops with a stack. That means you're doing the work hand building the stack instead of the run time system building stack frames.
@arturgasparyan2523
@arturgasparyan2523 4 жыл бұрын
@@johnlister That is true. My Java implementation was 4 times slower than the native recursion method 😒 I guess sometimes it's better to let the compiler do its thing.
@alixak4304
@alixak4304 3 жыл бұрын
I would really appreciate explaining how to figure out the helping function for tail recursion.
@TheDarkOne629
@TheDarkOne629 2 жыл бұрын
In cases like factorial and fibonacci you always use an "accumulator" as shown in the video. So the first step of thinking is "Which value/calculation can I use an accumulator for?" For some functions you don't need to do anything (like foldl in Haskell), for some it's not worth a re-write (like foldr in Haskell) and for some it is almost impossible (like the Ackermann function explained in another video).
@wnekuu
@wnekuu 4 жыл бұрын
But, for example in C#, if we invoke go inside go then we'll still have all gos on call stack, isn't that right? So we still can easily get stack overflow, so doesn't help that much in the end? At the end of each method instead of using it's result in calling method it would just finish but still we would need to put them all on call stack and then each one must finish in reversed order?
@davidmcgill1000
@davidmcgill1000 4 жыл бұрын
.NET actually supports tail calls, but no C# compiler will ever let you use it. It screws up stack traces.
@miquelfire
@miquelfire 4 жыл бұрын
Node.js supports tail recursion because I had an issue today where I (before watching this) basically converted a tail recursion function into a plain recursion function and got stack errors because I was hitting the limit. For the reason I making the program, I might see about switching to a non-recursive function, as I might need more work than I had attempted to make a tail recursion function.
@jamesh625
@jamesh625 4 жыл бұрын
Miquel Burns Hm, you must be using a version of node below 8, since tail call optimisation was deprecated, iirc.
@miquelfire
@miquelfire 4 жыл бұрын
@@jamesh625 It's version 12. It may not be tco but something else that makes it work like tco
@plase__
@plase__ 3 жыл бұрын
Thanks for sharing~
@beziko
@beziko 3 жыл бұрын
This guy is great
@mjohn414
@mjohn414 4 жыл бұрын
I tried this in Python. The tail recursive version of Factorial had no improvement in performance over the standard recursive version but for Fibonacci, it was a HUGE improvement. Lightening fast.
@KohuGaly
@KohuGaly 4 жыл бұрын
In case of the factorial, the tail recursive version is more memory efficient, especially if compiler optimizes it (basically converting it to for loop). As far as speed is concerned, they should behave the same. Especially in interpreted languages like python, which don't do much of an optimization.
@mikedelhoo
@mikedelhoo 3 жыл бұрын
@K.D.P. Ross "despite not knowing a lot about it" ;-)
@WeirdAlSuperFan
@WeirdAlSuperFan Жыл бұрын
I thought tail recursion was useless in python...?
@ChatGTA345
@ChatGTA345 7 ай бұрын
It's faster only because the algorithm is more efficient than Fib(n-1) + Fib(n), but it still uses regular stack frame recursion in Python, so it is memory-inefficient and will terminate above Fib(1000) (the default limit of recursion call depth in Python)
@danielgysi5729
@danielgysi5729 4 жыл бұрын
I remember reading this early into SICP
4 жыл бұрын
List is the first thing that I remember when reading the title. It's always follow up tail recursion.
@ArunKumar-iy3eo
@ArunKumar-iy3eo 4 жыл бұрын
Isn't the go function for the Fibonacci numbers just a for loop written as a recursive function? That is how we calculate without using a recursive function. int fib(int n) { if (n=0) return 0; for (a=0, b=1; n>0; n--) { temp=a; a=b; b=temp + b; } return b; } Am I wrong? How is recursion better here?
@kunalkheeva
@kunalkheeva Жыл бұрын
Thank you so much,
@NonTwinBrothers
@NonTwinBrothers 2 жыл бұрын
This blew my mind the first time I watched it, Also I thought this came out like 4 years ago, lol
@yan-amar
@yan-amar 3 жыл бұрын
Isn't that tail recursion the same trick also known as memoization - at least in this example ? From what I gather, there is one low-level language thing called "tail call optimization", that is not necessarily related to recursion, but is needed in order to have efficient tail recursion in practice. Tail recursion allows us to leverage TCO to avoid overflowing the stack, but saving the results of previous computations to avoid repeating them is by definition memoization, isn't it ?
@TheDarkOne629
@TheDarkOne629 2 жыл бұрын
Not really... Try it with Clojure for example. If you try explicit tail-recursion via "recur", it will calculate the answer for fibonacci of 1_000_000 pretty fast. Memoization will fail because you still need to create many stack frames. However, you are right in that both are great optimization techniques.:D If you set up a function with memoization and you can save the results so efficiently that you can save the result for fib(1_000_000) and you run it again, you only need to look it up. With tail-recursion, you would have to calculate everything again. Of cause you could combine both though.
@OcteractSG
@OcteractSG 4 жыл бұрын
I wrote some code for figuring out the Fibonacci Sequence: int fib1 = 1; int fib2 = 1; int output = 1 for (int i = 3; i
@gaier19
@gaier19 4 жыл бұрын
can you do this with Ackermann Function ^^ Edit: Ok. Would be nice to see the EXTRA BIT in the EXTRA BIT ^^
@TheDarkOne629
@TheDarkOne629 2 жыл бұрын
I heard that it is possible with some compiler trickery (CPS (Continuation Passing Style)) for functional languages. But I can't wrap my head around how it is tail-recursive at all even after writing multiple languages.
@Yobleck
@Yobleck 4 жыл бұрын
This might explains why my simple up arrow notation program chews through 16 Gb of ram in like 2 min when I plug a large power of 2 into it. It must be trying to keep track of 2, 4, 8 etc until like 2^1000000000000000000000 in ram
@ancientapparition1638
@ancientapparition1638 4 жыл бұрын
whooops
@diverfede
@diverfede 4 жыл бұрын
Is it always possible to convert a recursive algorithm into a tail recursive form ?
@derekeidum1307
@derekeidum1307 4 жыл бұрын
I'm pretty sure it's not. For example, if you're performing recursive operations on a tree, each node can have multiple children and you have to call your function on each child node.
@KohuGaly
@KohuGaly 4 жыл бұрын
Technically yes, because any recursion can be written as a loop and tail recursion is just loop implemented as recursion. I say "technically" because in order to convert general recursion into loop, you need to use stack data structure (basically dynamic array). Which basically means you are manually recreating in higher language what your compiler would compile your function calls into in machine code.
@retropaganda8442
@retropaganda8442 4 жыл бұрын
@@derekeidum1307 For trees, it's the data structure that's recursive. The routine that walks through it doesn't need to grow a stack with recursive calls. For example, if your tree has bidirectional pointers to navigate either to children or to parent nodes, I'm sure you can use the same CPU register to hold the current node address and mutate it as you traverse the tree.
@MrInk0gnit0
@MrInk0gnit0 4 жыл бұрын
@@retropaganda8442 I don't think it's that easy. You would still need to keep track of which child nodes you already traversed through because if you go back to the parent you need to know if you need to go to another child element of that node or back to the parent before.
@nukeeverything1802
@nukeeverything1802 4 жыл бұрын
No, there are some recursive functions which are not bounded (and hence cannot be implemented as a for loop). The most famous example of this is the Ackerman function.
@jgreitemann
@jgreitemann 4 жыл бұрын
The tail-recursive formulation basically does the same thing that an iterative solution would do. However, IIRC there are problem which are either only computable by recursion or for which only recursive solutions are known. Are there any cases where one of those can benefit from tail recursion or (as I suspect) is it the case that once you can rewrite it as a tail recursion, you might as well unroll the recursion into a loop and end up with an iterative solution, hence precluding any of those recursion-only problem from tail recursion?
@KohuGaly
@KohuGaly 4 жыл бұрын
Recursion and loops are equivalent. Any loop can be written as recursion and any recursion can be written as a loop. In fact, computers actually can't do true recursion. They implement it as a loop at hardware level. Converting loops to recursion is simple. You just create a function that performs the loop body and if condition is met, calls itself. It passes all the variables that persist between loop iterations as arguments and return values. Converting recursion to loops is a bit more elaborate. You need a stack data structure. You can push and pop values and you can access/modify values by their position relative to the top of the stack. You can then implement the recursion as a while loop with a giant switch statement inside it. The switch statement switches between different "function bodies". The stack holds all the local variables, including arguments and return values. It also holds identifiers of functions (ie, which function is called and where should it return), that the switch statement uses to pick the right code each loop.
@jgreitemann
@jgreitemann 4 жыл бұрын
@@KohuGaly I'm not sure your statement holds water. There is apparently thus a thing as non-primitive recursive functions, with the Ackermann function as an infamous example. On the other hand, since we can compute the Ackermann function on real computers, you are right that there is evidently a iterative way to do so. I suspect that that would require rapidly growing stacks, though, and that it thus is not considered an "iterative" solution?
@qwertz12345654321
@qwertz12345654321 4 жыл бұрын
@@jgreitemann you missed the existence of while loops. looping functions only allow primitive for loops. But it is quite trivial to implement the ackerman function using while loops
@KohuGaly
@KohuGaly 4 жыл бұрын
@@jgreitemann the general conversion of recursion into loops that I outlined is more-or-less how function calls are implemented in machine code. My solution has some extra fluff, to implement "call" and "return" opcodes using while loop and switch statement. You are correct in that this kind of pushes the definition of iteration by having a dynamic data structure to a implement stack.
@Atlantis357
@Atlantis357 4 жыл бұрын
@@jgreitemann loops that can only loop a value that has been defined before the loop was started (like for loops) are equivalent to primitive recursive functions. But while loops that can modify their termination condition while looping are just as powerful as the most complicated recursion. Like @KohuGaly said, for computers recursion and loops are equivalent and at a hardware level all loops and recursions are implemented as "go to line x" statements.
@itskdog
@itskdog 4 жыл бұрын
Can someone explain how using tail recursion is better than a for loop? Surely you can just have the accumulator outside the scope of the loop, and just run through from the starting case until the base case?
@TheDarkOne629
@TheDarkOne629 2 жыл бұрын
Hi. In case you didnt find it: Some functional languages don't support loops (or implement them with tail recursion). This reduces the number of native expressions you have to implement in your compiler/interpreter in case you want that number low. So instead of having "if" and "while", the "if" keyword will be enough. Then there's a compiler-technique for functional languages called continuation-passing-style (CPS) which can be easily used for tail-recursion but does not lend itself to loops well. The opposite is SSA-form for imperative languages, which plays well for loops but simply sub-optimal for functional languages. :)
@TheDarkOne629
@TheDarkOne629 2 жыл бұрын
Almost forgot: Some languages don't allow re-definition of variables to avoid doing many lookups. That's why you don't put an accumulator outside a loop in scheme or haskell. Thanks for the constructive question. Most others just said that loops are better without giving it any thought. (You clearly did by thinking of the accumulator and stuff) :D
@ori61511
@ori61511 4 жыл бұрын
Please do recursive descend for making a calculator
@edoardottt
@edoardottt 4 жыл бұрын
Yesss
@ruixue6955
@ruixue6955 3 жыл бұрын
3:09 problem with the simple recursion 3:58 potentially inefficient in terms of memory use 4:26 tail recursion
@TericT
@TericT 4 жыл бұрын
very cool
@everluck35
@everluck35 4 жыл бұрын
So it's like the dynamic programming approach but still recursive?
@TheHuesSciTech
@TheHuesSciTech 4 жыл бұрын
Tail recursion can trivially be transformed into an iterative representation. I find it hard to describe *any* implementation of Fibonacci as DP, but yeah I guess sorta.
@Prjanik122
@Prjanik122 4 жыл бұрын
Each problem that could be solved with tail-recursion can be solved with a loop.
@qandak
@qandak 4 жыл бұрын
... and vise versa.
@natmath2576
@natmath2576 4 жыл бұрын
This is really useful when writing GPU code, which doesn't allow recursion.
@AntOfThy
@AntOfThy 4 жыл бұрын
Chris Warburton I don't see it as "easier to understand" I see loops as a lot easier to understand. Whether the loop is implemented as a "re-start block" until some condition, or just "do block" N times. The only time I find real recursion actually useful in practical programming, is when you need to do double recursion, such as when dealing with a tree like structure. That is doing a full traversal, or rebalancing, not just a simple search down to the leaf. Only then do I find recursion in any way simpler. If it is a loop, not requiring a stack (procedural, or data stack), it is already equivalent in efficiency terms to tail recursion.
@jeffspaulding9834
@jeffspaulding9834 4 жыл бұрын
@@AntOfThy Some problems are easier to understand as loops, some are easier to understand as recursion. If it has a recursive definition mathematically, it's likely easier understood recursively. Spend long enough writing nothing but Scheme code (without the named let construct) and you start to see recursion as easier to understand. Prof. Hutton didn't show any, but in many cases you don't need the helper function and the recursive calls are really straightforward.
@dandan7884
@dandan7884 4 жыл бұрын
the more you know while learning about erlang, i got to see this technique of carrying information about the value to be returned in the arguments of an inner function. and it was called "currying", not tail recursion tail recursion i thought of as when the called function is being called again on its last expression, but for that to work correctly, and to not explode the stack, the compiler/interpreter for that language would have to have implemented tail call optimization
@Dizerfullpower
@Dizerfullpower 4 жыл бұрын
there's video on currying on this cannel too IIRC.
@ccgarciab
@ccgarciab 4 жыл бұрын
Currying is converting your multi-parameter function into a kind of "chain" of single parameter functions. It makes partial evaluation more ergonomic, and types more consistent.
@nang88
@nang88 2 жыл бұрын
🔥 🔥
@swagatochatterjee7104
@swagatochatterjee7104 4 жыл бұрын
Can all overlapping subproblems be converted to a tail recursion?
@abhinavchauhangujjar6456
@abhinavchauhangujjar6456 4 жыл бұрын
Can somebody explain Why should we use recursion if everything we can do with recursion can aso be done with loops more efficiently?
@1990Hefesto1990
@1990Hefesto1990 4 жыл бұрын
You can't do everything recursion does with just loops. i.e. Loops can be written in a recursive form, but not all recursive forms have a loop form.
@bhaskarbhasku2921
@bhaskarbhasku2921 4 жыл бұрын
Not everything can be computed using for loops for example you want to compute all the sets that are of length k. As far as i know it will be very hard using loops but its easy using backtracking.
@nukeeverything1802
@nukeeverything1802 4 жыл бұрын
Recursion is also more readable and easier to analyse than loops. The time complexity of quicksort for example can almost immediately be seen from the structure of the program.
@timharig
@timharig 4 жыл бұрын
@Xeridanus 1. Some functions are much simpler in recursive form because you are letting the stack do a lot of the tracking work for you for free. This is almost always true when doing depth first searches such as solving mazes. 2. Iteration is not possible in purely functional languages because you cannot set a variable more than once. Thus making loop counter variables impossible. Recursion is your only option. 3. Having a variable that cannot change once it has been defined is quite efficient when you are writing concurrent code. You can pass data by reference (which is faster and saves memory) without worrying about race conditions. You don't need locks or semaphores because the variable can never be changed. Its completely thread safe by design. 4. But this is only an advantage if you are not overflowing your stack with recursive stack frames. That is why tail call optimization is essential for purely function languages.
@abhinavchauhangujjar6456
@abhinavchauhangujjar6456 4 жыл бұрын
@@1990Hefesto1990 so there are programs which can not be written with loops in any way , but they can we written easily with recursion?
@spicybaguette7706
@spicybaguette7706 4 жыл бұрын
So you can also just run it in a for loop?
@TheEulerID
@TheEulerID 4 жыл бұрын
There's an error in that function right at the start. That's because the end condition should be 0 as factorial 0 is 1. It may seem counter-factual, but it's part of the definition, and the reason that's the case is because it's to do with the way a number of things can be arranged in a set. An empty set has 1 way of arranging it, as does a set with a single item. A function to return 0! should return 1.
@pqul2828
@pqul2828 4 жыл бұрын
You're totally right, the function could be expanded to 0! = 1, but it still works and gives the right results. I think Prof. Hutton wanted to simplify the example, and avoid confusion by avoiding this counter-intuitive case. Have a nice day!
@TheEulerID
@TheEulerID 4 жыл бұрын
@@pqul2828 It doesn't give the right result as it doesn't work for 0!. I realise it doesn't affect the rail recursion issue, but details matter.
@cheaterman49
@cheaterman49 4 жыл бұрын
It's funny how passing the starting points to the "go" function reminds me of lambda calculus. Like, THREE(increment)(1) = 3, etc :-)
@Alche_mist
@Alche_mist 4 жыл бұрын
If I'm not mistaken, it's an interrelated concept by itself.
@lixiaolong800
@lixiaolong800 3 жыл бұрын
How to decide fib will be use a pair, the factorial only use one number?
@TheDarkOne629
@TheDarkOne629 2 жыл бұрын
Because fib needs to remember two computed values and factorial only has to remember one.
@SwordQuake2
@SwordQuake2 4 жыл бұрын
The comments are much better than the actual video. If you keep making stack frames there'll be barely any difference.
@RobinHagg
@RobinHagg 4 жыл бұрын
I was jsut going to ask about some Python example but I see there is no support. Anyway. Great video and love to see it. Please let covid end and we can get back to magic markers and paper.
@Number_055
@Number_055 4 жыл бұрын
Python doesn't support it, no, but there is a way, so I'll shamelessly copy-paste a comment I made. Since, by definition, tail recursive functions make the recursive call last, you can convert them to a while loop with minimal changes. All you generally need to do is define the parameters before the loop, then replace the recursive call with code that edits the parameters. The following is a demonstration in python, which does not support TCO: #Factorial n = 4 a = 1 while (n > 0): a = a*n n -= 1 print(a) #Fibonacci n = 4 a = 0 b = 1 while (n > 0): temp = a a = b b = temp + b n -= 1 print(a)
@NeelSandellISAWESOME
@NeelSandellISAWESOME 3 жыл бұрын
Hey what tablet is he using to make this video
@macnolds4145
@macnolds4145 3 жыл бұрын
One humble suggestion for improvement: a more consistent use of parentheses and commas would be helpful. Example: "fib 4" becomes "fib(4)" and "go n a" becomes "go(n,a)". Python code follows below: def fibber(n): #uses recursion #instructive, but sloooooow and inefficient if (n==1) or (n==2): return 1 elif n>2: return fibber(n-1) + fibber(n-2) def fibbest(n): #uses tail recursion return go(n,0,1) def go(n,a,b): #for use with fibbest() if n==0: return a elif n==1: return b elif n>=2: return go(n-1,b,a+b)
@TheDarkOne629
@TheDarkOne629 2 жыл бұрын
Hi. It's cool that you grasped the concept! The language being used in the video is Haskell and the thing with haskell is that brackets confuse it. In general it is better style not to use too many brackets.
@diego1694
@diego1694 4 жыл бұрын
I have tried a few compilers in godbolt.org, and most are able to transform a recursive function into iterative, regardless of it is tail recursion or not. I would say, trust your compiler and write the function in the manner that is easier to read. Otherwise you are just making your code harder to maintain for no gain whatsoever.
@Donmegamuffin
@Donmegamuffin 4 жыл бұрын
*Cries in interpreted languages*
@framegrace1
@framegrace1 4 жыл бұрын
First, only tail call recursion is easily optimizable to iterable by a compiler. So all the examples you tried, for sure had a tail call recursive equivalent. The tail call optimization is simply a loop with whatever is inside the function. Very easy. But detection is complex so not all languages have than optimization, java, go, C#, python and ruby don't have it, for example. You will have stack overflow. So better only use recursion on functional languages. In all others, much better iterate.
@diego1694
@diego1694 4 жыл бұрын
​@@framegrace1 if the goal is to prevent stack overflow because you expect the recursion depth to be very high, absolutely, you should make your function iterative yourself instead of trusting the compiler. But otherwise, if you care enough about performance to be doing micro-optimizations like this, you should probably be using a different language. This seems to be implemented in LLVM, so I would expect any compiler that uses LLVM as the backend to support it.
@Respectable_Username
@Respectable_Username 5 ай бұрын
It looks like you're basically just turning the recursion into a for loop, by adding an iterator to count up from the base case rather than "normal" recursion to drill down into it. Which makes me think I should just use for loops instead of recursion wherever possible (unless threading is an option) 😛
@Babs42
@Babs42 Жыл бұрын
The one thing this seems to be missing is how they use tail recursion to optimize the call stack though.
@cambrown5633
@cambrown5633 4 жыл бұрын
Can you give an example that couldn't be written just as efficiently with a for loop & an accumulator?
@cambrown5633
@cambrown5633 4 жыл бұрын
@@chriswarburton4296 Thanks, as long as I know I've not been missing something this whole time :P Every language I use has loops.
What is a Monad? - Computerphile
21:50
Computerphile
Рет қаралды 588 М.
Lambda Calculus - Computerphile
12:40
Computerphile
Рет қаралды 996 М.
原来小女孩在求救#海贼王  #路飞
00:32
路飞与唐舞桐
Рет қаралды 46 МЛН
Who enjoyed seeing the solar eclipse
00:13
Zach King
Рет қаралды 115 МЛН
маленький брат прыгает в бассейн
00:15
GL Show Russian
Рет қаралды 3,1 МЛН
TLS Handshake Explained - Computerphile
16:59
Computerphile
Рет қаралды 539 М.
Has Generative AI Already Peaked? - Computerphile
12:48
Computerphile
Рет қаралды 248 М.
5 Simple Steps for Solving Any Recursive Problem
21:03
Reducible
Рет қаралды 1,1 МЛН
The Feigenbaum Constant (4.669)  - Numberphile
18:55
Numberphile
Рет қаралды 1,5 МЛН
3D Gaussian Splatting! - Computerphile
17:40
Computerphile
Рет қаралды 96 М.
Laziness in Python - Computerphile
14:21
Computerphile
Рет қаралды 362 М.
Coding a Web Server in 25 Lines - Computerphile
17:49
Computerphile
Рет қаралды 314 М.
Why this puzzle is impossible
19:37
3Blue1Brown
Рет қаралды 3,1 МЛН
Corel Linux - The (Word)Perfect Operating System
25:40
Michael MJD
Рет қаралды 78 М.
原来小女孩在求救#海贼王  #路飞
00:32
路飞与唐舞桐
Рет қаралды 46 МЛН