Swift e Objective-C

[Swift intermediário #5] Funções de ordem superior

As cinco funções map·filter·compactMap·flatMap·reduce se distinguem pelo que o closure retorna. Este guia explica quando escolhê-las em vez de loops for, os arrays intermediários criados por encadeamentos e os problemas resolvidos por sequências lazy.

7 min de leitura
Imagem de capa de [Swift intermediário #5] Funções de ordem superior

Todo mundo usa map e filter. O problema começa depois. compactMap e flatMap confundem por terem nomes parecidos, enquanto reduce é evitado porque sua assinatura parece difícil. Ao encadear várias chamadas, surge a dúvida de desempenho: “Quantos arrays novos isso está criando?”

A quinta parte da série intermediária é um guia prático sobre funções de ordem superior. Ela aborda a diferença exata entre as cinco funções, os critérios para escolhê-las em vez de loops for e os problemas resolvidos por sequências lazy. É a parte prática construída sobre os fundamentos do capítulo de closures.

O papel exato das cinco funções — o que a assinatura revela

Uma função de ordem superior recebe uma função como argumento. As cinco principais funções de processamento de arrays se distinguem pela assinatura do closure, mais precisamente, por aquilo em que um elemento se transforma.

map: elemento → novo elemento. Mantém a quantidade. Se entram n elementos, saem n. Apenas transforma; não filtra.

filter: elemento → Bool. A quantidade pode diminuir; os elementos permanecem iguais. Mantém apenas os elementos que atendem à condição e não faz transformações.

compactMap: elemento → opcional. Descarta nil e retorna o valor desembrulhado. É uma combinação especializada de map e filter, útil quando uma transformação pode falhar.

let inputs = ["1", "2", "três", "4"]
let numbers = inputs.compactMap { Int($0) }  // [1, 2, 4]

Int(“três”) resulta em nil, e compactMap o filtra. Se map { Int($0) } tivesse sido usado, [Int?] seria produzido, repetindo o tratamento de opcionais em cada ponto de uso. compactMap implementa, para coleções, o princípio do capítulo de opcionais: “desembrulhe o opcional na fronteira”.

flatMap: elemento → array. Achata um nível de aninhamento. Use quando um elemento se expande em vários. Se sentences.flatMap { $0.split(separator: " ") } transforma um array de frases em um array de palavras, map produziria [[단어]], enquanto flatMap remove um nível de aninhamento. A diferença para compactMap é simples: closure que retorna opcional significa compactMap; closure que retorna array ou sequência significa flatMap.

reduce: coleção inteira → um valor. É a forma geral de reduzir tudo a um resultado, como calcular uma soma, encontrar o maior valor ou criar um dicionário. reduce(0, +) calcula uma soma; o primeiro argumento é o valor inicial, e o segundo define como combinar o resultado acumulado com o próximo elemento. Guarde uma dica de desempenho: ao acumular um array ou dicionário, use reduce(into:). O reduce comum copia o acumulador a cada etapa, enquanto a versão into continua modificando um único valor, o que faz grande diferença com muitos dados.

Loops for vs. funções de ordem superior — a realidade da legibilidade

A afirmação de que “usar funções de ordem superior em vez de loops for é mais idiomático em Swift” é metade correta e metade perigosa. Veja os critérios.

O verdadeiro benefício das funções de ordem superior não é serem curtas, mas declararem a intenção. Ao ver filter, o leitor sabe, no nível da assinatura, que está filtrando e que os elementos não serão alterados. Com um loop for, é preciso ler todo o corpo para chegar à mesma conclusão. É um exemplo da expressividade vista na Parte 1: fazer a intenção aparecer diretamente no código.

Mas esse benefício só existe quando cada etapa é simples. Se o closure contém três níveis de condições e efeitos colaterais — alterar uma variável externa ou chamar print —, a expectativa de que filter apenas filtre é quebrada. O código pode ficar mais difícil de ler que um for, violando o princípio da menor surpresa. Os critérios são:

  • Combinações simples de transformação, filtragem e agregação → funções de ordem superior.
  • Ramificações complexas em cada etapa, efeitos colaterais ou saída intermediária (break) necessária → loop for. Funções de ordem superior não têm um equivalente adequado para break: first(where:) funciona para parar no primeiro elemento correspondente, mas loops são mais naturais para lógicas de saída antecipada mais complexas.
  • Tarefas que precisam de índice → também podem usar uma função de ordem superior com enumerated(), mas, quando ficam complexas, prefira um loop.

Um antipadrão precisa ser destacado: fazer append em um array externo dentro de forEach. var result: [Int] = []; items.forEach { result.append($0 * 2) } reimplementa manualmente o que uma linha de map faz e apenas aumenta o estado mutável. forEach serve para efeitos colaterais em cada elemento, como enviar notificações; quando o objetivo é produzir algo, use uma função da família map.

Diagrama comparando um encadeamento que acumula arrays intermediários com um pipeline lazy que deixa os elementos passarem um por vez
O encadeamento cria um array intermediário em cada etapa; lazy deixa os elementos passarem um por vez

O custo do encadeamento — o convidado oculto chamado array intermediário

Encadear funções de ordem superior parece elegante, mas cada etapa cria um novo array.

let result = products
    .filter { $0.inStock }      // array intermediário 1
    .map { $0.price }           // array intermediário 2
    .prefix(5)                  // final

Com um milhão de elementos, filter pode criar um array de até um milhão de itens, e map cria outro. Mesmo precisando apenas dos cinco primeiros, você percorre e aloca duas vezes.

lazy é a solução padrão. Ao inserir lazy como em products.lazy.filter{...}.map{...}.prefix(5), a natureza do pipeline muda. Em vez de executar imediatamente e criar arrays, ele cria uma sequência adiada que registra apenas “o que fazer”. No consumo final, cada elemento passa por todo o pipeline, um por vez. A iteração para assim que cinco elementos são obtidos; portanto, apenas a parte inicial necessária do milhão é acessada. Não há arrays intermediários.

Isso não significa que lazy deva ser usado sempre. Como não armazena resultados, consumir a mesma sequência adiada duas vezes executa o cálculo duas vezes. Como os closures são armazenados por referência e executados depois, também podem surgir restrições relacionadas a escaping. A regra é simples: lazy brilha com dados grandes e consumo parcial, como a combinação prefix, first(where:). Se tudo será consumido e armazenado como array, a execução imediata costuma ser melhor. É a versão para coleções da mesma pergunta por trás de uma propriedade lazy var: “Vale a pena adiar o cálculo?”

Receitas de combinações práticas

Veja algumas combinações frequentes organizadas como receitas.

Criando um dicionário. Use Dictionary(uniqueKeysWithValues: users.map { ($0.id, $0) }) para criar uma tabela de consulta por ID. Se houver possibilidade de chaves duplicadas, Dictionary(grouping: orders, by: { $0.customerID }) é a versão de agrupamento.

Passagem segura por opcionais. Transforme o array de IDs String da resposta do servidor em um array de URL: ids.compactMap { URL(string: $0) }. As falhas são filtradas silenciosamente, e o tipo é definido como [URL].

Combinações de ordenação. Também vale conhecer items.sorted { $0.priority > $1.priority }, que usa KeyPath em vez de items.sorted(using: KeyPathComparator(\.priority, order: .reverse)). KeyPath será abordado separadamente mais adiante.

Agregação. O total do carrinho é cart.reduce(0) { $0 + $1.price * Double($1.quantity) }. Para uma soma simples, porém, muitas vezes é mais legível dividi-la como cart.map(\.subtotal).reduce(0, +). Se o closure de reduce ficar complexo, é um sinal para dividi-lo.

Ilustração de um desenvolvedor lendo uma placa na bifurcação entre funções de ordem superior e loops for
Para transformações simples, funções de ordem superior; para ramificações, efeitos colaterais ou saída antecipada, loops

Resumo

  • O critério é o retorno do closure: novo elemento significa map, Bool significa filter, opcional significa compactMap, array significa flatMap e reduzir tudo a um valor significa reduce (reduce(into:) para coleções acumuladas).
  • A vantagem das funções de ordem superior é declarar a intenção. Quando o closure fica complexo ou inclui efeitos colaterais, essa vantagem desaparece; então volte ao loop for.
  • O encadeamento cria um array intermediário em cada etapa. Para consumir apenas parte de dados grandes, crie um pipeline adiado com lazy; ao consumir tudo, mantenha a execução imediata.
  • Se você está criando um array com forEach, é hora de trocar para uma função da família map.

A próxima parte aborda o tema de todo desenvolvedor iOS que lida com JSON: Codable avançado. Ela cobre CodingKeys, estruturas aninhadas, estratégias de data e a solução para o problema de a decodificação inteira falhar por causa de um único campo.

Continue lendo