Photo by Emile Perron on Unsplash

Programação Funcional em Swift

Lia Kassardjian
Zero e Umas
Published in
8 min readApr 28, 2020

--

Vamos começar pelos básicos: o que é Programação Funcional? Bom, é um paradigma de programação. Ok, mas o que é um paradigma de programação? Vamos às definições então:

Um paradigma é uma forma de se pensar; no caso da programação, é a forma como entendemos a construção e a lógica de um programa. Existem vários paradigmas diferentes e, portanto, várias formas de se pensar. Quanto começamos a programar, somos ensinados a dar comandos; essa é uma forma de se pensar, é o paradigma imperativo. Vamos ver o seguinte trecho de código:

var numero = 23numero = 34

Nesse trecho de código, uma variável é criada, um valor inicial é atribuído a ela e, depois, outro valor é atribuído. Podemos perceber que são dadas ordens, comandos ao programa, por isso é chamado de imperativo; nesse estilo de programação, há uma grande preocupação em definir como resolver um problema. Nesse estilo de programação, nós temos variáveis; esse nome cabe muito bem, pois são itens cujo valor é mutável. Esse é o estilo de programação que abriga os paradigmas Estruturado e Orientado a Objetos.

Existe, porém, um outro estilo de programação, que chamamos de Declarativo. Nesse estilo, não há a preocupação de dar comandos, mas sim em compor expressões, já que o foco é o que resolver, e não o como. Esse é o estilo de paradigmas como o Lógico e o próprio Funcional, que é o que queremos estudar aqui.

Agora sim podemos definir o paradigma Funcional: é aquele em que funções são considerados cidadãos de primeira classe, ou seja, podem ser passadas como argumentos, retorno ou atribuídas a constantes. É interessante ressaltar que, nesse paradigma, não há mutabilidade de estados nem “efeitos colaterais”, isto é, quando estados são mudados por funções que não os definiram. Esse paradigma é baseado no Lamba-cálculo e, nele, um programa é desenvolvido puramente com funções; é um paradigma de estilo declarativo, usando expressões ao invés de afirmações. Lisp e Scheme são exemplos de linguagens baseadas no paradigma funcional, mas Swift também possibilita o desenvolvimento com programação funcional.

Em Swift

Swift não é uma linguagem puramente funcional, mas oferece algumas possibilidades para quem deseja usar esse paradigma. Algumas dessas possibilidades são as funções map, filter e reduce, métodos que são aplicáveis a conjuntos de objetos. A Programação Funcional é possível de ser executada em Swift graças aos métodos, que também podem ser chamados de closures, e aos dados constantes, definidos como let; ao definir um dado como let, ele não pode ser alterado, logo, é constante e não há mutabilidade de estados.

Map

A função map aceita uma única função como parâmetro e retorna um vetor do mesmo tamanho do que lhe foi passado, mas não necessariamente do mesmo tipo.

Supondo que tenhamos a seguinte estrutura:

struct Fruta {    let nome: String    let emoji: String    let cor: Cor // enumerador já declarado que conforma com o protocolo String    let peso: Double}

Além disso, temos também um vetor de frutas:

Se quiséssemos exibir uma lista dos emojis das frutas definidas acima, poderíamos usar o map para criar um vetor de emojis. Vejamos, por exemplo, a seguinte função:

func emojis(de frutas: [Fruta]) -> [String] {    return frutas.map { $0.emoji }}

A função emojis mapeia um vetor de frutas que lhe é passado pelo atributo emoji e retorna um vetor de String. É interessante observar a programação funcional em ação: perceba que a função map recebe uma função como argumento, e não uma variável ou uma referência a um objeto. Se executarmos um print de emojis sobre frutas, teremos o seguinte resultado:

Se quiséssemos também gerar uma lista das frutas ordenadas por nome, poderíamos também usar o map para gerar um vetor com os nomes das frutas e, em seguida, aplicar o método sorted sobre o vetor, como mostra o código a seguir:

func ordenaNomes(de frutas: [Fruta]) -> [String] {    let nomesFrutas = frutas.map { $0.nome }    return nomesFrutas.sorted(by: <)}

De novo, vemos o paradigma funcional: o método sorted recebe a função <, que é um método de comparação entre elementos. Ao executar um print de ordenaNomes de frutas, teremos o resultado:

["Banana", "Kiwi", "Laranja", "Maracujá", "Maçã", "Maçã", "Morango", "Pêssego", "Uva"]

Filter

A função filter também aceita uma função como parâmetro e esta aceita um único objeto do conjunto que está sendo filtrado; esse objeto é o que será filtrado. A função retorna um Booleano, indicando se a condição do filtro foi satisfeita ou não.

Voltando à lista de frutas, podemos definir um filtro que, a partir de um conjunto de frutas, retorna aquelas que são de uma determinada cor, como mostra a função abaixo:

func filtra(por cor: Cor) -> ([Fruta]) -> [Fruta] {    return { frutas in        frutas.filter { $0.cor == cor }    }}

Como, no paradigma funcional, as funções são cidadãos de primeira classe, podemos retorná-las de outras funções, e é o que observamos na função filtra(por cor:), o retorno da função é uma outra função (ou uma closure, se você preferir chamar assim) e própria função filter recebe uma função como argumento. Nessa função, é retornado um vetor de frutas cuja cor é igual ao parâmetro cor. A partir dessa função, podemos definir um filtro e aplicá-lo a vários conjuntos de frutas.

let filtroVermelho = filtra(por: .vermelho)

É interessante notar que filtroVermelho não é um número, ou uma string, ou uma referência para um objeto; filtroVermelho é uma função; o paradigma funcional nos permite atribuir funções a constantes e usá-las múltiplas vezes como quisermos.

Na Programação Funcional, combinamos funções de modo a obter o resultado desejado. Podemos, então, aplicar a função emojis no resultado da função filtroVermelho sobre frutas, da seguinte forma:

print(emojis(de: filtroVermelho(frutas)))

obtendo o resultado:

Podemos brincar com a função filter passando outras funções como argumento:

func frutasComPesoAbaixoDe(_ peso: Double, frutas: [Fruta]) -> [Fruta] {    return frutas.filter { $0.peso < peso }}

A função frutasComPesoAbaixoDe recebe um Double como peso e uma lista de frutas, retornando aquelas cujo peso é menor que o que foi passado. O resultado dessa função com peso 100 gramas sobre o vetor frutas é:

Reduce

O método reduce, diferente dos últimos dois, recebe dois parâmetros: um ponto de início de um tipo genérico T e uma função que combina aquele valor inicial com um elemento do conjunto, produzindo outro valor do tipo T.

Nós podemos usar a função reduce para definir o peso de uma salada de frutas, por exemplo:

func pesoSaladaDeFrutas(com frutas: [Fruta]) -> Double {    return frutas.reduce(0) { (total, fruta) in        total + fruta.peso    }}

Mais uma vez, vemos o retorno da função sendo uma outra função: pesoSaladaDeFrutas retorna a função reduce iniciada com 0 e com a função total + fruta.peso, de modo que o resultado seja a soma do peso de todas as frutas do conjunto. Se aplicarmos sobre o vetor frutas, obtemos 805g.

Métodos de comparação

Estamos acostumados a usar métodos como <, > e == sobre números e até mesmo caracteres, mas Swift nos permite definir métodos como esses para nossos tipos customizados também, para podermos fazer comparações do tipo fruta1 <= fruta2, como veremos no exemplo a seguir.

Se definirmos que uma fruta é “menor” que a outra quando seu peso é menor e que é “igual” a outra quando o peso é igual, podemos definir os operadores < e == para Fruta. Fazemos, então, uma extensão do tipo Fruta para que se conforme com o protocolo Comparable; tipos que seguem esse protocolo podem ser relacionados entre si com os operadores relacionais. A implementação da extensão é a seguinte:

extension Fruta: Comparable {    public static func <(lhs: Fruta, rhs: Fruta) -> Bool {         return lhs.peso < rhs.peso    }    public static func ==(lhs: Fruta, rhs: Fruta) -> Bool {        return lhs.peso == rhs.peso    }}

Vamos explicar os termos: < e == são ambos funções que recebem, como parâmetros, dois objetos do tipo Fruta; lhs e rhs correspondem, respectivamente, a left hand side e right hand side, então podemos fazer uma metáfora: os operadores relacionais têm um objeto na sua mão esquerda e outro, na mão direita, e fazem comparação entre esses dois. O resultado dessas operações são Booleanos, e aí entra a implementação da função. Essas funções retornam o resultado de outras funções (isso mesmo, programação funcional de novo), mas não entre as frutas em si, mas, sim, seus pesos, por isso temos lhs.peso < rhs.peso e lhs.peso == rhs.peso.

Vamos executar nosso código então:

let frutasOrdenadas = frutas.sorted(by: { (f1: Fruta, f2: Fruta) -> Bool in return f1 < f2 })print(emojis(de: frutasOrdenadas))

e obtemos:

As frutas foram ordenadas pelo peso como era de esperar, uma vez que definimos que o peso seria o critério para definir qual fruta é maior e qual é menor.

Você pode estar se perguntando o que é aquela função enorme que passamos para sorted. Não é motivo pra pânico, é só uma outra forma de dizer que vamos ordenar crescentemente. Como? Passamos para sorted uma função com dois parâmetros (f1 e f2) e retorna um Booleano, que é o resultado de f1 < f2.

Comparando paradigmas

Sabemos que Swift não é uma linguagem puramente funcional, então tudo que fizemos até aqui poderia também ser feito por programação imperativa. Vamos, então, comparar os estilos de programação.

Vamos supor que estejamos interessados em ordenar pelo peso as frutas que pesam menos de 100g e são vermelhas. De que formas poderíamos fazer isso?

Se nosso raciocínio é dirigido apenas para a programação imperativa, provavelmente faremos algo como:

var frutasDeInteresse = [Fruta]()for fruta in frutas {    if fruta.peso < 100, fruta.cor == .vermelho {        frutasDeInteresse.append(fruta)    }}let frutasDeInteresseOrdenadas1 = frutasDeInteresse.sorted(by: <)print(emojis(de: frutasDeInteresseOrdenadas1))

e obteremos:

Basicamente, construímos um laço para verificar, dentro dele, as frutas que pesam menos de 100g e são vermelhas; depois disso, aplicamos um algoritmo de ordenação (no caso, o próprio sorted).

Porém, se nosso raciocínio estiver aberto a outros paradigmas, podemos compor um código funcional mais enxuto que o imperativo, como observa-se a seguir:

let frutasDeInteresseOrdenadas2 = frutas    .filter { $0.cor == .vermelho && $0.peso < 100 }    .sorted(by: <)print(emojis(de: frutasDeInteresseOrdenadas2))

O resultado obtido é o mesmo:

mas o código é consideravelmente menor, graças à programação funcional.

Podemos concluir, portanto, que dominar diferentes paradigmas de programação pode ser útil para o desenvolvimento de um código mais enxuto e elegante, através de um raciocínio que considera diferentes estilos de programação para construir o melhor código possível.

--

--