O que são as "do expressions" no JavaScript?

javascript5 min

porLucas Santos

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.