Em mais uma série de artigos sobre as mais recentes e mais animadoras propostas que temos no TC39 eu trago mais uma dessas ideias que eu acho super legais e realmente espero (e dou meu suporte quando possível) que entrem na linguagem no futuro.

Hoje a gente vai falar de uma proposta super interessante, não é algo novo, principalmente para quem já ouviu falar em programação funcional. A proposta toca no que é chamada de do expressions, ou expressões de bloco.

Essa é ainda uma proposta no estágio 1, ou seja, existe uma chance grande de ela ser rejeitada, porém, por ela estar há uns 5 anos aberta, eu acredito que ainda pode ter uma esperança!

Do expressions

A proposta dá a ideia de uma "nova" keyword para o JavaScript, o do. Eu digo "nova" porque essa keyword já existe em expressões como do while, então a parte sintática da linguagem fica (um pouco) mais simples.

Um bloco do é o que a gente chama de programação orientada a expressões, onde o resultado de uma atribuição, como const x = 1, pode ser o resultado de uma expressão mais complexa. Isso simplifica muito a escrita do código para não ficar usando ternários o tempo todo, que são bons, porém difíceis de ler.

Alguns exemplos que podemos dar desse tipo de uso são para podermos ter as variáveis com o menor escopo possível, evitando que elas vazem para outro lugar, por exemplo, se quisermos fazer uma operação com uma variável X, ao invés de fazermos isso:

function main(x, y, options) {
  const finalX = par(x) && !options?.useY ? x + y : !par(x) ? x * 2 : 10
  return options?.useY && options?.fromZero ? finalX-- : finalX
}

Que é um código bastante complexo de ler, ou então podemos simplificar um pouco com:

function main(x, y, options) {
  let finalX = 10
  if (par(x) && !options?.useY) finalX = x + y
  else if (!par(x)) finalX = x * 2
  if (options?.useY && options?.fromZero) finalX--
  return finalX
}

Mas ai teríamos uma variável acumuladora finalX dentro do escopo da função, se essa função fosse longa, poderíamos ter um memory leak. E se pudéssemos deixar essa expressão toda dentro do mesmo escopo? É isso que é proposto aqui:

function main (x, y, options) {
  return do {
    let finalX = 10
    if (par(x) && !options?.useY) finalX = x + y
    else if (!par(x)) finalX = x * 2
    if (options?.useY && options?.fromZero) finalX--
    finalX
  }
}

Ou, se você só precisar atribuir a uma variável:

function main (x, y, options) {
  const x = do {
    let finalX = 10
    if (par(x) && !options?.useY) finalX = x + y
    else if (!par(x)) finalX = x * 2
    if (options?.useY && options?.fromZero) finalX--
    finalX
  }
  // Resto do código
}

Esse pode não ser o melhor exemplo, mas com este estilo de desenvolvimento você consegue reduzir o escopo para a menor unidade possível, a de expressão. Dessa forma o coletor de lixo do seu runtime vai saber o quando recursos podem ser liberados de uma forma mais eficiente, já que as expressões podem ser completamente descartadas quando terminam.

Usos

Alguns usos interessantes:

  1. Escopo mínimo de variáveis:
let x = do {
  let tmp = f();
  tmp * tmp + 1
};

2. Uso de condicionais para um código mais legível:

let x = do {
  if (foo()) { f() }
  else if (bar()) { g() }
  else { h() }
};

3. do expressions tem um uso excelente para linguagens de templating, como o JSX:

return (
  <nav>
    <Home />
    {
      do {
        if (loggedIn) {
          <LogoutButton />
        } else {
          <LoginButton />
        }
      }
    }
  </nav>
)

O que é permitido

Além de casos mais simples, alguns casos mais complexos, os chamados edge cases são também permitidos, por exemplo:

Atribuição usando var

Por padrão, qualquer tipo de atribuição de variável retorna um resultado vazio, por isso (como vou mostrar logo mais) não é possível atribuir nenhum tipo de variável como uma expressão, exceto quando estamos usando var, pois o escopo da variável seria global e o valor poderia sofrer um hoist para o topo da função local.

Vazio

Você pode usar um do {}, o que seria equivalente a ter uma função com void 0

function v () {
    return void 0
}

Assincronismo com await ou yield

Você pode usar await ou yield dependendo do escopo da função que contém o do, por exemplo, se a sua função for uma função async, você poderá usar um do { await ... }, caso seja um generator o mesmo vale para yield x.

Erros com throw

O throw funciona da forma como a gente espera, ou seja, podemos dar um throw em um erro dentro de uma do expression:

const p = do {
    if (!p?.prop) throw new Error('Ops')
    else p.prop * 2
}

Quebras de controle com break, continue ou return

Da mesma forma, você pode usar keywords de quebra de controle quando você está em um escopo apropriado, por exemplo, usar um return se o do está dentro de uma função, da mesma forma que é possível usar continueou break dentro de loops.

Lembrando que return vai retornar da função como um todo, e não somente do do, então:

function getUserId(blob) {
  let obj = do {
    try {
      JSON.parse(blob)
    } catch {
      return null; // sai da função com retorno null
    }
  };
  return obj?.userId;
}

Um caso especial é que o JS pode se confundir quando as expressões continue e break não tem o que chamamos de label. Uma label é uma forma de dizer para o runtime que aquela expressão tem um nome, então podemos dizer para o JS exatamente qual é a estrutura de controle que estamos nos referindo, isso é especialmente útil quando temos loops aninhados:

outer:
for (let i = 0; i < 5; i++) {
    inner:
    for (let j = 0; j < 10; j++) {
        if (i % 2 === 0) break outer
    }
}

Dessa forma, não é possível ter continue ou break sem uma label em do.

Parâmetros de funções

Você também pode usar do dentro de parâmetros de funções, e eles aceitam o return:

function foo (p = do { 
               if (!x) throw new Error('X is required') 
	       else return null 
}) {}

Conflito com o do while

Para remediar o conflito da keyword do no do while, você pode usar as expressões do dentro de parenteses:

do while (true) {
    let x = (do { ... })
}

Limitações

Por conta da quebra de sintaxe, especialmente em alguns casos, essa funcionalidade é bastante limitada no que pode ou não pode ser feito.

Se alguma expressão como as abaixo são detectadas, o código automaticamente retorna um erro instantaneamente.

Atribuições diretas

Você não pode retorna apenas atribuições de variáveis:

(do {
  let x = 1;
});

Isso acontece porque declarações tem um valor vazio como sendo o valor de "completude", ou seja, se nada acontecer, elas não retornam nada. Então se você fizer algo como do { 'antes'; let x = 'depois'; } isso tudo vai retornar 'antes' e a segunda expressão não vai retornar nada.

Criação de funções

Da mesma forma você não pode criar funções dentro de expressões:

(do {
  function f() {}
});

Loops

Loops não são permitidos dentro de expressões, em nenhum caso, nem mesmo aninhados em outras estruturas de controle como if:

(do {
  while (cond) {
    // código
  }
});

Ou

(do {
  if (condition) {
    while (inner) {
      // código
    }
  } else {
    42;
  }
});

Labels fora de loops

Você também não pode definir labels arbitrárias, somente labels que estão fora da expressão são válidas:

(do {
  label: {
    let x = 1;
    break label;
  }
});

if sem else

Todos os ifs dentro de uma expressão devem conter um else acompanhado:

(do {
  if (foo) {
    bar
  }
});

Conclusão

Essa é uma proposta ainda muito no início, o que significa que provavelmente muito do que está escrito aqui vai mudar no futuro, porém ela promete trazer uma nova forma de fazer com que o seu código fique mais organizado e, quem sabe, mais eficiente em termos de uso de recursos.