Página principal

Compreendendo o conceito de Memoization (ou memoização) em 3 minutos

Autor: Codesmith || Versão original

Existem vários artigos excelentes que falam sobre a técnica de otimização chamada memoization (memoização). Você pode acessá-los aqui aqui e aqui (OBS: artigos em inglês). O que vamos fazer neste artigo é uma breve visão geral do conceito, fazendo uma análise passo a passo do que o código de memoização está fazendo, linha por linha.

Memoização em poucas palavras

Memoização é a prática programática de fazer funções recursivas/iterativas longas serem executadas com muito mais rapidez. Como isso acontece? Armazenando em cache os valores que a função retorna após sua execução inicial.

Quando inserimos o mesmo valor em nossa função memo, ela retorna o valor armazenado no cache em vez de executar a função novamente, aumentando assim o desempenho. Seu programa não precisa mais recalcular todos os números para obter um resultado. Parece incrível, certo? Bem, o que é ainda melhor é que não é difícil entender o que uma função memoizada está fazendo depois de analisar o código. Aqui está um exemplo de função memo básica:


                  var cache = {};
                  return function(){
                    var key = JSON.stringify(arguments);
                    if (cache[key]){
                      console.log(cache)
                      return cache[key];
                    }
                    else{
                      val = func.apply(null, arguments);
                      cache[key] = val;
                      return val; 
                    }
                }
              }
              
                

Vamos começar do começo…

Estamos retornando o que é chamado de anonymous closure (um closure anônimo). Nosso closure anônimo é capaz de herdar qualquer variável ou, neste caso, função passada para memo. Podemos então manipular o objeto de argumentos que nossa função passada fornece.

Observação: então, se cada função tem um objeto de argumentos, por que não herdamos o objeto de argumentos da função memo? Isso ocorre porque, como via de regra, closures não herdam o objeto de argumentos de uma função externa. Usamos essa regra a nosso favor para manipular a função que queremos memoizar.

Vamos analisar exatamente o que a memoização está fazendo, observando um exemplo muito simples e pouco prático. A propósito, você deve copiar / colar este código, ou qualquer código para esse assunto, para realmente ter uma ideia de como funciona.


                / *
                Closures não podem acessar o objeto de argumentos do pai, mas, 
                como as funções são objetos de primeira classe, podemos passar 
                uma função como parâmetro.
                O closure agora pode acessar o objeto de argumentos da função 
                que é passada como um parâmetro.
                Portanto, não há confusão sobre quais objetos de argumentos 
                queremos que o closure acesse. Basicamente, estamos aproveitando
                suas limitações.
                * /
                
                function demoMemo(func){
                // devemos retornar uma função para manter o state (estado)
                // isso ficará mais aparente em um exemplo recursivo
                
                  return function () {
                    console.log(func); // >   function (num){num + num}
                    console.log(arguments[0]); // > 1
                  }
                }
                
                // Nossa function expression aqui invoca demoMemo e passa uma 
                função anônima.
                
                var adder = demoMemo(function(num){
                 num + num;
                }) 
                
                // Depois que isso é passado, quando chamamos nossa expressão de 
                // função, temos acesso a todas as propriedades da função que 
                // queremos memoizar.
                
                adder(1);
                
                

Vamos aprofundar um pouco mais olhando para um retrato de uma função memo real:


                //Sabendo que temos acesso aos inputs do usuário em nossa function
                //expression, podemos escrever...
                  
                return function(){     
                     var key = JSON.stringify(arguments);
                     if (cash[key]){
                      return cache[key];
                     }
                    
                    else{
                      //apply() é bastante útil aqui e vai 
                      //retornar o valor da função que está chamando
                      val = func.apply(this, arguments);
                      //então definimos o valor da função para o key(argument).
                      //Da próxima vez que a função for executada
                      //se o argumento for o mesmo, nós retornamos
                      //o valor sem precisar que a função seja executada.
                
                      cash[key] = val;
                      return val;
                    }
                }
                
                

Agora que dividimos o que uma função memo está fazendo, vamos criar uma function expression e passar uma função Fibonacci recursiva para memo e ver o que acontece.


                var fib = memo(function(n) {
                  if (n < 2){
                    return 1;
                  }else{
                   //Vamos olhar via console.log um carregamento cada vez que
                   //tivermos um recurse
                    console.log("carregando...");
                    return fib(n-2) + fib(n-1);
                  }
               });
               
               
               
               // Agora vamos testar nosso código!
               // ======= TESTE =======
               fib (10)
               // Na primeira tentativa, precisamos carregar
               // => carregando…
               // => carregando…
               // => carregando…
               // => carregando…
               // => carregando…
               // => 4 carregamentos depois nós temos ...
               // => 89
               // E na segunda tentativa ...
               fib (10)
               // => 89
               // Sem carregamento! Yaay!
               // O legal de memoizar o algoritmo de Fibonacci recursivo é que, 
               // assim que fizermos uma chamada para o valor do enésimo número 
               // na série, poderemos armazenar todos os números anteriores da série.
               // Então, se tentarmos ...
               fib (7) // => 21
               // Não precisamos recalcular nada.
               // E se quisermos tentar fib (11) ...
               fib (11)
               //carregando...
               // => 144
               // Nossa função recursiva memoizada já armazenava fib (1-10) em cache, 
               //então tudo o que precisava fazer era calcular os valores armazenados 
               //em cache.
               // O cache está assim agora:
               / *
               {‘{“ 0 ”: 0}’: 1,
               ‘{“ 0 ”: 1}’: 1,
               ‘{“ 0 ”: 2}’: 2,
               ‘{“ 0 ”: 3}’: 3,
               ‘{“ 0 ”: 4}’: 5,
               ‘{“ 0 ”: 5}’: 8,
               ‘{“ 0 ”: 6}’: 13,
               ‘{“ 0 ”: 7}’: 21,
               ‘{“ 0 ”: 8}’: 34,
               ‘{“ 0 ”: 9}’: 55,
               ‘{“ 0 ”: 10}’: 89,
               ‘{“ 0 ”: 11}’: 144}
               * /
               
                

Conclusão

A memoização é desmistificada quando é reduzida a pares de valores-chave. Tudo o que estamos fazendo é criando um objeto, verificando os valores existentes que correspondem à entrada do usuário e armazenando novos pares de valores-chave, se eles não existirem ainda em nosso objeto. Claro, armazenar todos esses dados significa que vamos usar toda a memória. É melhor implementar a memoização em funções que são puras e envolvem cálculos pesados e repetitivos.