Swift / SwiftUI

Consultando coleções em Swift | Swift by Sundell

O artigo explora como usar métodos do Swift Standard Library para simplificar consultas em coleções, como arrays e dicionários. Ele demonstra como substituir loops manuais por funções como 'first(where:)' e 'allSatisfy', tornando o código mais conciso e legível.

Compartilhar
Querying collections in Swift | Swift by Sundell

Quando trabalhamos com coleções, como arrays ou dicionários, é muito comum querer consultar essas estruturas para obter informações sobre os valores que elas contêm. Podemos precisar encontrar o primeiro elemento que atende a um conjunto específico de critérios, validar todos os valores conforme uma determinada exigência e assim por diante.

Nestes casos, pode parecer inicialmente que sempre precisamos escrever algoritmos personalizados que realizem cada consulta usando um loop clássico for ou while, e embora isso seja uma abordagem totalmente aceitável em alguns casos, geralmente descobrimos que escrever tais iterações manuais pode ser bastante desnecessário.

Nesta semana, vamos explorar por quê — examinando algumas das algoritmos integrados da biblioteca padrão do Swift para consultar coleções e como podemos compor e combinar esses algoritmos para formar uma quantidade quase infinita de diferentes consultas.

Genius Scan SDK

Swift by Sundell é trazido a você pelo Genius Scan SDK — Adicione um poderoso escaneador de documentos a qualquer aplicativo móvel e transforme os scans em PDFs de alta qualidade com uma linha de código. Tente hoje.

Vamos começar examinando um tipo muito comum de situação, na qual queremos consultar uma coleção para encontrar o primeiro elemento que atende a um predicado específico.

Por exemplo, digamos que estamos trabalhando em um aplicativo de recomendação de filmes e que quando o usuário toca em um botão para mostrar o próximo filme dentro de um determinado gênero, procuramos uma movieQueue array e exibimos a primeira correspondência — assim:

func showNextMovie(inGenre genre: Genre) {
var match: Movie?

for movie in movieQueue {
if movie.genre == genre {

match = movie
break
}
}

guard let movie = match else {
return showNoMovieFoundView()
}

showMovieView(for: movie)
}

Ao contrário do código acima, ele poderia definitivamente ser implementado de uma maneira mais compacta, o que neste caso também melhoraria sua legibilidade.

Para começar, vamos nos livrar dessa variável local match, movendo nosso loop for para dentro de sua própria função dedicada. Como vimos em "Funções puras no Swift", estruturar a lógica como funções que simplesmente operam com base em entrada/saída pode melhorar nossa codificação — tanto em termos de quão fácil é ler, quanto de quão testável nosso sistema geral se torna.

Enquanto nossa nova função não será inteiramente "purificada" (já que ainda dependerá do estado da nossa array movieQueue), ela agora simplesmente retornará o primeiro filme correspondente encontrado, em vez de atribuí-lo a uma variável local:

func nextMovie(inGenre genre: Genre) -> Movie? {
for movie in movieQueue {
if movie.genre == genre {
return movie
}
}

return nil
}

Com isso em mente, podemos agora simplificar bastante nossa função showNextMovie de antes — já que ela agora apenas precisa conter a lógica que determina qual vista deve ser exibida para o estado atual do aplicativo:

func showNextMovie(inGenre genre: Genre) {
    guard let movie = nextMovie(inGenre: genre) else {
        return showNoMovieFoundView()
    }

    showMovieView(for: movie)
}

No entanto, usando o poder da biblioteca padrão do Swift, podemos simplificar nosso código ainda mais. Se olharmos de perto nossa função nextMovie e o loop for que ela contém, não há nada sobre isso que seja específico para filmes — simplesmente itera por um array e retorna o primeiro elemento que corresponde a um predicado dado.

A frase clássica da App Store poderia ser ligeiramente retorcida: “Há uma API de coleção para isso”. Nesse caso, podemos simplesmente substituir nossa função personalizada nextMovie com uma chamada ao método embutido first(where:), que nos permite passar um predicado a ser comparado. Combine isso com o método map do tipo Optional — e podemos implementar toda nossa função showNextMovie assim:

func showNextMovie(inGenre genre: Genre) {
    let movie = movieQueue.first(where: { $0.genre == genre })
    movie.map(showMovieView) ?? showNoMovieFoundView()
}

E assim, reduzimos um algoritmo de 10+ linhas a apenas duas linhas de código, usando as APIs e algoritmos embutidos da biblioteca padrão. Muito legal!

Também há uma equivalente last(where:) à API usada acima, bem como variantes firstIndex(where:) e lastIndex(where:) que retornam índices em vez de elementos. No entanto, as variantes “últimas” estão disponíveis apenas para coleções que conformem ao protocolo BidirectionalCollection, como Array.

No entanto, nem todas as consultas a coleções são sobre recuperar um elemento — às vezes só queremos verificar se uma série de valores atendem a um requisito específico.

Por exemplo, digamos que estamos trabalhando em um aplicativo para agendar e gerenciar eventos, e que o app inclui uma funcionalidade que permite a cada participante marcar se eles estão prontos para que o evento comece. Nós então estendemos nosso modelo de Event do app com um método que nos permite verificar facilmente se todos os participantes marcaram como prontos — que atualmente parece assim:

extension Event {
    func isReadyToBegin() -> Bool {
        for participant in participants {
            guard participant.isReady else {
                return false
            }
        }

        return true
    }
}

A razão pela qual a API acima foi implementada como um método e não uma propriedade computada é porque sua complexidade de tempo não é O(1). Para saber mais sobre essa filosofia, confira “Propriedades computadas em Swift”.

No entanto, assim como antes, há uma maneira muito mais fácil de implementar o acima — desta vez usando a API allSatisfy da biblioteca padrão, que (como o nome sugere) nos permite verificar se todos os elementos de uma coleção satisfazem um predicado dado. Combine isso com o fato de que agora caminhos-chave podem ser passados como funções, e podemos substituir toda nossa implementação por apenas uma linha de código — assim:

extension Evento {
func estaProntoParaComecar() -> Bool {
participants.allSatisfy(<.isReady>)
}
}

Com essa alteração em vigor, a questão é se ainda precisamos manter o método acima, dado que agora ele contém apenas uma chamada para outra API. Se estamos apenas procurando usá-lo em um único lugar, podemos simplesmente incorporar a chamada ao allSatisfy diretamente no local da chamada — no entanto, nosso método acima adiciona algum contexto adicional, pois seu nome torna claro que estamos verificando se um evento está pronto para começar, então ainda pode fazer sentido mantê-lo.

Agora, vamos dar uma olhada em outro tipo comum de consulta — calcular valores mínimos e máximos a partir de uma série de elementos. Dessa vez, digamos que estamos construindo um jogo e queremos adicionar uma API de conveniência para calcular a pontuação máxima entre um array de jogadores para um determinado nível. Se começarmos com um algoritmo implementado manualmente, podemos escrever algo como:

extension Nivel {
func pontuacaoMaximaDoJogador() -> Int {
var pontuacaoMaxima = 0

for jogador in jogadores {
pontuacaoMaxima = max(pontuacaoMaxima, jogador.score)
}

return pontuacaoMaxima
}
}

Ao usar a função max que usamos acima (juntamente com sua equivalente min) é incrivelmente comum e pode ser encontrada em uma ampla gama de linguagens de programação, o Swift também inclui uma variante dela que pode ser chamada diretamente em uma coleção. Tudo que temos a fazer para usá-la é passar um bloco que ordena os elementos da coleção em ordem crescente — como este:

extension Nivel {
func pontuacaoMaximaDoJogador() -> Int {

let jogadorVencedor = jogadores.max { $0.score < $1.score }
return jogadorVencedor?.score ?? 0
}
}

Uma abordagem alternativa ao acima seria primeiro ordenar o array de jogadores pela propriedade score de cada jogador e, em seguida, simplesmente escolher o primeiro elemento. No entanto, fazer isso seria pior em termos de complexidade temporal, já que min e max podem ser executados em tempo linear (O(n)), enquanto sort tem uma complexidade temporal de O(n log n).

Ao longo deste artigo, focamos principalmente em substituir algoritmos personalizados de coleção por chamadas à API da biblioteca padrão e embora seja certamente benéfico fazê-lo sempre que possível, isso nem sempre é o caso.

Vamos voltar ao nosso modelo Evento anterior e dar uma olhada em outra API também implementada como consulta de coleção. Esta verifica se um conjunto de usuários está presente na lista atual de participantes do evento e atualmente parece assim:

extension Event {
    func participantesContem(_ usuarios: Set<User>) -> Bool {
        for usuario in usuarios {
            guard participantes.contains(where: { $0.userID == usuario.id }) else {
                return false
            }
        }

        return true
    }
}

Ainda que neste caso não haja uma API do conjunto padrão equivalente que possamos simplesmente substituir o código acima, ainda há uma maneira pela qual poderíamos melhorá-lo.

A beleza dos algoritmos que vêm embutidos como parte da biblioteca padrão é que eles são todos muito focados e estreitos — o que por sua vez significa que podem frequentemente ser compostos para formar lógica de nível superior. Portanto, não precisamos necessariamente encontrar uma única API que corresponda a um algoritmo como um todo, simplesmente precisamos encontrar um correspondente para cada um dos seus componentes.

Olhando para o nosso algoritmo acima, já estamos usando a API contains(where:) para formar o loop interno, enquanto nossa iteração externa ainda usa um clássico loop for. No entanto, se olharmos mais de perto, descobrimos que o loop externo tem exatamente a mesma forma que aquele que inicialmente usamos dentro do nosso método isReadyToBegin anterior — na medida em que verifica que todos os elementos dentro do conjunto de usuários passado atendem a uma determinada exigência.

Agora, poderíamos voltar ao nosso método participantsContain e refatorar o código ali mesmo, mas neste caso implementaremos nossa lógica como um algoritmo genérico em vez disso — combinando allSatisfy e contains(where:) em um novo método para o protocolo Sequence (que todos os coleções e outras sequências seguem):

extension Sequence {
    func contém<T: Sequence>( 
        _ valores: T,
        correspondidoPor matcher: (Element, T.Element) -> Bool
    ) -> Bool {
        valores.allSatisfy { valor in
            contains(where: { matcher($0, valor) })
        }
    }
}

Com o acima em vigor, podemos agora voltar ao nosso tipo Event e simplesmente fazer com que seu método participantsContain chame nossa nova API passando sua condição como uma closure — assim:

extension Event {
    func participantesContem(_ usuarios: Set<User>) -> Bool {
        participantes.contains(usuarios, correspondidoPor: {
            $0.userID == $1.id
        })
    }
}

A escolha de implementar um pedaço de lógica como um algoritmo genérico ou algo que seja especificamente escrito para um caso de uso específico pode ser ocasionalmente bastante difícil. No entanto, uma regra geral a manter em mente é que sempre que nossa lógica for simplesmente uma composição de outros algoritmos genéricos — provavelmente faz sentido para esse novo algoritmo também ser genérico.

A biblioteca padrão do Swift contém uma série de algoritmos, funções e outras APIs incrivelmente úteis — e usar plenamente essas ferramentas não apenas reduz a quantidade de código que precisamos manter por nossa conta, mas também nos permite construir nossa lógica em cima de código compartilhado que é bem testado e implantado em cada programa Swift no planeta.

Há muitos mais APIs de Coleção além das destacadas neste artigo, então recomendo fortemente verificar os artigos sobre fatias e transformações, se você ainda não o fez, e explorar as próprias interfaces do Sequence e Collection — é provável que você encontre pelo menos uma maneira de simplificar um dos seus algoritmos.

Tem perguntas, comentários ou feedback? Sempre estou à disposição para entrar em contato comigo, seja via Twitter ou email.

Obrigado por ler! 🚀

Fonte original

Conteúdo traduzido e adaptado pela redação do Notícias Mobile. Confira também a matéria na fonte original.

Leia a matéria completa