Capítulo 11 Estruturas de controle do fluxo de execução e funções

Os conteúdos desta introdução e da seção 11.1 podem ser visualizados neste vídeo.

Nos capítulos anteriores, utilizamos dezenas de funções para realizar os processamentos desejados com os dados. O uso de funções é um dos principais instrumentos para modularizar uma linguagem de programação.

As funções isolam um código que podem ser executado inúmeras vezes. Elas criam uma interface para o código, por meio de zero ou mais argumentos, que permite ao usuário utilizar a função sem conhecer os detalhes de como ela é executada.

Funções são elas próprias objetos. Assim podem ser tratadas como quaisquer outros objetos no R.

Ao escrevermos uma função, frequentemente diversas estruturas de controle do fluxo de execução dos comandos são utilizadas. Estruturas de controle frequentemente usadas são:
- if-else: testa uma condição e executa um código dependendo do resultado do teste;
- for: executa uma sequência de comandos (laço - loop) um certo número de vezes;
- while: executa um laço enquanto uma condição for verdadeira;
- repeat: executa um laço um número infinito de vezes (para parar é preciso usar break);
- break: interrompe a execução de um laço;
- next: pula uma iteração de um laço.

Essas estruturas podem ser utilizadas também fora de funções, em sessões interativas com o R. Vamos entendê-las antes de entrarmos em funções.

11.1 if-else

Como os termos indicam, uma estrutura do tipo if-else permite a execução de um ou outro código, dependento de uma condição ser verdadeira ou falsa.

A sintaxe básica é mostrada abaixo. Observem o uso de uma expressão lógica entre parênteses e chaves envolvendo os códigos executados como cada resultado da expressão lógica.

if (expressão lógica) { # se expressão lógica for verdadeira
      # faça isso
} else {
      # senão faça outra coisa
}

É possível não termos a parte do else, como mostrado abaixo. Nesse caso, um código será executado se a expressão lógica for verdadeira. Se a expressão não for verdadeira, então nada será executado.

if (expressão lógica) { # se expressão lógica for verdadeira
      # faça isso
} # se não, não faça nada

É possível termos ifs aninhados dentro de outros ifs quantas vezes forem necessárias, como abaixo.

if (expressão lógica) { # se expressão lógica for verdadeira
      # faça isso
} else if (outra expressão lógica) {
      # se outra expressão for verdadeira, faça isso
} else {
      # se outra expressão não for verdadeira, faça isso
}

As expressões lógicas são construídas por meio dos operadores == (igual), > (maior), < (menor), >= (maior ou igual), <= (menor ou igual), != (diferente), ou combinações de expressões lógicas por meio dos operadores booleanos “OU” (|), “E” (&), e “Não” (!). É recomendável isolar cada expressão lógica por parênteses para facilitar a leitura do código.

Vamos ver um exemplo:

## [1] "1 é ímpar"

O primeiro comando simula o lançamento de um dado, gerando aleatoriamente um número inteiro de 1 a 6 e armazena o resultado no objeto x.

O comando if avalia a expressão lógica entre parênteses. Essa expressão testa se o resto da divisão de x por 2 é igual a zero. O símbolo de porcentagem duplicado realiza a operação para obter o resto da divisão do número à esquerda pelo número à direita.

Se o resto da divisão for igual a zero, então será executada a função print, que imprime na tela que o valor de x é par.

Se o resto da divisão não for igual a zero, então será executada a função print, que imprime na tela que o valor de x é ímpar.

Vamos utilizar a janela de edição de arquivos do RStudio (área superior à esquerda) para executarmos uma série de comandos simultaneamente. A figura 11.1 mostra os comandos acima digitados na área de edição de arquivos no RStudio.

Área superior à esquerda do RStudio com o código mostrado anteriormente.

Figura 11.1: Área superior à esquerda do RStudio com o código mostrado anteriormente.

Se selecionarmos os comandos no arquivo (ou parte deles) e, em seguida, clicarmos no botão Run (figura 11.2), os comandos selecionados serão executados na console do RStudio.

Área de script do RStudio. Ao selecionarmos os comandos e clicarmos no botão Run, os comandos serão executados na console do RStudio.

Figura 11.2: Área de script do RStudio. Ao selecionarmos os comandos e clicarmos no botão Run, os comandos serão executados na console do RStudio.

11.2 Laços com for (repetições)

Os conteúdos desta seção e das seções 11.3, 11.4, 11.5 e 11.6 podem ser visualizados neste vídeo.

Frequentemente, ao desenvolvermos um script no R, precisamos executar uma sequência de comandos um certo número de vezes. Essa estrutura de repetições de uma sequência de comandos é chamada de laço (loop em inglês). Cada uma das repetições é chamada de iteração. O conjunto de comandos é executado uma vez e, em seguida, o fluxo de execução retorna ao primeiro comando do conjunto e o processo continua um certo número de vezes.

Suponhamos que queiramos somar os elementos de um vetor numérico e não tivéssemos a função sum à nossa disposição. Uma maneira de realizar essa soma no R é usar uma estrutura como no exemplo a seguir:

## [1] "i = 1 , x[1] = 11 , soma = 11"
## [1] "i = 2 , x[2] = 12 , soma = 23"
## [1] "i = 3 , x[3] = 13 , soma = 36"
## [1] 36

Os laços formados com a palavra chave for executam os comandos entre chaves quantas vezes a expressão entre parênteses especificar. No exemplo acima, criamos um vetor (x) com os números inteiros de 11 a 13. Depois iniciamos a variável soma com o valor 0 e n com o comprimento do vetor x, nesse caso, n = 3. O comando soma = soma + x[i] e print (…) serão executados para cada valor de i, que assume em sequência os valores de 1 até 3. Assim a variável soma vai acumulando sucessivamente os valores do vetor x.

Existe uma função chamada seq_len que substitui a expressão 1:length. Assim o laço for acima poderia ser escrito como:

11.3 Laços com while

Laços do tipo while possuem a seguinte estrutura:

while ( expressão lógica ) { # se expressão lógica for verdadeira
      # sequência de comandos }

Inicialmente, a expressão lógica é avaliada. Se ela for verdadeira, a sequência de comandos entre chaves é executada. Em seguida a expressão lógica é novamente avaliada e assim por diante, até que a expressão lógica seja falsa e então o fluxo de execução sai do laço.

Vamos reescrever o código para calcular a soma dos elementos de um vetor numérico da seção 11.2, utilizando o laço com while, mostrado abaixo.

## [1] "i = 1 , x[1] = 11 , soma = 11"
## [1] "i = 2 , x[2] = 12 , soma = 23"
## [1] "i = 3 , x[3] = 13 , soma = 36"
## [1] 36

Observem a expressão lógica i <= n que é testada a cada iteração. Inicialmente i é igual a 1 e é aumentado de uma unidade a cada iteração. Quando i atinge um valor maior que o comprimento de x, o laço é interrompido.

Nesse exemplo, o laço com for é mais simples. Em laços com while, é preciso tomar cuidado para que a expressão lógica não seja sempre verdadeira. Caso a expressão lógica nunca se torne falsa, o laço nunca terminará.

11.4 Laços com repeat e break

Um laço com repeat no R é executado sucessivas vezes até ser interrompido pelo comando break.

Vamos novamente reescrever o código para calcular a soma dos elementos de um vetor numérico da seção 11.2, utilizando agora o laço com repeat:

## [1] "i = 1,  x[1] = 11, soma = 11"
## [1] "i = 2,  x[2] = 12, soma = 23"
## [1] "i = 3,  x[3] = 13, soma = 36"
## [1] 36

O fluxo do laço repeat é parecido com o do laço while. A diferença é que a condição é testada em qualquer ponto do laço, não antes da execução do laço. Inicialmente i é igual a 1 e é aumentado de uma unidade a cada iteração. Quando i atinge um valor maior que o comprimento de x, a condição i > n é verdadeira e o comando break é executado, interrompendo o laço.

O comando break, como o nome indica, interrompe a execução de um laço sempre que for executado.

Os códigos dos laços for, while e repeat mostrados nesses exemplos são apenas para fins didáticos. Nós poderíamos obter a soma dos elementos do vetor x, usando a função sum dessa forma:

## [1] 36

11.5 next

O comando next é usado para interromper uma iteração de um laço.

Vamos supor que temos um vetor com os números inteiros de 10 a 30 e que desejamos somar somente os elementos de x a partir do décimo-primeiro elemento. O código a seguir realiza essa soma:

## [1] "i = 11,  x[11] = 20, soma = 20"
## [1] "i = 12,  x[12] = 21, soma = 41"
## [1] "i = 13,  x[13] = 22, soma = 63"
## [1] "i = 14,  x[14] = 23, soma = 86"
## [1] "i = 15,  x[15] = 24, soma = 110"
## [1] "i = 16,  x[16] = 25, soma = 135"
## [1] "i = 17,  x[17] = 26, soma = 161"
## [1] "i = 18,  x[18] = 27, soma = 188"
## [1] "i = 19,  x[19] = 28, soma = 216"
## [1] "i = 20,  x[20] = 29, soma = 245"
## [1] "i = 21,  x[21] = 30, soma = 275"
## [1] 275

No laço acima, enquanto i < 11, o comando next é executado, interrompendo a iteração corrente e indo para o início da próxima iteração. Então, para i = 1, 2, até 11, os dois comandos após o if não são executados.

Quando i for igual a 11 em diante, o comando next não é executado e os comandos após o if são executados. A variável soma ao final vai ser igual à adição dos números do vetor a partir do décimo-primeiro elemento.

Novamente esse exemplo visa somente a ilustrar o uso do comando next. O comando a seguir obtém o mesmo resultado do laço acima.

## [1] 275

Assim, antes de utilizarmos construções mais complexas, usando if ou laços, tentemos verificar se não é possível obter a funcionalidade que desejamos por meio de funções já disponíveis no R.

11.6 Laços aninhados

Laços podem ser aninhamos quantas vezes forem necessárias, embora, normalmente, mais de três níveis de aninhamento seja raramente necessário.

O código abaixo calcula a soma dos elementos de uma matriz, onde no laço externo, i percorre as linhas da matriz e, no laço interno, j percorre as colunas da matriz.

Inicialmente criamos uma matriz com 2 linhas e 3 colunas, contendo os números 1 a 3 na primeira linha e os números 4 a 6 na segunda linha.

##      [,1] [,2] [,3]
## [1,]    1    3    5
## [2,]    2    4    6

Em seguida, codificamos o laço que realiza a soma dos elementos da matriz.

## [1] "i =  1 , soma acumulada =  9"
## [1] "i =  2 , soma acumulada =  21"
## [1] 21

Primeiramente inicializamos a variável soma com 0, n_linhas com o número de linhas da matriz x (2), e n_colunas com o número de colunas da matriz x (3).

Ao entrarmos no laço externo, i = 1. Agora, o laço interno vai ser então executado com os valores de i = 1 e j variando de 1 a 3. Então, como vimos nos laços anteriores, soma vai assumir a soma dos valores da primeira linha da matriz, x[1,1] + x[1, 2] + x[1,3] e o fluxo de execução sai do laço interno e passa ser o próximo comando após o laço interno. A função print imprime os valores correntes de i e soma.

Em seguida, o fluxo retorna para o laço mais externo, i passa a ser 2 e o laço interno será executado mais uma vez, agora com i = 2 e j variando de 1 a 3 novamente. No laço interno, vamos somando a variável soma sucessivamente com os valores da segunda linha de x. Então soma passa a ser 9 + 2 + 4 + 6 = 21. Ao somarmos o terceiro elemento da segunda linha, abandonamos o laço interno e são impressos os valores correntes de i e soma.

Como i percorreu os valores de 1 a 2, o laço externo não será mais executado. O fluxo de execução passa para a próxima a linha após o laço externo e é exibido o valor final de soma (21).

Outra vez, esse exemplo visa somente a ilustrar o uso de laços aninhados. A soma acima seria obtida de maneira mais fácil com a função a função sum.

## [1] 21

11.7 Estrutura básica de uma função

O conteúdo desta seção pode ser visualizado neste vídeo.

Nós temos usado muitas funções ao longo desses vídeos. As funções conferem modularidade a uma linguagem de programação e permite a reutilização de código.

Em vez de o usuário ter que escrever códigos extensos para realizar uma análise de seu interesse, basta combinar funções escritas por terceiros que, em conjunto, produzem os resultados desejados. Dessa forma, os usuários não precisam conhecer os detalhes de como cada função foi implementada, basta saber as funcionalidades que ela proporciona e como ela deve ser chamada.

Uma função embute dentro dela uma sequência de comandos que realiza a funcionalidade sugerida pelo seu nome. Uma função bem simples é apresentada abaixo.

## [1] "Alô mundo!"

Para criarmos uma função, usamos a palavra chave function seguida de uma lista de argumentos formais entre parênteses e separados por vírgulas. O código que é executado cada vez que a função é chamada aparece entre chaves. No exemplo acima, a função simplesmente imprime na tela a expressão “Alô mundo!”. Para executarmos a função, usamos o nome dela com a lista de argumentos entre parênteses. Nesse exemplo, a função não possui argumentos.

Vamos criar uma função mais complexa, que realiza a soma dos elementos de um vetor numérico.

O nome dado para a função é soma.vetor. Essa função possui dois argumentos formais: o primeiro argumento é o vetor cujos elementos desejamos somar, o segundo argumento define se é para remover os elementos do vetor que são NA.

Cada argumento formal de uma função possui um nome, o qual pode ser usado para especificar que valor o argumento irá assumir na chamada de uma função. Ao segundo argumento na.rm, foi atribuído o valor default TRUE.

A primeira parte da função soma.vetor testa se o vetor é numérico ou inteiro. Se não for, uma mensagem é impressa na tela e a função retorna, sem executar o restante do código.

Se o vetor for numérico ou inteiro, a execução da função continua.

O comando seguinte verifica o valor do argumento na.rm.

Se o valor de na.rm for igual a TRUE, então todos os elementos do vetor que são NA são removidos por meio do comando vetor[!is.na(vetor)], e o resultado é armazenado no objeto vetor_sem_NA.

Se o valor de na.rm for igual a FALSE, então, se houver algum valor NA no vetor, a função irá encerrar, retornando o valor NA.

Finalmente, se somente houver valores numéricos no vetor, ou na.rm é igual a TRUE, o trecho de código seguinte percorre todos os elementos de vetor_sem_NA e vai acumulando a soma dos elementos no objeto soma, que é retornado ao final da execução da função.

O valor da última expressão executada no código da função é o valor de retorno da mesma.

A figura 11.3 mostra o código da função soma.vetor na janela de edição de arquivos do RStudio.

Código da função soma.vetor na janela de edição de arquivos do RStudio.

Figura 11.3: Código da função soma.vetor na janela de edição de arquivos do RStudio.

Poderíamos selecionar todas as linhas que definem a função e clicar no botão Run para compilar a função e armazená-la como um objeto no workspace. Porém vamos salvar o script da função em um arquivo e aprendermos como carregar uma função no R a partir de um arquivo que contém o seu código. A figura 11.4 mostra a aba Files do RStudio (área inferior à direita). Nessa figura, a pasta selecionada é temp e ela mostra os arquivos dessa pasta.

Aba Files do RStudio.

Figura 11.4: Aba Files do RStudio.

Na aba Files, é possível navegar no sistema de arquivos de seu computador, remover e renomear arquivos, criar pastas etc. Além disso, ao clicarmos no icone More (figura 11.5), é possível, entre outras coisas, fazermos com que a pasta exibida na aba Files passe a ser o diretório corrente, ou irmos para a pasta que foi estabelecida anteriormente como diretório corrente.

Aba Files do RStudio com a opção para especificarmos a pasta mostrada em Files como o diretório corrente.

Figura 11.5: Aba Files do RStudio com a opção para especificarmos a pasta mostrada em Files como o diretório corrente.

Ao clicarmos no ícone indicado pela seta verde na figura 11.6 na área de edição de arquivos, poderemos salvar o script da função soma.vetor em um arquivo. Poderemos então selecionar a pasta e o nome do arquivo que será gravado (figura 11.7). Arquivos contendo um script do R, por convenção, devem possuir a extensão R.

Ícone para salvar o script em um arquivo no RStudio.

Figura 11.6: Ícone para salvar o script em um arquivo no RStudio.

Escolhendo o nome e o local do arquivo do conteúdo da área de script do RStudio.

Figura 11.7: Escolhendo o nome e o local do arquivo do conteúdo da área de script do RStudio.

Ao clicarmos em Save na figura 11.7, o arquivo será gravado na pasta especificada. A figura 11.8 mostra a pasta temp com o arquivo somaVetor.R recém gravado.

Aba Files com o arquivo do script gravado.

Figura 11.8: Aba Files com o arquivo do script gravado.

A figura 11.9 mostra novamente a área de edição de arquivos do RStudio, agora com o nome do arquivo fornecido anteriormente.

Área de script, mostrando o nome do arquivo do script.

Figura 11.9: Área de script, mostrando o nome do arquivo do script.

Vemos que o arquivo está salvo, mas a função soma.vetor ainda não está disponível no ambiente de trabalho. Poderíamos compilá-la, selecionando todo o script que a define e clicando no botão Run, mas vamos fazer de outra forma. Vamos testar a função soma e aprender mais algumas particularidades das funções.

Para compilar a função, vamos marcar a opção Source On Save (figura 11.10) e, em seguida, salvamos o arquivo. O arquivo será executado por meio da função source. O comando e o resultado da execução do arquivo são mostrados na console do RStudio (figura 11.11).

Área de script com a opção de executar o script ao salvar o arquivo.

Figura 11.10: Área de script com a opção de executar o script ao salvar o arquivo.

Salvando o script com a opção Source on save selecionada fará com que o script seja compilado e o resultado mostrado na console do RStudio.

Figura 11.11: Salvando o script com a opção Source on save selecionada fará com que o script seja compilado e o resultado mostrado na console do RStudio.

Se houver erros, as mensagens serão exibidas. Nesse exemplo, nenhum resultado é mostrado na console, mas a função soma.vetor foi criada (vejam na aba Environment) e agora ela pode ser utilizada.

Nos comandos abaixo, executados na console do RStudio, criamos um vetor x e, em seguida, executamos a função soma.vetor, atribuindo x ao argumento vetor da função.

## [1] 210

Não foi necessário especificar o valor do argumento na.rm, porque, se o mesmo não for especificado, o valor assumido será TRUE, conforme a definição da função.

No exemplo abaixo, como desejamos incluir elementos NA na soma, então devemos especificar o valor de na.rm como FALSE. O resultado da soma do vetor x obviamente será NA, já que a soma de qualquer valor com NA será NA.

## [1] NA

11.8 Pareamento dos argumentos

Os conteúdos desta seção e da seção 11.9 podem ser visualizados neste vídeo.

A função formals(nome_da_funcao) fornece os parâmetros formais da função. No comando a seguir, obtemos os dois argumentos da função soma.vetor, vetor e na.rm, sendo TRUE o valor default para na.rm.

## $vetor
## 
## 
## $na.rm
## [1] TRUE

Na chamada da função soma.vetor abaixo, não especificamos o nome do primeiro argumento; simplesmente passamos o vetor x. Não ocorreu erro, porque o R associa uma posição a cada argumento formal de acordo com a definição da função. Assim o primeiro argumento na chamada será atribuído a vetor, o segundo argumento a na.rm.

## [1] 210
## [1] NA

Podemos misturar na chamada de uma função argumentos de acordo com a posição ou pelo nome.

## [1] NA

Se um argumento que não possui valor padrão não for especificado na chamada, ocorrerá um erro. Por exemplo, se não especificarmos o valor de vetor na chamada da função soma.vetor:

ocorrerá o seguinte erro:

Error in soma.vetor() : argumento “vetor” ausente, sem padrão

Caso o vetor não seja numérico, a função imprimirá uma mensagem e retornará o valor NULL.

## [1] "vetor não é numérico"
## NULL

Voltando à questão da notação posicional para a especificação dos argumentos de uma função, consideremos a função rnorm(n, mean = 0, sd = 1). Essa função gera n números aleatórios de acordo com uma distribuição normal com argumentos formais mean (média) e sd (desvio padrão).

Ao especificarmos os argumentos pelo nome, a ordem dos argumentos é irrelevante. Assim rnorm(n=10, mean = 20, sd = 4) é equivalente a rnorm(mean = 20, sd = 4, n=10), por exemplo.

Quando um argumento é especificado pelo nome, ele é “removido” da lista de argumentos e os argumentos não nomeados restantes são pareados na ordem em que eles são listados na definição da função.

Assim, na chamada da função rnorm(10, sd = 8, 70), sd é removido da lista de argumentos não nomeados, o primeiro valor, 10, corresponde ao valor de n, e o valor 70 corresponde ao argumento mean.

Não é recomendável misturar chamadas com argumentos nomeados e de acordo com a posição, pois isso pode gerar confusão e eventualmente a resultados não esperados.

O R também realiza um pareamento parcial dos argumentos. No comando a seguir, a função rnorm foi chamada com os argumentos me = 20 e s = 4. O R reconheceu que me correspondia ao argumento mean e s ao argumento sd.

## [1] 22.84470 23.32186

11.9 O uso do argumento …

O argumento … é usado para indicar que existe um número variável de argumentos que usualmente são passados para outras funções. Ele é frequentemente usado quando uma função estende outra função agregando alguma funcionalidade extra e depois chamando a função estendida. Isso evita que, ao definir uma função, tenhamos que incluir toda a lista de argumentos da função que será estendida.

Vamos ver um exemplo. O código abaixo cria uma amostra aleatória (y) com tamanho 2000 de uma distribuição normal padrão e, em seguida, plota o histograma de y (figura 11.12).

Histograma de frequência de uma amostra aleatória com 2000 elementos de uma distribuição normal padrão.

Figura 11.12: Histograma de frequência de uma amostra aleatória com 2000 elementos de uma distribuição normal padrão.

A função hist gera por padrão um histograma de frequência. Vide a ajuda dessa função. Para gerar um histograma de densidade de frequência relativa, é necessário especificar o argumento freq = FALSE. Vamos definir uma função que gera por padrão o histograma de densidade de frequência relativa. A função meu_hist abaixo faz exatamente isso. Ela recebe um vetor (dados) e chama a função hist com o argumento freq = FALSE. Observem o argumento “…” na definição da função e na chamada de hist. Isso ocorre para evitar de ter que colocar todos os argumentos que são utilizados pela função hist e pelas funções que hist eventualmente chama.

Ao executarmos a função meu_hist, especificamos, além do vetor de dados (y), os rótulos dos eixos x e y. Os argumentos xlab e ylab serão repassados para a função hist (figura 11.13).

Histograma de densidade de frequência relativa de uma amostra aleatória com 2000 elementos de uma distribuição normal padrão.

Figura 11.13: Histograma de densidade de frequência relativa de uma amostra aleatória com 2000 elementos de uma distribuição normal padrão.

Funções genéricas usam o argumento “…”, de modo que argumentos extras possam ser repassados para funções chamadas internamente.

A função str exibe a estrutura interna de um objeto do R. Se chamarmos str com uma função, ela irá exibir os argumentos da função. Abaixo, ela foi executada com o argumento paste.

## function (..., sep = " ", collapse = NULL)

A função paste concatena vetores após convertê-los para character. Veja uma execução da função paste a seguir. O resultado foi uma string formada pela concatenação dos elementos 3, 4, 5 e 6, separados por " ".

## [1] "3 4 5 6"

Na definição da função paste, é preciso inserir o argumento … no início, porque não sabemos de antemão quantos vetores de caracteres serão passados para a função.

Observação:
Quaisquer argumentos que aparecem depois de … na lista de argumentos têm que ser nomeados explicitamente e não podem ser parcialmente pareados ou pareados pela posição. Por exemplo, na chamada de paste abaixo, foi gerada uma string com os elementos separados por “;”.

## [1] "3; 4; 5; 6"

Se tentarmos um pareamento parcial do argumento sep, não obteremos o resultado desejado:

## [1] "3 4 5 6 ; "

Nesse caso, a função paste não reconhece “se” como o argumento sep. O “;” será tratado como mais um elemento a ser concatenado, e os elementos são separados por um espaço, o separador padrão.