Aguarde...

19 de janeiro de 2019

Padrões do iterador de JavaScript

Padrões do iterador de JavaScript

Neste artigo, vamos explorar diferentes maneiras de criar iteradores e valores iteráveis ​​em Javascript, especificamente funções , iteradores , iteráveis e geradores .

O JavaScript é uma linguagem muito flexível e, na maioria das vezes, você pode atingir os mesmos objetivos de muitas maneiras diferentes, os iteradores não são exceção!

A Wikipedia define os iteradores da seguinte maneira:

Na programação de computadores, um iterador é um objeto que permite ao programador percorrer um contêiner, particularmente as listas. Vários tipos de iteradores são frequentemente fornecidos através da interface de um contêiner.

Nós ampliaremos esta definição ainda mais, já que não nos concentraremos em construir iteradores para valores pré-computados como listas, mas veremos como iterar sobre seqüências gerativas como a seqüência de Fibonacci .

Muito provavelmente você não estará usando a seqüência Fibonacci em sua programação do dia a dia (a menos que você esteja entrevistando para alguma empresa que queira validar seu conhecimento de recursão 😆), mas a ideia de gerar uma sequência de valores sob demanda (avaliação lenta). ) traduz bem para muitos cenários da vida real como:

  • atravessando estruturas de dados personalizadas
  • consumindo APIs paginadas
  • drenando uma fila
  • processando arquivos longos linha por linha
  • leia todos os registros de uma tabela SQL
  • etc.

A seqüência de Fibonacci

Caso você nunca tenha visto a seqüência de Fibonacci antes (ou você não se lembra da definição exata), aqui está como se parece:

1 1 2 3 5 8 13 21 ...

Essencialmente, um número na sequência é dado pela soma dos dois números anteriores.

Em termos matemáticos mais formais, você pode definir a sequência como:

1 = F 2 = 1 
n = F (n-1) + F (n-2)

Poucas coisas para notar:

  • A sequência é infinita (seria impossível armazená-la em uma lista sem limite superior).
  • É feito por inteiros positivos.

Então, como escrevemos algum código JavaScript que nos permite iterar sobre essa sequência e calcular um número arbitrário de elementos?

Bem, existem muitas maneiras …

Funções

Em JavaScript, as funções são cidadãos de primeira classe e a maioria dos padrões pode ser modelada com o uso de apenas funções simples. Isso se tornará natural quando você dominar os conceitos de escopo de função , funções anônimas e funções aninhadas .

Então, como podemos construir uma seqüência de fibonacci usando apenas funções?

Aqui está um exemplo:

const genFib = (max = Number.MAX_SAFE_INTEGER) => {
  // initialize default values in the scope
  let n1 = 0
  let n2 = 0

  // returns an anonymous function that will return the next element
  // every time that it is called
  return () => {
    // calculates the next value
    const nextVal = n2 === 0 ? 1 : n1 + n2

    // redefines n1 and n2 to match new values
    const prevVal = n2
    n2 = nextVal
    n1 = prevVal

    // if we reached the upper bound return null (iteration completed)
    if (nextVal >= max) {
      return null
    }

    // return the new value
    return nextVal
  }
}

Eu adicionei alguns comentários para tornar o código fácil de entender, mas vamos passar por isso mais uma vez.

  1. genFibé uma função que aceita um parâmetro opcional, que é o limite superior usado para definir quando parar de calcular elementos na sequência. Números de JavaScript começam a perder precisão depois Number.MAX_SAFE_INTEGER, então esse é um padrão sensato.
  2. A primeira coisa que acontece na função é inicializar o escopo da função. n1n2são os dois únicos valores que precisamos para calcular um elemento da seqüência. Eles representam os últimos 2 números computados. Nós os definimos como 0padrão.
  3. Neste ponto, a função retorna uma função anônima. Esta função pode ser invocada um número arbitrário de vezes e toda vez que ele irá calcular e retornar um novo elemento na sequência, assegurando que o estado interno seja atualizado de acordo.

Observe que genFibirá iniciar um novo escopo isolado contendo n1n2. Esses valores serão acessíveis (e modificáveis) somente pela função anônima retornada por genFib. Isso significa que você pode gerar múltiplos “iteradores” e todos eles serão independentes uns dos outros.

Para entender isso ainda melhor, vamos ver um exemplo de como um usuário usaria esse código:

const f = genFib(6) // limit the sequence to numbers below 6
f() // 1
f() // 1
f() // 2
f() // 3
f() // 5
f() // null
f() // null
f() // null

// or with a loop

// prints all the numbers of the sequence below MAX_SAFE_INTEGER
const f2 = genFib()
let current
while ((current = f2()) !== null) {
  console.log(current)
}

O protocolo do iterador

No exemplo anterior, criamos a nossa maneira de definir como iterar os elementos (função anônima retornada) e como entender se a sequência foi concluída (retorno de null).

O ECMAScript 2015 fornece uma maneira padrão e interoperável de definir objetos do iterador. Isso é chamado de protocolo Iterador .

Em suma, um objeto JavaScript é um iterador se implementa um next() método com a seguinte semântica:

  • next() não aceita nenhum argumento.
  • next()tem que retornar um objeto com 2 propriedades: donevalue.
  • doneé um booleano e será definido como truese e somente se não houver mais elementos na sequência.
  • valueconterá o valor real calculado na última iteração (pode ser undefinedquando doneé true).

Ok, agora vamos reescrever nossa seqüência de Fibonacci para implementar o protocolo Iterator:

const genFibIterator = (max = Number.MAX_SAFE_INTEGER) => {
  let n1 = 0
  let n2 = 0

  // this time we return an iterator object (rather than a function)
  return {
    // the logic needed to compute the next element is inside the `next` method
    next: () => {
      // calculates the next value
      let nextVal = n2 === 0 ? 1 : n1 + n2

      // redefines n1 and n2 to match new values
      const prevVal = n2
      n2 = nextVal
      n1 = prevVal

      // if we reached the upper bound (iteration completed)
      // set done to true and nextVal to undefined
      let done = false
      if (nextVal >= max) {
        nextVal = undefined
        done = true
      }

      // return the iteration object as for the iteration protocol
      return { value: nextVal, done }
    }
  }
}

Os comentários no código devem ajudá-lo a entender a nova lógica.

Vamos ver como usar nossa nova implementação do iterador Fibonacci:

const it = genFibIterator(6) // { next: [Function: next] }
it.next() // { value: 1, done: false }
it.next() // { value: 1, done: false }
it.next() // { value: 2, done: false }
it.next() // { value: 3, done: false }
it.next() // { value: 5, done: false }
it.next() // { done: true }

// or

const it2 = genFibIterator(6)
let result = it2.next()
while (!result.done) {
  console.log(result.value)
  result = it2.next()
}
// 1
// 1
// 2
// 3
// 5

O protocolo iterável

Na seção anterior, vimos como definir objetos Iterator que estão em conformidade com o protocolo Iterator. Na realidade, poderíamos querer expressar o conceito de “iterabilidade” de maneira mais genérica, de modo que, dado qualquer objeto, possamos dizer se tal objeto é iterável ou não.

Por esse motivo, o ECMAScript 2015 também define o protocolo Iterable .

Um objeto é considerado iterável se expõe uma propriedade chamada Symbol.iterable, que é uma função que retorna um objeto iterador .

Você pode verificar introspectivamente se um objeto é iterável com algum código como este:

function isIterable(obj) {
  return Boolean(obj) && typeof obj[Symbol.iterator] === 'function'
}

O ECMAScript 2015 também fornece uma nova forconstrução ( for...of) que permite iterar facilmente sobre os elementos de um objeto iterável:

for (let current of someIterable) {
  console.log(current)
}

Objetos iteráveis ​​também podem ser usados ​​em combinação com o operador spread para carregar ansiosamente todos os valores e armazená-los em uma matriz:

const allValues = [...someIterable]

Ok, agora vamos reescrever nossa seqüência de Fibonacci para implementar o protocolo Iterable:

const genFibIterable = (max = Number.MAX_SAFE_INTEGER) => {
  let n1 = 0
  let n2 = 0

  // returns an iterable object
  return {
    [Symbol.iterator] () {
      // returns an iterator
      return {
        next() {
          let nextVal = n2 === 0 ? 1 : n1 + n2

          const prevVal = n2
          n2 = nextVal
          n1 = prevVal

          let done = false
          if (nextVal >= max) {
            nextVal = undefined
            done = true
          }

          return { value: nextVal, done }
        }
      }
    }
  }
}

O que fizemos aqui foi simplesmente mover a implementação do iterador visto na seção anterior para a Symbol.iteratorfunção.

Observe que é possível criar uma implementação que possa satisfazer os protocolos Iterator e Iterable ao mesmo tempo:

const genFib = (max = Number.MAX_SAFE_INTEGER) => {
  let n1 = 0
  let n2 = 0

  return {
    // this satisfies the Iterator protocol
    next: () => {
      let nextVal = n2 === 0 ? 1 : n1 + n2

      const prevVal = n2
      n2 = nextVal
      n1 = prevVal

      let done = false
      if (nextVal >= max) {
        nextVal = undefined
        done = true
      }

      return { value: nextVal, done }
    },

    // this satisfies the Iterable protocol
    [Symbol.iterator] () {
      // returns `this` because the object itself is an iterator
      return this
    }
  }
}

Os comentários no código devem ajudá-lo a entender a lógica nessas duas implementações.

Com essas novas abordagens, você pode gerar números da sequência de Fibonacci da seguinte maneira:

// prints all the numbers in the sequence until MAX_SAFE_INTEGER
const f = genFibIterable()
for (let n of f) {
  console.log(n)
}

// creates an array with all the Fibonacci numbers lower than 17
const f2 = genFibIterable(17)
const lowerThan17 = [...f2] // [ 1, 1, 2, 3, 5, 8, 13 ]

Se neste momento você ainda estiver lutando para ver a diferença lógica entre um iterador e um objeto iterável, você pode ver da seguinte maneira:

  • Um iterável é um objeto no qual você pode iterar.
  • Um iterador é um objeto de cursor que permite iterar sobre um iterável .

Geradores

Outra grande adição vinda do ECMAScript 2015 para o JavaScript são os Geradores . Mais especificamente, o ECMAScript 2015 define funções Gerador e objetos Geradores .

Uma function*declaração (palavra-chave de função seguida por um asterisco) define uma função Generator , que retorna um objeto Generator .

Geradores são funções que podem ser encerradas e, posteriormente, reintroduzidas. Seu contexto (ligações variáveis) será salvo entre as reentrâncias.

Para simplificar um pouco este conceito, você pode ver as funções do gerador como funções que podem “retornar” (ou “ produzir ”) várias vezes.

Vamos explorar a sintaxe do gerador com um exemplo simples:

// generator function
function* countTo3() {
  yield 1
  yield 2
  return 3
}

Neste exemplo estamos definindo um counterque gera números de 1 a 3. Podemos usá-lo da seguinte maneira:

// c is a generator object
const c = countTo3()
c.next() // { value: 1, done: false }
c.next() // { value: 2, done: false }
c.next() // { value: 3, done: true }
c.next() // { done: true }
c.next() // { done: true }
// ...

Então, o modo como um gerador funciona é o seguinte:

  • Quando você invoca uma função geradora , um objeto gerador é retornado.
  • Objetos geradores possuem um next()método.
  • Quando você invoca o next()método de um objeto gerador, o código do gerador será executado até que o primeiro yield(ou return) seja encontrado.
  • Se um yieldfoi encontrado, o código é parado e o valor produziu será passada para o contexto de chamada que um objecto com a seguinte forma: { value: <yieldedValue>, done: false }.
  • Na próxima vez que next()for invocada, a execução será retomada do ponto em que foi inicialmente suspensa até uma nova yieldou returnser encontrada.
  • Se uma returninstrução for encontrada (ou a função for concluída), o objeto retornado será semelhante a: { value: <returnedValue>, done: true } (observe o doneagora definido como true).
  • Chamadas consecutivas next()sempre serão produzidas { done: true }.

Naturalmente, a razão pela qual estamos explorando esse tópico é porque podemos implementar nossa sequência de Fibonacci como um gerador:

function* Fib (max = Number.MAX_SAFE_INTEGER) {
  // initialize the state variables
  let n1 = 0
  let n2 = 0
  // we can now pre-initialize nextVal to 1 as part of the state
  let nextVal = 1

  // loop until we exceed the max number
  while (nextVal <= max) {
    // yields the current value
    yield nextVal

    // shifts nextVal -> n2 and n2 -> n1
    const prevVal = n2
    n2 = nextVal
    n1 = prevVal

    // calculates the next value
    nextVal = n1 + n2
  }
}

Os comentários no código devem ajudá-lo a entender essa implementação.

Você pode notar imediatamente que, como não temos que lidar com uma função aninhada , a implementação parece mais fácil de ler ou, pelo menos, pode parecer mais fácil ler o código e entender o fluxo de execução real.

Por esse motivo, você pode preferir usar geradores sobre funções simples nesse tipo de cenário.

Podemos usar nossa nova seqüência Fibonacci baseada em gerador como neste exemplo:

const fib = Fib(6)
fib.next() // { value: 1, done: false }
fib.next() // { value: 1, done: false }
fib.next() // { value: 2, done: false }
fib.next() // { value: 3, done: false }
fib.next() // { value: 5, done: false }
fib.next() // { done: true }

// or

const fib2 = Fib(6)
let result = fib2.next()
while (!result.done) {
  console.log(result.value)
  result = fib2.next()
}
// 1
// 1
// 2
// 3
// 5

Neste ponto você pode estar vagando:

Um objeto gerador é um iterador ou iterável?

Bem, acontece que um objeto gerador é tanto um iterador quanto iterável !

Então você também pode usar nossa última implementação com a sintaxe for...ofe a propagação :

const fib = Fib(6)
for (let current of fib) {
  console.log(current)
}
// 1
// 1
// 2
// 3
// 5

// or
const fib2 = Fib(6)
[...fib2] // [ 1 1 2 3 5 ]

Finalmente, como os geradores são iteradores , você pode usá-los como Symbol.iteratorpropriedade de um objeto iterável . Isso pode ajudá-lo a definir a lógica de iteração de uma maneira mais elegante e concisa, aproveitando a yieldpalavra – chave.

Até certo ponto, você pode ver os geradores como um açúcar sintático para definir objetos iteráveis.

Conclusão

Neste artigo, aprendemos maneiras diferentes de gerar seqüências dinâmicas usando funções simples, iteradores, iteráveis ​​e geradores.

Observe que essas abordagens são ideais quando a operação necessária para gerar o próximo elemento é síncrona (não requer recursos externos para serem carregados de forma assíncrona).

Quando você precisa iterar sobre valores que se tornam disponíveis de forma assíncrona, é necessário contar com diferentes padrões, como emissores de eventos , fluxos ou iteradores assíncronos .

Além disso, observe que os geradores têm alguns recursos avançados interessantes não abordados neste artigo, como a oportunidade de passar novos valores no contexto toda vez que .next()é chamado ou lançar exceções, portanto, verifique a documentação dos geradores .

Postado em BlogTags:
Escreva um comentário