Тёмный

Tail Recursion Explained - Computerphile 

Computerphile
Подписаться 2,4 млн
Просмотров 173 тыс.
50% 1

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/nottsco...
Computerphile is a sister project to Brady Haran's Numberphile. More at www.bradyharan.com

Опубликовано:

 

28 сен 2024

Поделиться:

Ссылка:

Скачать:

Готовим ссылку...

Добавить в:

Мой плейлист
Посмотреть позже
Комментарии : 540   
@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
@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.
@DaChrisstar
@DaChrisstar 4 года назад
They could also enable community Translation
@HazhMcMoor
@HazhMcMoor 4 года назад
Yeah I'm pretty sure some ESL also will appreciate English sub especially complicated topics like this.
@haskellhutt
@haskellhutt 4 года назад
We are in the process of putting the subtitles together.
@krishna9438
@krishna9438 4 года назад
Graham Hutton Great😃
@haskellhutt
@haskellhutt 4 года назад
Subtitles now up!
@frognik79
@frognik79 4 года назад
No stack frames were harmed in the making of this video.
@pedrohenriquedadaltdequeir4859
@pedrohenriquedadaltdequeir4859 3 года назад
lolz
@lycan2494
@lycan2494 Год назад
cringe
@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.
@thomasdtrain
@thomasdtrain 4 года назад
This was actually pretty interesting. Thanks.
@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 4 года назад
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
@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
@DrGreenGiant
@DrGreenGiant 4 года назад
This is very elegant
@danser_theplayer01
@danser_theplayer01 15 дней назад
It still needs a whole chain of functions before it can collapse to the answer. People are laso saying it's compiler dependant so it doesn't work the same way in all languages like simple math would (a for loop with outer variables n and accumulator will work the same in every language and won't create a chain of calls that eat through your stack). You need to separate your functions by an "air gap" just in programming context, the functions must return after one iteration to break the chain and not create a call MONOLITH on the stack. I'm cooking something up in my head I just haven't realised it yet, maybe if I start coding I will know if my idea works or not, but I'm currently busy with another very juicy idea for a custom data structure that I'm actually coding rn.
@snensnurnberg8891
@snensnurnberg8891 4 года назад
Thank you Mr.Graham ! Really enjoyed this Lesson.
@eyemotif
@eyemotif 4 года назад
my time with f# has made me write all my recursive functions with the fac/go format
@Respectable_Username
@Respectable_Username 10 месяцев назад
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) 😛
@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 4 года назад
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.
@robertbrummayer4908
@robertbrummayer4908 3 года назад
Your videos are excellent
4 года назад
List is the first thing that I remember when reading the title. It's always follow up tail recursion.
@pedrohenriquedadaltdequeir4859
@pedrohenriquedadaltdequeir4859 3 года назад
Thank you so much!!! I've been learning Haskell and this is super useful!!!! Loved to learn it :D
@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.
@georgichelenkov4360
@georgichelenkov4360 4 года назад
Thanks for the great content!
@yan-amar
@yan-amar 3 года назад
Thanks very much this was just what I was looking for.
@emm7494
@emm7494 2 года назад
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.
@oktavarium
@oktavarium 4 года назад
Pretty satisfactorial!
@kunalkheeva
@kunalkheeva 2 года назад
Thank you so much,
@OLApplin
@OLApplin 4 года назад
In other word, its a way to simulate a for loop in language that dont support it :P !
@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.
@Seltyk
@Seltyk 4 года назад
No mention of Lua in the comments yet? I'm disappointed, because Lua's tail call optimization applies not only to recursive functions but to any function whose return statement is just a function call
@lixiaolong800
@lixiaolong800 3 года назад
How to decide fib will be use a pair, the factorial only use one number?
@TheDarkOne629
@TheDarkOne629 3 года назад
Because fib needs to remember two computed values and factorial only has to remember one.
@ori61511
@ori61511 4 года назад
Please do recursive descend for making a calculator
@rishiraje
@rishiraje 4 года назад
So you are just developing an iterative formula and calling it tail recursion. Can this method be applied to say the Ackerman function
@Xeridanus
@Xeridanus 4 года назад
Both examples really are just iterative functions which are imo much easier for programmers to understand and don't require the compiler/interpreter to know about these and fiddle with the call stack to actually get the improvements. I'm failing to see the benefit other than to show that some recursive functions are better as iterative ones.
@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.
@joestevenson5568
@joestevenson5568 4 года назад
@@timharig all your points are just that functional languages have a massive performance flaw and this is the workaround. Is there any reason to use tail recursion over a loop in a language that supports both?
@daddy3118
@daddy3118 4 года назад
So it is very niche. A little less niche is people knowing if they write their recursion in a particular way then compilers/interpreters *might* do this optimisation for them behind the scenes.
@timharig
@timharig 4 года назад
@@joestevenson5568 I had four points. Exactly one of them (number 4) deals with a performance hit using recursion -- which is fixed using tail call optimization. Point 1 explains that recursion is often much simpler to write and cleaner to read than the equivalent iterative form. Point 3 explains that immutable variables (which requires recursion) is both safer than iteration and potentially faster than iteration when writing concurrent (ie multiprocessing) because it requires no locking when passing variables by reference, and never requires passing variables be value for thread safety reasons. There are other advantages that I did not address, such as ease debugging added by having stack a stack traffic trace of where a bug occurred. You may call that point 5.
@bertblankenstein3738
@bertblankenstein3738 4 года назад
I think I'll just program in a way that avoids recursion when possible. It is easy enough to program Fibonacci or factorial with a for loop. No need to push things on the stack, popping the stack, pointers, return values.
@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.
@Noor_alden
@Noor_alden 2 года назад
thank u 🤩🤩
@formbi
@formbi 4 года назад
nobody mentioning Scheme in the comments :(
@swagatochatterjee7104
@swagatochatterjee7104 4 года назад
Can all overlapping subproblems be converted to a tail recursion?
@Arnatious
@Arnatious 4 года назад
Nitpicking but shouldn't you have terminated at fac 0?
@rogerbosman2126
@rogerbosman2126 4 года назад
You could, but why add extra steps?
@Vinxian1
@Vinxian1 4 года назад
@@rogerbosman2126 because 0 should be a valid input to a factorial function. Fac(0) will underlow with a implementation that uses 1 as it's terminating case.
@TheEulerID
@TheEulerID 4 года назад
I'm glad somebody else picked that up as it's an error.
@alejandrolim8615
@alejandrolim8615 4 года назад
Where was this when I took 61A
@kanavmehra8022
@kanavmehra8022 4 года назад
too real, denero destroyed me
@EvanGaoTV
@EvanGaoTV 4 года назад
Ayyy 61A gang
@casperes0912
@casperes0912 4 года назад
You have so funny course names. We just called the course "Programming Languages". Hehe :P
@b4ttlemast0r
@b4ttlemast0r 2 года назад
couldn't you just rewrite these tail recursive functions as a for loop? They seem to have essentially the same structure. int fib (int n){ int a = 0; int b = 1; for (int i = n, n > 0, n--){ int temp = a; a = b; b = temp + b; } return a; }
@AndreiGeorgescu-j9p
@AndreiGeorgescu-j9p 7 месяцев назад
This is haskell
@i_sometimes_leave_comments
@i_sometimes_leave_comments 4 года назад
Can every recursive function be made tail recursive? If yes, is there a definitive method for it?
@thomasstambaugh5181
@thomasstambaugh5181 4 года назад
See Scheme 1975
@samsapiel
@samsapiel 4 года назад
Nice to see some Haskelllovers in the wild ;)
@GarethField
@GarethField 4 года назад
Paper, please?
@RealCadde
@RealCadde 4 года назад
But before doing any of this, you should have explained that IF you can do something without using recursion you should always do that instead. function fac(n) { int result = 1; for (i = 2; i
@Broan13
@Broan13 4 года назад
When is recursion better to use (besides it just looking nice and feeling great)? The only thing I have used that I think recursion is superior to a loop would be efficient searching or sorting algorithms.
@RealCadde
@RealCadde 4 года назад
Also worth noting is that the largest factorial you can fit in a 32 bit number is 12! and as such you'd be better off just using a lookup table. as such, any requested N in the range 1 to 12 can return a value from the lookup table. For bigger factorials you can also keep values in a table for up to 64 bits or even 128 bits. Don't know off hand what the limits are. For even bigger factorials than what exists in the lookup table you can simply take the max of the lookup table and work onwards from there saving you some cycles. The same can be done for the fibonacci sequence.
@diego1694
@diego1694 4 года назад
Compilers are smart enough to turn recursion into iteration, regardless of if it is a tail call not. So just write code in whatever form is easier to understand.
@Leon-ol6oe
@Leon-ol6oe 4 года назад
If the compiler of the language of your choice can do tail recursion optimization, I don't think that the recursion is more expensive. Only one return address would need to be stored. There is no stack then.
@EdouardCOLE
@EdouardCOLE 4 года назад
No algorithm is more efficient when wrote in its recursive form. This is just cosmetic.
@Uerdue
@Uerdue 3 года назад
So, all you really do is compute go^n, and then you realize that go is a linear map in a 2-dimensional, real vectorspace that can be diagonalized in R, and this way you arrive at the closed-form expression. :)
@retropaganda8442
@retropaganda8442 4 года назад
So, eventually you ran out of paper?
@silaspoulson9935
@silaspoulson9935 4 года назад
It's probably just not in their houses
@ducc5511
@ducc5511 4 года назад
for loops are cool too..... and it would have a fixed memory footprint in this case
@timharig
@timharig 4 года назад
For loops don't work in purely functional languages. You cannot have a loop counter when you cannot define a variable more than once.
@blackAngel88it
@blackAngel88it 4 года назад
Too bad you get nothing from tail recursion when the language doesn't optimize for it. Yes, I'm talking to you PHP! I don't even know why they don't implement it... shouldn't be too hard and would make some tasks so much easier...
@TheAguydude
@TheAguydude 4 года назад
Don't be so sure it's trivial. In general, languages are forbidden from performing an optimization if that optimization will become visible to the developer (ignoring cases where developers bypass safety systems and ignoring performance). Since php supports exceptions (and emits stack traces after an exception), support for a tail call optimization is more difficult. Further, it's usually not hard to convert code which can be tail-call optimized into a simple loop.
@ProjectPhysX
@ProjectPhysX 4 года назад
What's even more efficient is to not use recursion at all whenever possible.
@ccgarciab
@ccgarciab 4 года назад
Sometimes recursive definitions are much easier to use and reason about. And tail recursion is normally optimized in many modern compilers, making it as practically as efficient as an iterative solution.
@szymonwysocki1110
@szymonwysocki1110 4 года назад
@@ccgarciab especially in tree navigation recursion just makes so much sense
@123FireSnake
@123FireSnake 4 года назад
that's always:D
@DancingRain
@DancingRain 4 года назад
Today I learned that decrement is pronounced differently on the other side of the pond.
@cidercreekranch
@cidercreekranch 4 года назад
I realize that it's just an example, but factorial can be computed with a simple loops. Thus avoiding the overhead of making n-1 function calls. package main; use strict; sub factorial { my ( $n ) = @_; return 1 if ( $n == 1 ); my $factorial = 1; for ( my $i = $n; $i > 1; --$i ) { $factorial += $factorial * ($i - 1); } return $factorial; } for ( my $i = 10; $i > 1; --$i) { print "factorial($i) = ", factorial( $i ), " "; }
@AlwinMao
@AlwinMao 4 года назад
ah, a loop
@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"
@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 4 года назад
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 6 месяцев назад
Tip: you don't need a temp variable, you can do a, b = b, a+b
@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.
@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.
@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. 😄😄😄
@leoncruz9757
@leoncruz9757 4 года назад
CPython: I have no idea of what you are talking about...
@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 4 года назад
I'm a professional developer and their videos are absolutely great for me too
@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.
@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.
@tinashine6544
@tinashine6544 Месяц назад
beginner here, so in this case by using tail recursion in fibonacci do the space complexity become o(1)?or it stays same i didnt really understood...
@agmessier
@agmessier Месяц назад
@@tinashine6544 Yes, if tail recursion is used as described, it's constant space efficiency (3 integers, a, b and n). If you consider that larger numbers may require more storage, it coud be O(log n), I suppose.
@NonTwinBrothers
@NonTwinBrothers 3 года назад
This blew my mind the first time I watched it, Also I thought this came out like 4 years ago, lol
@bradH2049
@bradH2049 6 месяцев назад
The explanation was very clear and easy to understand. Thank you for making this world a better place.
@retropaganda8442
@retropaganda8442 4 года назад
This is why teaching assembler still makes sense. This kind of problem is better explained at machine level.
@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.
@hallenb328
@hallenb328 4 года назад
This is a terrible video. You spend most of the video explaining several recursive algorithms without connecting anything to the relevance of tail recursion. You have taken simple concepts and managed to complicate things without connecting the dots to what tail recursion is. You have not explained Tail Recursion, which is the title of the video.
@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 3 года назад
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.
@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?
@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.
@Semtx552
@Semtx552 4 года назад
I keep finding great litle tools here that might help me write better scripts. Thanks!
@tifahefendijagaming9606
@tifahefendijagaming9606 4 года назад
Same
@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.
@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
@Kepler57
@Kepler57 2 года назад
I cant discribe how your lesson is helpful. Thank you so much. Where is the button for 1000000000000000000000000000000000000000 likes?
@JNCressey
@JNCressey 4 года назад
Isn't this just an iterative loop with extra steps?
@thirdeyeblind6369
@thirdeyeblind6369 11 месяцев назад
cringe.
@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 3 года назад
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.
@SwordQuake2
@SwordQuake2 4 года назад
The comments are much better than the actual video. If you keep making stack frames there'll be barely any difference.
@Lovuschka
@Lovuschka 4 года назад
You forgot to define that fac 0 = 1 (This is not needed for the programming, but for mathematical accuracy. Otherwise fac 0 would not be defined and wrongly output an error)
@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.
@nang88
@nang88 3 года назад
🔥 🔥
@ЯрославФ-ы7ж
@ЯрославФ-ы7ж 4 года назад
That just sounds like a for loop with an extra step
@boxedowl
@boxedowl 4 года назад
😉 Drop them both in compiler explorer (godbolt.org). Set optimizations to -Os for gcc. Check assembly output. By the time you strip out debugging data and the compiler fully optimizes everything you will have difficulty seeing meaningful differences.
@EvilSapphireR
@EvilSapphireR 4 года назад
Yeah, if a new stack frame doesn't get established with each function call (which it isn't and is exactly how tail recursion saves memory compared to normal recursion) this is just a fancy way to write a simple for loop. I'm genuinely curious if this has any practical benefits or is just dabbed into because of the intellectual stimulation.
@HenryTitor
@HenryTitor 4 года назад
Is it me skipping classes of Computational Theories, or is it my university course always assumes infinite memories..?
@murrayhorn8817
@murrayhorn8817 4 года назад
So tail recursion is no better than a loop. got it.
@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 3 года назад
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 3 года назад
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
@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
@christopherg2347
@christopherg2347 4 года назад
Factorial and Fibonacci are poor examples. I kept thinking "just use a dang loop!" It has the same memory saving, while also saving you all those funciton calls. Those two are trivially easy to de-recurse.
@qzbnyv
@qzbnyv 4 года назад
Christopher G Hi! Professor Hutton teaches (and researches) the style of programming known as Functional Programming, with his videos using the Haskell programming language to demonstrate the principles of Functional Programming (FP). There are plenty of other videos out there on the interwebs showing you how to do traditional Imperative Programming loops. But as these videos by Professor Hutton are still on the basics of FP, you would only really include a loop if your goal was to contrast the FP style of programming with that of traditional Imperative Programming. But they can’t do that in every single FP video that they make. Or, at least, I hope they don’t. At some point you have to start assuming interested people have watched the earlier ones in order to make progress :)
@christopherg2347
@christopherg2347 4 года назад
@Mike If they have funtions, they have jumps. If they got jumps, you can make a loop. But I simply did not know this was looking at it from a pure funtional language viewpoint.
@TheHuesSciTech
@TheHuesSciTech 4 года назад
Yo, if you're going to make such suggestions, how about suggesting specific examples that meet your requirements? By taking that extra step, you might think it through and realise that any such non-trivially-de-recursable algorithm would be so complicated to explain that the actual topic at hand (tail recursion, remember) would be completely overshadowed.
@Emily-fm7pt
@Emily-fm7pt 4 месяца назад
Not my Oxford interviewer telling me to remove my "unnecessary" accumulator variable in my Maths and CS interview...
@sogerc1
@sogerc1 4 года назад
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)).
@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.
@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.
@Babs42
@Babs42 2 года назад
The one thing this seems to be missing is how they use tail recursion to optimize the call stack though.
@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. :)
@ruixue6955
@ruixue6955 3 года назад
3:09 problem with the simple recursion 3:58 potentially inefficient in terms of memory use 4:26 tail recursion
@Sunomis
@Sunomis 4 года назад
Ok, but how does the system know to not keep track of the stack ?
@TheDarkOne629
@TheDarkOne629 3 года назад
How most interpreters I saw do it: Step 1: Set up a small stack where you save the ids of called functions. Native stuff like "if" does not go on the stack, but operators +, -, *, /, &, ... do. Step 2: When you call a function as a parameter to the 2nd function on the stack, you can not tail-recurse. (Think of Ackermann). In that case, put it on the stack. Step 3: When you call a function and its id is not equal to the last one on the stack, put the new function on the stack and do not tail-recurse. Step 4: When you call a function and its id is equal to the last one on the stack, you can tail-recurse. Step 5: Evaluate the parameters of the call and tell them that they are the parameters to some other call and save their results somewhere. Step 6: Now go back to the beginning of the last call. This can be done via a jump or exception throw. Step 7: Done. Notice that, as explained in the video, the stack is not expanded if a tail-call is done.
@may007ank
@may007ank 4 года назад
This is what happens when you do more Computer Science/Programming than anything else. You write a '*' for multiplication rather than a 'x' even when writing.
@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.
@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.
@Teneban
@Teneban 4 года назад
I'll only do recursion, if it's the last thing I do!
@InspektorDreyfus
@InspektorDreyfus 2 года назад
If I found a tail recursion for my computation, then I can implement it directly in a while loop. Then it is no recursion any more.
@rationalityfirst
@rationalityfirst 4 года назад
And thus, using that tablet, the supply of matrix printer paper will be infinite.
@beziko
@beziko 4 года назад
This guy is great
@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)
@venil82
@venil82 4 года назад
My favourite Haskell professor 👨‍🏫
@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 3 года назад
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
@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 3 года назад
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.
@danielgysi5729
@danielgysi5729 4 года назад
I remember reading this early into SICP
Далее
Programming Loops vs Recursion - Computerphile
12:32
Просмотров 1,5 млн
Lambda Calculus - Computerphile
12:40
Просмотров 1 млн
Witch changes monster hair color 👻🤣 #shorts
00:51
"Когти льва" Анатолий МАЛЕЦ
53:01
5 Simple Steps for Solving Any Recursive Problem
21:03
Pi-oneers (interview with Sinha & Saha) - Numberphile
24:51
TLS Handshake Explained - Computerphile
16:59
Просмотров 559 тыс.
What on Earth is Recursion? - Computerphile
9:40
Просмотров 744 тыс.
This Algorithm is 1,606,240% FASTER
13:31
Просмотров 831 тыс.
Recursion 'Super Power' (in Python) - Computerphile
12:18
Tail Call Optimization
8:30
Просмотров 8 тыс.
TCP Meltdown - Computerphile
14:52
Просмотров 220 тыс.
How AI 'Understands' Images (CLIP) - Computerphile
18:05
Witch changes monster hair color 👻🤣 #shorts
00:51