Percursos
/
R
R
/
Exercícios
/
Clube do queijo
Clube do queijo

Clube do queijo

Exercício de aprendizagem

Introdução

O que é a programação funcional?

Para os nossos objetivos, estas são algumas características fundamentais:

  • Funções puras, que mapeiam entradas para saídas de forma consistente e sem efeitos secundários.
  • Dados imutáveis.
  • Funções de primeira classe, que podem ser tratadas como valores como qualquer outro tipo.
  • Funções de ordem superior, que recebem outras funções como argumentos ou como valores devolvidos.

No entanto, o R não é uma linguagem funcional pura (ao contrário de, por exemplo, Haskell).

Vê o R como uma linguagem pragmática, que (cada vez mais) oferece capacidades funcionais quando estas são uma forma limpa e fácil de fazer o trabalho. Em geral, o objetivo é a análise de dados, e a programação é apenas um meio para esse fim.

Pipes

A utilização de pipes |> para ligar funções é uma técnica cada vez mais comum em várias linguagens.

Em resumo:

  • Um resultado à esquerda do pipe torna-se o primeiro argumento da função à direita (em que «direita» ignora quebras de linha opcionais).
  • Quaisquer argumentos restantes podem ser incluídos na chamada à função.
  • Em R, temos de usar o pipe para uma chamada de função, não apenas para um nome, pelo que os parênteses são sempre obrigatórios (isto é diferente de algumas outras linguagens).

Alguns programadores acham útil pronunciar o pipe como «then» ao rever código: faz o passo 1, depois faz o passo 2...

O exemplo abaixo ilustra a sintaxe.

library(stringr)

"Monday-25-February" |> 
  str_split("-") |>     
  unlist() |>               # [1] "Monday"   "25"       "February"
  str_sub(1, 3) |>          # [1] "Mon" "25"  "Feb"
  str_to_upper() |>         # [1] "MON" "25"  "FEB"
  str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"

Este tipo de pipe foi adicionado ao Base R na v4.1.0, e podes vê-lo referido como o «pipe nativo».

O editor RStudio (entre outros) usa Ctrl-Shift-M como atalho de teclado para adicionar um pipe com espaços adequados.

Funções de aplicação

Vimos anteriormente que muitas funções em R são «vetorizadas»: operam sobre vetores inteiros, sem necessidade de escrever ciclos explícitos.

As funções que usam apenas funções vetorizadas são elas próprias provavelmente vetorizadas automaticamente. Num exemplo trivial:

triple <- function(x) return(3 * x)
triple(1:10)
 [1]  3  6  9 12 15 18 21 24 27 30

No entanto, funções mais complexas podem não aceitar entrada vetorial, por exemplo se contiverem ciclos ou blocos if-else.

Precisamos de uma forma mais geral de aplicar funções a vetores (semelhante a map() nalgumas outras linguagens).

Como o R tem uma grande variedade de estruturas de dados na linguagem base, também tem toda uma família de funções *apply() para operar sobre elas. Mais tecnicamente, são frequentemente chamadas «funções de ordem superior» pelos programadores e «funcionais» pelos matemáticos, porque recebem funções como argumentos.

Por agora, considera lapply() e sapply().

lapply()

O «List-apply» foi concebido para trabalhar com entradas de listas, mas os vetores serão silenciosamente convertidos em listas quando necessário.

O resultado é uma lista, pelo que pode ser necessário desempacotá-la para obter um vetor:

h <- function(size) {
    switch(size,
           "small" = 6,
           "medium" = 8,
           "large" = 10
    )
}

v <- c("small", "large", "medium")

h(v) # bad use of an unvectorized function
#> Error in switch(size, small = 6, medium = 8, large = 10) : 
#>   EXPR must be a length 1 vector

lapply(v, h)  # output is a list
#> [[1]]
#> [1] 6

#> [[2]]
#> [1] 10

#> [[3]]
#> [1] 8

lapply(v, h) |> unlist() # output is a vector
#> [1]  6 10  8

Os estudantes familiarizados com o Conceito Switch podem notar que podíamos, em alternativa, substituir o switch escalar pela função vetorizada recode_values() da biblioteca dplyr. A vetorização é boa, e o R moderno fornece várias formas de a conseguir.

library(dplyr)

h <- function(size) {
  recode_values(size,
         "small" ~ 6,
         "medium" ~ 8,
         "large" ~ 10
  )
}

v <- c("small", "large", "medium")
h(v)
#> [1]  6 10  8

sapply()

O «Simplified-apply» faz algo semelhante ao lapply, mas também tenta simplificar o valor devolvido: por exemplo, um vetor com nomes em vez de uma lista. Dependendo do contexto, pode ser necessário usar unname():

sapply(v, h)
 small  large medium 
     6     10      8 

sapply(v, h) |> unname()
#> [1]  6 10  8

A biblioteca purrr

lapply e sapply são simples, familiares para a maioria dos utilizadores de R, e ainda bastante populares para vetores e listas simples.

O Base R também tem muitas outras funções de aplicação. Fornecer uma alternativa mais simples e consistente para este tipo de confusão é um dos principais objetivos do Tidyverse.

Existe agora o pacote purrr, que fornece várias opções adicionais como map(), melhorando a consistência do design e a clareza das mensagens de erro.

Menos limitado pela compatibilidade retroativa do que o Base R, este pacote aproxima significativamente o R da programação funcional moderna encontrada noutras linguagens.

Há muitas funções diversificadas no purrr, que não se resumem facilmente. Os recursos que ajudam a navegar na biblioteca incluem:

As muitas funções map

A função map() aplica uma função a cada elemento da entrada e devolve sempre uma lista.

Até aqui, isto parece o lapply(), apenas com outro nome, e talvez com melhores mensagens de erro.

No entanto, map() é apenas um membro de uma grande família.

Algumas perguntas a considerar ao escolher uma função:

  • O que queres devolver? map() devolve sempre uma lista, mas se quiseres um tipo específico de vetor atómico usa map_chr() para strings, map_int() para inteiros, etc.
  • Precisas de cada índice posicional da entrada? Coloca o prefixo i nos nomes das funções, como em imap().
  • Queres processar várias entradas em paralelo?
    • Substitui map por map2 no nome, como em map2_dbl() para duas entradas (por exemplo, valores e pesos, para calcular uma média ponderada)
    • Substitui por pmap para uma lista de entradas paralelas, como em pmap_chr(). E sim, é possível paralelismo ao nível do hardware.
  • Queres processar todos os elementos da entrada? Se não, considera map_if() para aplicar um filtro de predicado (booleano), map_at() para especificar critérios de índice posicional, head_while() para processar a entrada até um elemento falhar um predicado.
  • Os dados de entrada estão profundamente aninhados? Existem funções para essa situação.
  • Queres que o valor devolvido seja sempre do mesmo tipo que a entrada? Usa as funções modify() em vez de map, ou modify_tree() para aplicação recursiva.

Estas funções têm os dados de entrada como primeiro argumento sempre que possível, para garantir que funcionam bem com pipes.

Além disso, o purrr é (deliberadamente) mais pedante do que o Base R. Se pedires algo específico, as funções devolvem exatamente isso ou param com uma mensagem de erro. Duro, mas muito eficaz a reduzir bugs difíceis de encontrar.

Funções de redução

As funções map têm geralmente um valor devolvido com o mesmo comprimento da entrada.

Algumas outras funções reduzem a dimensionalidade dos dados, por exemplo sum().

sum(1:100)
#> [1] 5050

No caso acima, convertemos um vetor de comprimento 100 num inteiro de comprimento 1. Veremos num futuro Conceito sobre matrizes e arrays que o conceito de redução de dimensionalidade é mais geral.

A função sum() é incorporada (tal como muitas outras funções estatísticas). No entanto, precisamos de uma forma de aplicar funções arbitrárias de redução de dimensionalidade a uma estrutura de dados, usando alguma função de ordem superior equivalente a map().

Noutras linguagens, usam-se nomes de funções como fold, foldl e foldr. Em R, a função relevante é reduce(), que corresponde à conhecida (entre entusiastas de algoritmos) framework MapReduce.

O primeiro argumento é, como habitualmente, os dados de entrada. O segundo argumento é uma função de 2 argumentos: muitas vezes uma função anónima, mas possivelmente tão simples como um operador aritmético como +.

# surround an infix operator with backticks, to use as a function
reduce(1:100, `+`)
#> [1] 5050  # same as sum(1:100)

# anonymous function, alternately add and subtract
reduce(1:10, \(accum, nextval) ifelse(nextval %% 2 == 0, accum + nextval, accum - nextval))
#> [1] 7

Importa se aplicamos a função a partir do início ou do fim da entrada?

Não com operadores associativos como + ou *. Claramente, (1 + 2) + 3 == 1 + (2 + 3).

Em contrapartida, - e / são não associativos:

(1 - 2) - 3
#> [1] -4
1 - (2 - 3)
#> [1] 2

Para funções em que a direção importa, existe um argumento opcional .dir.

# default is .dir = "forward"
reduce(1:10, `-`)
#> [1] -53
reduce(1:10, `-`, .dir = "backward")
#> [1] -5

Tal como com map() e map2(), existe também uma função reduce2() para operar sobre dois vetores de entrada em paralelo.

Em R, gostamos de trabalhar com vetores. Imagina que, em vez de um único valor final, queres ver todos os passos intermédios.

Para isso, as funções correspondentes são accumulate() e accumulate2(). A sintaxe é semelhante a reduce(), com alguns argumentos opcionais extra para especificar o tipo de resultado.

accumulate(1:10, `+`)
#> [1]  1  3  6 10 15 21 28 36 45 55

Alguns tipos comuns de accumulate têm as suas próprias funções. O Conceito Funções de Vetores abordou cumsum() e cumprod() no Base R, e a biblioteca dplyr tem cummean() para médias numéricas, cumall() e cumany() para vetores lógicos.

Funções de filtragem

Frequentemente, queremos incluir (ou excluir) elementos da entrada que correspondem a um determinado predicado.

No Conceito Filtragem de Vetores vimos uma forma idiomática de fazer isto.

v <- 1:3
v[v >= 2]
#> [1] 2 3

Isto é muito usado no R tradicional, mas encaixa mal nos pipelines funcionais modernos.

Consequentemente, o purrr fornece um conjunto de funções de predicado, que seguem a convenção habitual de argumentos f(input, func, ...) para funcionar com pipes.

Os nomes são um pouco diferentes da maioria das outras linguagens (nomes como filter e drop já significam algo completamente diferente em R).

As funções correspondentes do purrr incluem keep() e discard(), além de várias outras.

Temos também every(), some() e none(), para substituir all() e any() ao devolver um único valor lógico.

Instruções

Estamos a criar um clube de queijo, que vai usar ML para selecionar novos queijos a oferecer aos nossos clientes amantes de queijo, com base no seu historial e nos seus gostos.

Os novos membros têm de preencher um inquérito inicial para reunirmos alguns dados básicos com que começar. A partir daí, verificou-se que existe um subconjunto de clientes enfáticos cujas críticas carecem de subtileza. Como isto pode acabar por enviesar de forma irrevogável um algoritmo mais subtil, existe um algoritmo à parte, criado para responder às suas necessidades. Pedem-te que forneças algumas funções auxiliares para tratar os seus dados.

Note

Embora possa haver diferentes formas de resolver as tarefas seguintes, cada uma pode ser resolvida com, no máximo, duas funções de ordem superior.

1. Classifica os clientes

O sistema de avaliação tem uma base de cinco estrelas, que consiste simplesmente nos números inteiros 1:5. Os clientes enfáticos só dão avaliações de 1 ou 5, e queremos saber se um cliente tem este comportamento.

Implementa all_15(), que recebe um vetor de avaliações e devolve true se todas as avaliações forem 1 ou 5, e false caso contrário.

all_15(c(2, 3, 4, 4, 1))
#> [1] FALSE

all_15(c(1, 5, 5, 1, 5))
#> [1] TRUE

2. Nomeia os clientes

Precisamos de associar o nome (ou ID) do cliente às suas avaliações.

Implementa name_customers(), que recebe um vetor de nomes e uma lista de avaliações e os "junta" numa lista de listas.

Os elementos internos das listas devem ter os nomes name e rating.

names <- c("c1", "c2")
ratings <- list(c(2, 3, 5), c(1, 1, 5))
name_customers(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c1"

#> [[1]]$rating
#> [1] 2 3 5

#> [[2]]
#> [[2]]$name
#> [1] "c2"

#> [[2]]$rating
#> [1] 1 1 5

3. Separa os clientes enfáticos

Precisamos de separar os clientes mais enfáticos dos restantes.

Implementa emphatics(), que recebe os clientes e as avaliações. Devolve uma lista restrita aos clientes que usam apenas avaliações de 1 ou 5 estrelas.`

# names and ratings as in the task 2 example

emphatics(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c2"

#> [[1]]$rating
#> [1] 1 1 5

4. Converte as avaliações em binário

Como os clientes enfáticos só usam avaliações de 1 e 5, será computacionalmente mais conveniente converter estas em 0 e 1.

Implementa to_binary(), que recebe um vetor de avaliações enfáticas. Devolve avaliações binárias, em que 1 foi convertido em 0 e 5 foi convertido em 1.

to_binary(c(1, 1, 5, 5, 1))
#> [1] 0 0 1 1 0

5. Calcula as avaliações médias ao longo do tempo

Queres continuar a melhorar as recomendações para todos os clientes individualmente (não apenas para os enfáticos), à medida que compreendes melhor os seus gostos.

Implementa satisfactions(), que recebe um vetor de avaliações. Devolve um vetor de avaliações médias acumuladas até esse momento. Arredonda os resultados a 2 casas decimais.

Esperemos que os números continuem a crescer, para clientes felizes!

satisfactions(c(1, 2, 2, 3))
#> [1] 1.00 1.50 1.67 2.00
Editar via GitHub A ligação abre numa nova janela ou separador
R Exercism

Estás pronto para começar Clube do queijo?

Inscreve-te no Exercism para aprenderes e dominares R com 21 conceitos111 exercícios, e mentoria humana real, tudo grátis.