ANA LUIZA BASTOS
github.com/anabastos
@naluhh
@anapbastos
Software Developer na Quanto e
cientista da computação da
PUC-SP
anabastos.me
JSLADIES
fb.com/jsladiesbr
twitter.com/jsladiessp
meetup.com/JsLadies-BR/
LAMBDA.IO
t.me/lambdastudygroup
github.com/lambda-study-group/
meetup.com/Lambda-I-O-Sampa-
Meetup/
VISÃO GERAL SOBRE
RECURSÃO &
TRAMPOLINES
VAMOS FALAR SOBRE
RECURSÃO
Recursão é quando uma função
chama a si mesma até uma condição parar o
loop.
λ Programação Funcional
Fatorial
1! = 1
3! = 1 * 2 * 3 = 6
1. Qual parte do código é
recursiva
2. Condição de saída
const fac = (n) => {
if (n == 0) {
return 1
}
return n * fac(n - 1)
}
● EXPRESSIVO
● PURO / EVITANDO MUDANÇA DE
ESTADO(CONTADOR)
● IMUTABILIDADE DE DADOS
● DECLARATIVO
● IDEMPOTENCIA
JS é single threaded
e orientado a stack
GC
fac(3)
GC
fac(2)
fac(3)
GC
fac(1)
fac(2)
fac(3)
GC
fac(0)
fac(1)
fac(2)
fac(3)
GC
GC
fac(3)
GC
fac(2)
fac(3)
GC
fac(1)
fac(2)
fac(3)
GC
fac(0)
fac(1)
fac(2)
fac(3)
GC
1
1* 1 = 1
1 * 2 = 2
2 * 3 = 6
GC
fac(9999)
GC
fac(9998)
fac(9999)
GC
fac(9997)
fac(9998)
fac(9999)
GC
oh no
...
fac(9998)
fac(9999)
GC
StackOverFlow :(
Cruza os dedos?
Senta e chora?
TAIL CALL OPTIMIZATION
(TCO)
Truque antigo que muitas
linguagens funcionais fazem
GC
fac(3)
GC
fac(2)
fac(3)
GC
fac(1)
fac(2)
fac(3)
GC
fac(0)
fac(1)
fac(2)
fac(3)
GC
TCO faz com que a gente evite
explodir o stack quando
fazemos chamadas recursivas
PROPER TAIL CALLS
(PTC)
Possibilita que sua função
recursiva seja otimizada pela
engine.
RECURSÃO EM CAUDA
(TAIL CALL)
//Não é tail call :(
const fac = (n) => n == 0
? 1
: n * fac(n - 1)
A última coisa a ser feita na função
é o retorno da própria função.
Acumulador
const tailRecursiveFac = (n, acc = 1) => {
return n == 0
? acc
: tailRecursiveFac(n - 1, acc +
n)
}
ES6
http://www.ecma-international.org/ecma-262/6.0/#sec-tail-position-
calls
Além disso existem casos de
algoritmos que precisam de mais
de duas chamadas
recursivas(multiple recursion) e não
tem como colocar tudo no final
COMO LIDAR COM ISSO?
CONTINUATION PASSING
STYLE (CPS)
Um estilo de programação em que
o controle é passado explicitamente
em forma de continuação
Continuação não chama a si mesma, ela só expressa
um fluxo de computação onde os resultados fluem em
uma direção.
Aplicar continuações não é chamar funções é passar
o controle do resultado.
Continuação é uma parte de
código que ainda vai ser executada
em algum ponto do programa
Callbacks por exemplo são continuations
LAZY EVALUATION
CALL-BY-NEED
> (1 + 3) * 2
8
> const expression = () => (1 + 3) * 2
> expression()
8
Ou seja, o programa espera receber os
dados antes de continuar.
E daí?
CPS elimina a necessidade de um
call stack pois os valores estarão
dentro da continuation sendo
executada .
Acumulador para uma
continuação
IDENTITY FUNCTION
const id = x => x
● O último parâmetro da função é sempre a
continuation.
● Todas as funções precisam acabar chamando
sua continuação com o resultado da execução
da função.
const cpsFac = (n, con) => {
return n == 0
? con(1)
: cpsFac(n - 1, y =>
con(n + y))
}
cpsFac(3, id) //6
TRAMPOLINES
Técnica stack safe pra fazer
chamadas tail-recursive em
linguagens orientadas a stack
Um único trampoline consegue expressar
todas as transferência de controle do
programa.
Um trampoline é um loop
que iterativamente invoca
funções que retornam thunks
(continuation-passing style)
THUNKS
Função que encapsula outra
função com os parâmetros que
para quando a execução dessa
função for necessária.
function thunk(fn, args) {
return fn(...args)
}
Ao invés de chamar a tail call
diretamente, cada método retorna
a chamada para um thunk para o
trampolim chamar.
const trampoline = (thunk) => {
while(thunk instanceof Function){
thunk = thunk()
}
return thunk
}
● É um loop que chama funções repetidamente
● Cada função é chamada de thunk.
● O trampolim nunca chama mais de um thunk
● É como se quebrasse o programa em pequenos
thunks que saltam pra fora do trampolim, assim o
stack não cresce.
const trampolineFac = (n) {
const sum = (ac, n) => {
return n == 0
? ac
: thunk(sum, ac * n, n -
1)
}
return trampoline(thunk(sum, n, 0))
}
GC
sum(3)
GC GC
sum(2)
GC GC
:(
Trade off por stack safety
Trocamos o trabalho de criar stack frames com
o de criar binding de funções.
Em muitos casos o trade-off de
overhead por expressividade vale a
pena
Trampolines são mais apropriadas
para funções complexas em que não
existem soluções iterativas e não
conflitam com outras técnicas de
mediar controle de fluxo(Promises).
● Kyle Simpson. Functional Light Programming. Cap
8. - github.com/getify/Functional-Light-JS
● Functional Programming Jargon -
github.com/hemanth/functional-programming-
jargon
● Structure and Interpretation of Computer
Programs - Javascript Adaptation
● Compatibilidade TCO -
kangax.github.io/compat-table/es6/#test-
proper_tail_calls_
● Yoyojs - npmjs.com/package/yoyojs
● Ramda Memoisation -
ramdajs.com/docs/#memoize
t.me/lambdastudygroup
github.com/lambda-study-group
OBRIGADA :)
https://speakerdeck.com/anabastos

[JS EXPERIENCE 2018] Uma visão geral sobre recursão e trampolines - Ana Bastos, Quanto

Notas do Editor

  • #4 Uma visão geral sobre recursão e trampolines
  • #5 Antes de falar sobre recursão vamos falar sobre recursão
  • #7 In most of the pure functional programming languages (Haskell, Clean, Erlang) there are no for or while loops, so iterating over lists needs to be done using recursive functions. Pure functional programming languages have language support and are optimized for list comprehension and list concatenation. Muitas linguagens de programação são em torno de recursão e apesar do JS ter muitos elementos funcionais não foi feita pra isso
  • #8 In most of the pure functional programming languages (Haskell, Clean, Erlang) there are no for or while loops, so iterating over lists needs to be done using recursive functions. Pure functional programming languages have language support and are optimized for list comprehension and list concatenation.
  • #9 Precisamos definir: Defining an exit condition, an atomic definition that exists by itself (also called a “base case”). Defining which part of the algorithm is recursive.(ou seja, quando a função deve chamar a si mesma)
  • #10 A curried function takes one argument at a time and returns a function that takes the next argument. In Reason, functions can automatically be partially called:
  • #11 Avoiding State Change (and mutable data) - one of the characteristics of functional programming is that functions do not change the state of the application, they rather create a new state from the old one. Declarations vs statements - in functional programming as in Mathematics a declarative approach is used for defining/describing functions. Idempotence - it means when invoking a function (any number of times) using the same arguments will always have the same result, this also goes hand in hand with avoiding state change Recursion, however, is a natural match with pure functional programming - no state is needed to recurse, except for the (read-only) function arguments and a (write-only) return value. if re-written using iterative loops would be much more complex, unweildy or harder to read/maintain. Think about binary searching algorithms like tree-traversal. However, not having side effects also means that recursion can be implemented more efficiently, and the compiler can optimize it more aggressively. I haven't studied any such compiler in depth myself, but as far as I can tell, most functional programming languages' compilers perform tail call optimization, and some may even compile certain kinds of recursive constructs into loops behind the scenes. GCC does a much better job of TCO than GHC, because you can't do TCO across the creation of a thunk.
  • #12  Javascript is single threaded, which means that only one task can be run at any given time. When the Javascript interpreter on the page starts executing code it is running in an environment that is referred to as the Global Execution Context.
  • #13 factorial(0) // The factorial of 0 is 1 by definition (base case) factorial(1) // This call depends on factorial(0) factorial(2) // This call depends on factorial(1) factorial(3) // This first call depends on factorial(2)
  • #15 factorial(0) // The factorial of 0 is 1 by definition (base case) factorial(1) // This call depends on factorial(0) factorial(2) // This call depends on factorial(1) factorial(3) // This first call depends on factorial(2)
  • #16 So the way computers generally implement this idea of a function that could call another function that could call another. In memory they reserve an area of memory that's given the name a stack frame, generally. It's a small area of memory that keeps track of all the variables, sometimes referred to as the variables that are on the stack. At some point, there's a physical limit to the system. At some point we've run out. Now when you weren't doing recursion. I
  • #18  Various language features will trigger the creation of a new execution context (functions, eval, let blocks, closures, etc.). https://www.datchley.name/content/images/2015/11/Execution-Stack---JS.png Keep in mind that because of the scope chain, execution contexts will be able to access variables and functions delared in any parent (previous) scope. function calls add to the stack. . When the code in a given context is finished executing, that stack entry is destroyed and control is returned to the executable code from the previous stack execution context. https://www.datchley.name/content/images/2015/11/Single-Function--Iterative-Stack---JS.png
  • #19 In functional languages (like Elm, Elixir, Haskell, etc), it is impossible to do imperative loops, so the only option is recursion. não porque elas querem mas porque elas precisam Programacao funcional é legal mas js não foi feito pra ser uma lang funcional o suporte js é poop Since recursion is built into the language, the compiler will often make optimizations to guarantee that the call stack isn’t exceeded when processing large datasets Clojure: Nao suporta TCO pois JVM loop...recur: Note the similarity between this code and the Python fib_tail shown earlier. This is not a coincidence! Once the algorithm is expressed in tail form, it's pretty easy to convert it to an iteration pattern manually; if it wasn't easy, compilers wouldn't be able to do it automatically for the past 40 years! . Since the tail call carries the whole state around in arguments, we just imitate this using an explicit loop and state variables.
  • #20 Here, Javascript recognizes the tail-call and can then reuse the existing stack frame to make the recursive call, removing any previously local variables and state from the old call. As it turns out in the general sense, creating a new stack frame and throwing an old stack frame array. , we could just reuse the same stack frame, we could just override it with. The memory, we could reuse that memory for the next stack frame Differently to what happens with proper tail calls, tail call optimization actually improves the performance of tail recursive functions and makes running them faster. Tail call optimization is a technique used by the compiler to transform your recursive calls into a loop using jumps.
  • #22 Como fazer o tco funfar?
  • #23  So it's kind of like PTC is the umbrella and TCO is the way that an engine might choose to do different sorts of optimizations, make things not slow.
  • #24 http://25.media.tumblr.com/7dbfc8ccf3d93aa83aa98a2d9e269c58/tumblr_mx55sc2PYy1qbx12bo2_500.gif
  • #25 A curried function takes one argument at a time and returns a function that takes the next argument. In Reason, functions can automatically be partially called:
  • #26 hey’re always the last thing to be done and evaluated before the return and the return value of this called function is returned by the calling function.
  • #27 Pra refatorar eu preciso fazer um acumulador
  • #28 Retorna uma função com um argumento a mais chamado argumento Assim o retorno sempre será a própria função
  • #29 http://25.media.tumblr.com/7dbfc8ccf3d93aa83aa98a2d9e269c58/tumblr_mx55sc2PYy1qbx12bo2_500.gif
  • #30  At the time of writing, Safari is the only browser to have shipped PTC. Node implemented tail calls in version 6.5, but it was hidden behind a flag (later they removed support for PTC altogether in Node 8). The folks at Apple that drive Safari, they don't put features into their browser unless there is a really good reason why they want to. So my suspicion is that either something that's already shipped or something that's shipping soon on their road map, distinctly and directly requires them to have PTC support. he reason it's not in the other engines is not because it's too hard for them. They don't want to implement it. They've decided that implementing this feature, even though they voted to put it in the spec, they've decided that implementing this feature is gonna unnecessarily hamper other performance things that they wanna do. And so, they're pushing back on the TC39 committee saying, we wanna take that out of this spec. And the WebKit folks are like, well, we already shipped it like six months ago. So, we don't wanna take it out of the spec, we like it, we think it's good. If the engine just started doing that, that would actually make every function in your program, every single time recursive or not slower. So just implementing that feature that they could run in the recursive case in fixed memory would make all functions, even non-recursive ones, run slower.
  • #33 Conceito legal de programação
  • #34  CPS desugars function return, exceptions and first-class continuations; function call turns into a single jump instruction
  • #35 SCHEME?
  • #36 A continuation is an object that captures the current state of a program Easier: it is the work that remains to be done A common representation is a function I takes only one parameter with the result of previous computations
  • #37 FORMA DE SIMULAR AVALIACAO DEVAGAR AVALIACAO PREGUICOSA
  • #38 Call-by-ned: evaluation mechanism that delays the evaluation of an expression until its value is needed.
  • #40 Continuations are often seen in asynchronous programming when the program needs to wait to receive data before it can continue. The response is often passed off to the rest of the program, which is the continuation, once it's been received. unbounded continuations can be treated as first-class values in a language, they become so powerful that they can be used to implement pretty much any control-flow feature you can imagine - exceptions, threads, coroutines and so on. This is precisely what continuations are sometimes used for in the implementation of functional languages, where CPS-transform is one of the compilation stages. (lida com cps call/cc) const printAsString = (num) => console.log(`Given ${num}`) const addOneAndContinue = (num, cc) => { const result = num + 1 cc(result) } addOneAndContinue(2, printAsString) // 'Given 3' const continueProgramWith = (data) => { // Continues program with data } readFileAsync('path/to/file', (err, response) => { if (err) { // handle error return } continueProgramWith(response) }) Even though most programming languages don't support real, unbounded continuations, bounded continuations is another deal. A bounded continuation is just a function that returns to its caller Applying unbounded continuations is not just calling a function - it's passing control without hope of return. Just like coroutines, or longjmp in C. Unbounded continuations do not return to their caller. They express a flow of computation where results flow in one direction, without ever returning
  • #42  So, if I was in the very first call, that is I passed in only one number, then that would be calling the identity function with whatever I passed in, and just passing back the value directly. But if I was deeper into my stack of recursion, then I would just be passing sum into some function that had been passed to me, that had been passed to me, that had been passed. [00:02:57] So, what does that non-base case look like? You notice again, instead of calling sum plus recur, I call recur with my list of nums, that's my running array list if you will, but my last position there is a function. It's another continuation, a continuation that takes a v and calls whatever the current continuation is with that plus the sum. And since it does not rely on stack value, any values will be captured inside of the continuation. We can make even more than one continuation to execute on a different execution path or even on the different CPU with no problem with mutable state.
  • #43 Pra refatorar eu preciso fazer um acumulador
  • #44 Funcao identdade, que é um termo pra uma função que torna tudo que você poe dentro dela
  • #45 Para extrair o valor final
  • #48 Junta alguns conceitos ja abordados
  • #49  is common in functional programming and provides us a way to call our function in tail position without growing the stack.
  • #51 As used in some Lisp implementations
  • #52 http://25.media.tumblr.com/7dbfc8ccf3d93aa83aa98a2d9e269c58/tumblr_mx55sc2PYy1qbx12bo2_500.gif
  • #53 dessa forma usamos esse thunk pra emular uma avaliação lazy, que linguagens como js nao suportam por padrão. So the sole purpose of the trampoline function is to control the execution in an iterative way, and that ensures the stack to have only a single stack frame on the stack at any given time. Remember how I said, when discussing unbounded continuations, that in "regular" languages like Python we're just cheating and simulating continuations with function calls? Trampolines is what make this viable without blowing the stack def fact_cps_thunked(n, cont): if n == 0: return cont(1) else: return lambda: fact_cps_thunked( n - 1, lambda value: lambda: cont(n * value)) e o que é esse thunk? é uma expresão em uma função de argumetnos. Esse encapsulamento "atrasa" a avaliacao da expressao ate o ponto em que a funcao é chamada algo como > 2 * (3 + 4) // 14 > const f = () => 2 * (3 + 4) > f() // 14
  • #54 A curried function takes one argument at a time and returns a function that takes the next argument. In Reason, functions can automatically be partially called:
  • #56 A curried function takes one argument at a time and returns a function that takes the next argument. In Reason, functions can automatically be partially called:
  • #58 A curried function takes one argument at a time and returns a function that takes the next argument. In Reason, functions can automatically be partially called:
  • #59 Here, Javascript recognizes the tail-call and can then reuse the existing stack frame to make the recursive call, removing any previously local variables and state from the old call. As it turns out in the general sense, creating a new stack frame and throwing an old stack frame array. , we could just reuse the same stack frame, we could just override it with. The memory, we could reuse that memory for the next stack frame Differently to what happens with proper tail calls, tail call optimization actually improves the performance of tail recursive functions and makes running them faster. Tail call optimization is a technique used by the compiler to transform your recursive calls into a loop using jumps.
  • #60 http://25.media.tumblr.com/7dbfc8ccf3d93aa83aa98a2d9e269c58/tumblr_mx55sc2PYy1qbx12bo2_500.gif
  • #62  In my own performance profiling I found that the overhead from using the trampoline wasn’t nearly as large as I thought it would be. There’s no question about it — the trampoline is slower than an iterative loop. However, in many cases where a recursive solution can be cleaner and less error-prone, the performance overhead may be worth the readability benefits.
  • #64 http://www.crockford.com/javascript/little.html
  • #66 http://25.media.tumblr.com/7dbfc8ccf3d93aa83aa98a2d9e269c58/tumblr_mx55sc2PYy1qbx12bo2_500.gif
  • #67 http://25.media.tumblr.com/7dbfc8ccf3d93aa83aa98a2d9e269c58/tumblr_mx55sc2PYy1qbx12bo2_500.gif