Funções de Ordem Superior
Existem duas maneiras de construir um design de software: uma é torná-lo tão simples que obviamente não haja deficiências, e a outra é torná-lo tão complicado que não haja deficiências óbvias.

Um programa grande é um programa custoso, e não apenas por causa do tempo que leva para construí-lo. O tamanho quase sempre envolve complexidade, e a complexidade confunde programadores. Programadores confusos, por sua vez, introduzem erros (bugs) nos programas. Um programa grande então oferece muito espaço para esses bugs se esconderem, tornando-os difíceis de encontrar.
Vamos voltar brevemente aos dois últimos programas de exemplo da introdução. O primeiro é autocontido e tem seis linhas.
let total = 0, count = 1; while (count <= 10) { total += count; count += 1; } console.log(total);
O segundo depende de duas funções externas e tem uma linha.
console.log(sum(range(1, 10)));
Qual deles é mais provável de conter um bug?
Se contarmos o tamanho das definições de sum e range, o segundo programa também é grande—até maior que o primeiro. Mas ainda assim, eu argumentaria que ele tem mais chances de estar correto.
Isso acontece porque a solução é expressa em um vocabulário que corresponde ao problema que está sendo resolvido. Somar um intervalo de números não tem a ver com loops e contadores. Tem a ver com intervalos e somas.
As definições desse vocabulário (as funções sum e range) ainda vão envolver loops, contadores e outros detalhes incidentais. Mas, como estão expressando conceitos mais simples do que o programa como um todo, são mais fáceis de acertar.
Abstração
No contexto da programação, esse tipo de vocabulário geralmente é chamado de abstração. Abstrações nos dão a capacidade de falar sobre problemas em um nível mais alto (ou mais abstrato), sem nos perdermos em detalhes pouco interessantes.
Como analogia, compare estas duas receitas de sopa de ervilha. A primeira é assim:
Coloque 1 xícara de ervilhas secas por pessoa em um recipiente. Adicione água até que as ervilhas estejam bem cobertas. Deixe as ervilhas na água por pelo menos 12 horas. Retire as ervilhas da água e coloque-as em uma panela. Adicione 4 xícaras de água por pessoa. Tampe a panela e mantenha as ervilhas fervendo em fogo baixo por duas horas. Pegue meia cebola por pessoa. Corte em pedaços com uma faca. Adicione às ervilhas. Pegue um talo de aipo por pessoa. Corte em pedaços com uma faca. Adicione às ervilhas. Pegue uma cenoura por pessoa. Corte em pedaços. Com uma faca! Adicione às ervilhas. Cozinhe por mais 10 minutos.
Por pessoa: 1 xícara de ervilhas secas partidas, 4 xícaras de água, meia cebola picada, um talo de aipo e uma cenoura.
Deixe as ervilhas de molho por 12 horas. Cozinhe em fogo baixo por 2 horas. Pique e adicione os vegetais. Cozinhe por mais 10 minutos.
A segunda é mais curta e mais fácil de interpretar. Mas você precisa entender mais algumas palavras relacionadas à culinária, como deixar de molho, cozinhar em fogo baixo, picar e, imagino, vegetal.
Ao programar, não podemos contar que todas as palavras de que precisamos estarão esperando por nós no dicionário. Assim, podemos cair no padrão da primeira receita—detalhar os passos exatos que o computador deve executar, um por um, ignorando os conceitos de nível mais alto que eles expressam.
É uma habilidade útil, na programação, perceber quando você está trabalhando em um nível de abstração baixo demais.
Abstraindo repetição
Funções simples, como as que vimos até agora, são uma boa maneira de construir abstrações. Mas às vezes elas não são suficientes.
É comum um programa fazer algo um certo número de vezes. Você pode escrever um loop for para isso, assim:
for (let i = 0; i < 10; i++) { console.log(i); }
Podemos abstrair “fazer algo N vezes” como uma função? Bem, é fácil escrever uma função que chama console.log N vezes.
function repeatLog(n) { for (let i = 0; i < n; i++) { console.log(i); } }
Mas e se quisermos fazer algo diferente de registrar os números? Como “fazer algo” pode ser representado como uma função e funções são apenas valores, podemos passar nossa ação como um valor de função.
function repeat(n, action) { for (let i = 0; i < n; i++) { action(i); } } repeat(3, console.log); // → 0 // → 1 // → 2
Não precisamos passar uma função pré-definida para repeat. Muitas vezes, é mais fácil criar um valor de função na hora.
let labels = []; repeat(5, i => { labels.push(`Unit ${i + 1}`); }); console.log(labels); // → ["Unit 1", "Unit 2", "Unit 3", "Unit 4", "Unit 5"]
Isso é estruturado de forma semelhante a um loop for—primeiro descreve o tipo de loop e depois fornece um corpo. No entanto, o corpo agora é escrito como um valor de função, que fica dentro dos parênteses da chamada de repeat. É por isso que ele precisa ser fechado com a chave de fechamento e o parêntese de fechamento. Em casos como este exemplo, em que o corpo é uma única expressão pequena, você também pode omitir as chaves e escrever o loop em uma única linha.
Funções de ordem superior
Funções que operam sobre outras funções, seja recebendo-as como argumentos ou retornando-as, são chamadas de funções de ordem superior. Como já vimos que funções são valores comuns, não há nada particularmente surpreendente no fato de que tais funções existam. O termo vem da matemática, onde a distinção entre funções e outros valores é levada mais a sério.
Funções de ordem superior nos permitem abstrair ações, não apenas valores. Elas aparecem em várias formas. Por exemplo, podemos ter funções que criam novas funções.
function greaterThan(n) { return m => m > n; } let greaterThan10 = greaterThan(10); console.log(greaterThan10(11)); // → true
Também podemos ter funções que modificam outras funções.
function noisy(f) { return (...args) => { console.log("calling with", args); let result = f(...args); console.log("called with", args, ", returned", result); return result; }; } noisy(Math.min)(3, 2, 1); // → calling with [3, 2, 1] // → called with [3, 2, 1] , returned 1
Podemos até escrever funções que fornecem novos tipos de fluxo de controle.
function unless(test, then) { if (!test) then(); } repeat(3, n => { unless(n % 2 == 1, () => { console.log(n, "is even"); }); }); // → 0 is even // → 2 is even
Existe um método embutido de array, forEach, que fornece algo parecido com um loop for/of como uma função de ordem superior.
["A", "B"].forEach(l => console.log(l)); // → A // → B
Dataset de sistemas de escrita
Uma área onde funções de ordem superior brilham é no processamento de dados. Para processar dados, precisaremos de alguns dados de exemplo reais. Este capítulo usará um dataset (conjunto de dados) sobre sistemas de escrita como latino, cirílico ou árabe.
Lembre-se do Unicode, o sistema que atribui um número a cada caractere na linguagem escrita, do Capítulo 1? A maioria desses caracteres está associada a um sistema de escrita específico. O padrão contém 140 sistemas de escrita diferentes, dos quais 81 ainda estão em uso hoje e 59 são históricos.
Embora eu consiga ler fluentemente apenas caracteres latinos, aprecio o fato de que as pessoas escrevem textos em pelo menos 80 outros sistemas de escrita, muitos dos quais eu nem reconheceria. Por exemplo, aqui está uma amostra de escrita à mão em Tamil:

O dataset de exemplo contém algumas informações sobre os 140 sistemas de escrita definidos no Unicode. Ele está disponível no ambiente de código para este capítulo como a ligação SCRIPTS. A ligação contém um array de objetos, cada um descrevendo um sistema de escrita.
{
name: "Coptic",
ranges: [[994, 1008], [11392, 11508], [11513, 11520]],
direction: "ltr",
year: -200,
living: false,
link: "https://en.wikipedia.org/wiki/Coptic_alphabet"
}
Esse objeto nos informa o nome do sistema de escrita, os intervalos Unicode atribuídos a ele, a direção em que é escrito, o tempo de origem (aproximado), se ainda está em uso e um link para mais informações. A direção pode ser "ltr" (da esquerda para a direita), "rtl" (da direita para a esquerda, como nos textos em árabe e hebraico) ou "ttb" (de cima para baixo, como na escrita mongol).
A propriedade ranges contém um array de intervalos de caracteres Unicode, cada um sendo um array de dois elementos contendo um limite inferior e um limite superior. Qualquer código de caractere dentro desses intervalos é atribuído ao sistema de escrita. O limite inferior é inclusivo (o código 994 é um caractere copta) e o limite superior não é inclusivo (o código 1008 não é).
Filtrando arrays
Se quisermos encontrar os sistemas de escrita no dataset que ainda estão em uso, a seguinte função pode ser útil. Ela filtra elementos em um array que não passam em um teste.
function filter(array, test) { let passed = []; for (let element of array) { if (test(element)) { passed.push(element); } } return passed; } console.log(filter(SCRIPTS, script => script.living)); // → [{name: "Adlam", …}, …]
A função usa o argumento chamado test, um valor de função, para preencher uma “lacuna” no cálculo—o processo de decidir quais elementos coletar.
Observe como a função filter, em vez de remover elementos do array existente, constrói um novo array apenas com os elementos que passam no teste. Essa função é pura. Ela não modifica o array que recebe.
Assim como forEach, filter é um método de array padrão. O exemplo definiu a função apenas para mostrar o que ela faz internamente. A partir de agora, vamos usá-la assim:
console.log(SCRIPTS.filter(s => s.direction == "ttb")); // → [{name: "Mongolian", …}, …]
Transformando com map
Digamos que temos um array de objetos representando sistemas de escrita, produzido ao filtrar o array SCRIPTS de alguma forma. Queremos um array de nomes em vez disso, que seja mais fácil de inspecionar.
O método map transforma um array aplicando uma função a todos os seus elementos e construindo um novo array a partir dos valores retornados. O novo array terá o mesmo comprimento que o array de entrada, mas seu conteúdo terá sido mapeado para uma nova forma pela função.
function map(array, transform) { let mapped = []; for (let element of array) { mapped.push(transform(element)); } return mapped; } let rtlScripts = SCRIPTS.filter(s => s.direction == "rtl"); console.log(map(rtlScripts, s => s.name)); // → ["Adlam", "Arabic", "Imperial Aramaic", …]
Assim como forEach e filter, map é um método padrão de array.
Resumindo com reduce
Outra coisa comum a fazer com arrays é calcular um único valor a partir deles. Nosso exemplo recorrente, somar uma coleção de números, é um caso disso. Outro exemplo é encontrar o sistema de escrita com mais caracteres.
A operação de ordem superior que representa esse padrão é chamada de reduce (às vezes também chamada de fold). Ela constrói um valor pegando repetidamente um único elemento do array e combinando-o com o valor atual. Ao somar números, você começaria com o número zero e, para cada elemento, somaria ao total.
Os parâmetros de reduce são, além do array, uma função de combinação e um valor inicial. Essa função é um pouco menos direta que filter e map, então observe com atenção:
function reduce(array, combine, start) { let current = start; for (let element of array) { current = combine(current, element); } return current; } console.log(reduce([1, 2, 3, 4], (a, b) => a + b, 0)); // → 10
O método padrão de array reduce, que naturalmente corresponde a essa função, tem uma conveniência adicional. Se seu array contém pelo menos um elemento, você pode omitir o argumento start. O método usará o primeiro elemento do array como valor inicial e começará a reduzir a partir do segundo elemento.
console.log([1, 2, 3, 4].reduce((a, b) => a + b)); // → 10
Para usar reduce (duas vezes) para encontrar o sistema de escrita com mais caracteres, podemos escrever algo assim:
function characterCount(script) { return script.ranges.reduce((count, [from, to]) => { return count + (to - from); }, 0); } console.log(SCRIPTS.reduce((a, b) => { return characterCount(a) < characterCount(b) ? b : a; })); // → {name: "Han", …}
A função characterCount reduz os intervalos atribuídos a um sistema de escrita somando seus tamanhos. Observe o uso de destructuring na lista de parâmetros da função redutora. A segunda chamada a reduce então usa isso para encontrar o maior sistema de escrita, comparando repetidamente dois sistemas de escrita e retornando o maior.
O sistema de escrita Han tem mais de 89.000 caracteres atribuídos a ele no padrão Unicode, tornando-o de longe o maior sistema de escrita no dataset. Han é um sistema de escrita às vezes usado para textos em chinês, japonês e coreano. Esses idiomas compartilham muitos caracteres, embora tendam a escrevê-los de forma diferente. O Unicode Consortium (com sede nos EUA) decidiu tratá-los como um único sistema de escrita para economizar códigos de caracteres. Isso é chamado de unificação Han e ainda deixa algumas pessoas bastante irritadas.
Componibilidade
Considere como teríamos escrito o exemplo anterior (encontrar o maior sistema de escrita) sem funções de ordem superior. O código não é tão pior assim.
let biggest = null; for (let script of SCRIPTS) { if (biggest == null || characterCount(biggest) < characterCount(script)) { biggest = script; } } console.log(biggest); // → {name: "Han", …}
Há algumas variáveis a mais, e o programa é quatro linhas mais longo, mas ainda é bastante legível.
As abstrações que essas funções fornecem realmente brilham quando você precisa compor operações. Como exemplo, vamos escrever código que encontra o ano médio de origem para sistemas de escrita vivos e mortos no dataset.
function average(array) { return array.reduce((a, b) => a + b) / array.length; } console.log(Math.round(average( SCRIPTS.filter(s => s.living).map(s => s.year)))); // → 1165 console.log(Math.round(average( SCRIPTS.filter(s => !s.living).map(s => s.year)))); // → 204
Como você pode ver, os sistemas de escrita mortos no Unicode são, em média, mais antigos do que os vivos. Essa não é uma estatística muito significativa ou surpreendente. Mas espero que você concorde que o código usado para calculá-la não é difícil de ler. Você pode enxergá-lo como um pipeline: começamos com todos os sistemas de escrita, filtramos os vivos (ou mortos), pegamos os anos deles, calculamos a média e arredondamos o resultado.
Você certamente também poderia escrever esse cálculo como um único loop grande.
let total = 0, count = 0; for (let script of SCRIPTS) { if (script.living) { total += script.year; count += 1; } } console.log(Math.round(total / count)); // → 1165
No entanto, é mais difícil ver o que estava sendo calculado e como. E como os resultados intermediários não são representados como valores coerentes, seria muito mais trabalhoso extrair algo como average para uma função separada.
Em termos do que o computador realmente está fazendo, essas duas abordagens também são bem diferentes. A primeira constrói novos arrays ao executar filter e map, enquanto a segunda calcula apenas alguns números, fazendo menos trabalho. Normalmente você pode se dar ao luxo de usar a abordagem mais legível, mas se estiver processando arrays enormes muitas vezes, o estilo menos abstrato pode valer a pena pela velocidade extra.
Strings e códigos de caracteres
Um uso interessante desse dataset seria descobrir qual sistema de escrita um pedaço de texto está usando. Vamos percorrer um programa que faz isso.
Lembre-se de que cada sistema de escrita tem um array de intervalos de códigos de caracteres associado a ele. Dado um código de caractere, poderíamos usar uma função como esta para encontrar o sistema de escrita correspondente (se houver):
function characterScript(code) { for (let script of SCRIPTS) { if (script.ranges.some(([from, to]) => { return code >= from && code < to; })) { return script; } } return null; } console.log(characterScript(121)); // → {name: "Latin", …}
O método some é outra função de ordem superior. Ele recebe uma função de teste e informa se essa função retorna verdadeiro para qualquer elemento do array.
Mas como obtemos os códigos de caracteres em uma string?
No Capítulo 1 mencionei que strings em JavaScript são codificadas como uma sequência de números de 16 bits. Esses são chamados de unidades de código. Um código de caractere Unicode originalmente deveria caber dentro de uma dessas unidades (o que permite um pouco mais de 65.000 caracteres). Quando ficou claro que isso não seria suficiente, muitas pessoas resistiram à necessidade de usar mais memória por caractere. Para lidar com essas preocupações, foi criado o UTF-16, o formato também usado pelas strings em JavaScript. Ele descreve a maioria dos caracteres comuns usando uma única unidade de código de 16 bits, mas usa um par de duas dessas unidades para outros.
O UTF-16 é geralmente considerado uma má ideia hoje. Parece quase intencionalmente projetado para provocar erros. É fácil escrever programas que tratam unidades de código e caracteres como a mesma coisa. E se seu idioma não usa caracteres de duas unidades, isso aparentemente funciona bem. Mas assim que alguém tenta usar esse programa com alguns caracteres chineses menos comuns, ele quebra. Felizmente, com o surgimento dos emoji, todo mundo passou a usar caracteres de duas unidades, e o ônus de lidar com esses problemas ficou mais bem distribuído.
Infelizmente, operações óbvias em strings JavaScript, como obter seu tamanho pela propriedade length e acessar seu conteúdo com colchetes, lidam apenas com unidades de código.
// Dois caracteres emoji, cavalo e sapato let horseShoe = "🐴👟"; console.log(horseShoe.length); // → 4 console.log(horseShoe[0]); // → (Meio caractere inválido) console.log(horseShoe.charCodeAt(0)); // → 55357 (Código da metade do caractere) console.log(horseShoe.codePointAt(0)); // → 128052 (Código real do emoji de cavalo)
O método charCodeAt do JavaScript fornece uma unidade de código, não um código completo de caractere. O método codePointAt, adicionado posteriormente, fornece um caractere Unicode completo, então podemos usá-lo para obter caracteres de uma string. Mas o argumento passado para codePointAt ainda é um índice na sequência de unidades de código. Para percorrer todos os caracteres em uma string, ainda precisamos lidar com a questão de saber se um caractere ocupa uma ou duas unidades de código.
No capítulo anterior, mencionei que um loop for/of também pode ser usado em strings. Assim como codePointAt, esse tipo de loop foi introduzido em uma época em que as pessoas estavam bem conscientes dos problemas com UTF-16. Quando você o usa para iterar sobre uma string, ele fornece caracteres reais, não unidades de código.
let roseDragon = "🌹🐉"; for (let char of roseDragon) { console.log(char); } // → 🌹 // → 🐉
Se você tem um caractere (que será uma string de uma ou duas unidades de código), pode usar codePointAt(0) para obter seu código.
Reconhecendo texto
Temos uma função characterScript e uma maneira de iterar corretamente sobre caracteres. O próximo passo é contar os caracteres que pertencem a cada sistema de escrita. A seguinte abstração de contagem será útil aqui:
function countBy(items, groupName) { let counts = []; for (let item of items) { let name = groupName(item); let known = counts.find(c => c.name == name); if (!known) { counts.push({name, count: 1}); } else { known.count++; } } return counts; } console.log(countBy([1, 2, 3, 4, 5], n => n > 2)); // → [{name: false, count: 2}, {name: true, count: 3}]
A função countBy espera uma coleção (qualquer coisa que possamos percorrer com for/of) e uma função que calcula um nome de grupo para um elemento dado. Ela retorna um array de objetos, cada um nomeando um grupo e informando quantos elementos foram encontrados nele.
Ela usa outro método de array, find, que percorre os elementos do array e retorna o primeiro para o qual uma função retorna verdadeiro. Ele retorna undefined quando não encontra tal elemento.
Usando countBy, podemos escrever a função que nos diz quais sistemas de escrita são usados em um pedaço de texto.
function textScripts(text) { let scripts = countBy(text, char => { let script = characterScript(char.codePointAt(0)); return script ? script.name : "none"; }).filter(({name}) => name != "none"); let total = scripts.reduce((n, {count}) => n + count, 0); if (total == 0) return "No scripts found"; return scripts.map(({name, count}) => { return `${Math.round(count * 100 / total)}% ${name}`; }).join(", "); } console.log(textScripts('英国的狗说"woof", 俄罗斯的狗说"тяв"')); // → 61% Han, 22% Latin, 17% Cyrillic
A função primeiro conta os caracteres por nome, usando characterScript para atribuir um nome a eles e usando a string "none" como fallback para caracteres que não pertencem a nenhum sistema de escrita. A chamada a filter remove a entrada "none" do array resultante, já que não estamos interessados nesses caracteres.
Para poder calcular percentuais, primeiro precisamos do número total de caracteres que pertencem a um sistema de escrita, o que podemos calcular com reduce. Se não encontrarmos tais caracteres, a função retorna uma string específica. Caso contrário, transforma as entradas de contagem em strings legíveis com map e depois as combina com join.
Resumo
Ser capaz de passar valores de função para outras funções é um aspecto profundamente útil do JavaScript. Isso nos permite escrever funções que modelam cálculos com “lacunas”. O código que chama essas funções pode preencher essas lacunas fornecendo valores de função.
Arrays fornecem vários métodos úteis de ordem superior. Você pode usar forEach para iterar sobre os elementos de um array. O método filter retorna um novo array contendo apenas os elementos que passam na função predicado. Você pode transformar um array aplicando uma função a cada elemento com map. Pode usar reduce para combinar todos os elementos de um array em um único valor. O método some testa se algum elemento corresponde a uma função predicado, enquanto find encontra o primeiro elemento que corresponde a um predicado.
Exercícios
Achatando
Use o método reduce em combinação com o método concat para “achatar” um array de arrays em um único array que contém todos os elementos dos arrays originais.
let arrays = [[1, 2, 3], [4, 5], [6]]; // Seu código aqui. // → [1, 2, 3, 4, 5, 6]
Seu próprio loop
Escreva uma função de ordem superior loop que forneça algo semelhante a uma instrução de loop for. Ela deve receber um valor, uma função de teste, uma função de atualização e uma função de corpo. A cada iteração, deve primeiro executar a função de teste no valor atual do loop e parar se ela retornar false. Em seguida, deve chamar a função de corpo, passando o valor atual, e por fim chamar a função de atualização para criar um novo valor e recomeçar do início.
Ao definir a função, você pode usar um loop comum para fazer a repetição real.
// Seu código aqui. loop(3, n => n > 0, n => n - 1, console.log); // → 3 // → 2 // → 1
Tudo
Arrays também têm um método every análogo ao método some. Esse método retorna true quando a função fornecida retorna true para todos os elementos do array. De certa forma, some é uma versão do operador || que atua sobre arrays, e every é como o operador &&.
Implemente every como uma função que recebe um array e uma função predicado como parâmetros. Escreva duas versões: uma usando um loop e outra usando o método some.
function every(array, test) { // Seu código aqui. } console.log(every([1, 3, 5], n => n < 10)); // → true console.log(every([2, 4, 16], n => n < 10)); // → false console.log(every([], n => n < 10)); // → true
Mostrar dicas...
Assim como o operador &&, o método every pode parar de avaliar elementos assim que encontrar um que não corresponda. Portanto, a versão baseada em loop pode sair do loop—com break ou return—assim que encontrar um elemento para o qual a função predicado retorna false. Se o loop terminar sem encontrar tal elemento, sabemos que todos os elementos corresponderam e devemos retornar true.
Para construir every a partir de some, podemos aplicar as leis de De Morgan, que afirmam que a && b é igual a !(!a || !b). Isso pode ser generalizado para arrays, onde todos os elementos correspondem se não houver nenhum elemento no array que não corresponda.
Direção dominante de escrita
Escreva uma função que calcule a direção de escrita dominante em uma string de texto. Lembre-se de que cada objeto de sistema de escrita tem uma propriedade direction que pode ser "ltr" (da esquerda para a direita), "rtl" (da direita para a esquerda) ou "ttb" (de cima para baixo).
A direção dominante é a direção da maioria dos caracteres que têm um sistema de escrita associado. As funções characterScript e countBy definidas anteriormente no capítulo provavelmente serão úteis aqui.
function dominantDirection(text) { // Seu código aqui. } console.log(dominantDirection("Hello!")); // → ltr console.log(dominantDirection("Hey, مساء الخير")); // → rtl
Mostrar dicas...
Sua solução pode se parecer bastante com a primeira metade do exemplo textScripts. Novamente, você precisa contar caracteres com base em um critério derivado de characterScript e então filtrar a parte do resultado que se refere a caracteres sem sistema de escrita (irrelevantes).
Encontrar a direção com a maior contagem de caracteres pode ser feito com reduce. Se não estiver claro como, consulte o exemplo anterior no capítulo, onde reduce foi usado para encontrar o sistema de escrita com mais caracteres.