Avançar para o conteúdo
Imagem com logotipo, contendo link para a página inicial
  • United Stated of America flag, representing the option for the English language.
  • Bandeira do Brasil, simbolizando a opção pelo idioma Português do Brasil.

Aprenda Programação: Subrotinas (Funções e Procedimentos)

Exemplos de uso de subrotinas em Python, Lua, GDScript e JavaScript.

Créditos para a imagem: Imagem criada pelo autor usando o programa Spectacle.

Pré-Requisitos

Na introdução sobre ambientes de desenvolvimento, indiquei Python, Lua e JavaScript como boas escolhas de linguagens de programação para iniciantes. Posteriormente, comentei sobre GDScript como opção para pessoas que tenham interesse em programar jogos digitais ou simulações. Para as atividades de introdução a programação, você precisará de, no mínimo, um ambiente de desenvolvimento configurado em uma das linguagens anteriores.

Caso queira experimentar programação sem configurar um ambiente, você pode usar um dos editores online que criei:

Contudo, eles não possuem todos os recursos dos interpretadores para as linguagens. Assim, cedo ou tarde, você precisará configurar um ambiente de desenvolvimento. Caso precise configurar um, confira os recursos a seguir.

Assim, se você tem um Ambiente Integrado de Desenvolvimento (em inglês, Integrated Development Environment ou IDE) ou a combinação de editor de texto com interpretador, você está pronto para começar. Os exemplos assumem que você saiba executar código em sua linguagem escolhida, como apresentado nas páginas de configuração.

Caso queira usar outra linguagem, a introdução provê links para configuração de ambientes para as linguagens C, C++, Java, LISP, Prolog e SQL (com SQLite). Em muitas linguagens, basta seguir os modelos da seção de experimentação para adaptar sintaxe, comandos e funções dos trechos de código. C e C++ são exceções, por requerem o uso de ponteiros para acesso à memória.

Subrotinas, Modularização e Reuso de Código

Em estruturas de condicionais, comentou-se sobre a conveniência de pensar no código-fonte de um programa como composição de blocos. A partir deste tópico, você poderá começar a criar seus próprios blocos de código reusáveis na forma de subrotinas.

Praticamente toda linguagem de programação (de uma forma ou outra) fornece recursos para a criação de funções, procedimentos e/ou métodos. Embora cada um dos termos seja um pouco diferente e tenha suas particularidades, pode-se usar subrotina como termo genérico para qualquer um deles.

Uma subrotina (ou subprograma) é um bloco de código que, quando chamado, executa uma seqüência de instruções definida por uma programadora ou por um programador. Subrotinas são úteis porque permitem definir uma única vez um bloco de código que desempenhe um mesmo processamento ou mesma operação, mas que pode ser usado sempre que necessário.

Decompor um programa em subrotinas é uma boa prática de programação, chamada de decomposição funcional. Um programa decomposto em subrotinas é dito modularizado, pois a prática chama-se modularização. Como é possível usar subrotinas várias vezes após a definição, modularização promove reuso de código. Com boas escolhas para nomes, subrotinas também podem facilitar a leitura e o entendimento de um código-fonte, reduzindo a necessidade de comentários.

Funções

Funções talvez sejam o tipo mais tradicional de subrotinas, pois são semelhantes a funções na Matemática. Contudo, funções em programação são mais flexíveis e potencialmente mais poderosas que as matemáticas.

Na Matemática, uma função é uma relação que calcula um resultado a partir de parâmetros. Funções possuem um domínio, que especifica um conjunto de valores aceitos, e uma imagem, que especifica possíveis resultados. Por exemplo, a função módulo (não relacionado com modularização nem com resto de divisão) ou valor absoluto mapeia números reais para os respectivos valores não negativos.

Pode-se usar a função quantas vezes forem necessárias, para qualquer valor considerado válido (no caso, qualquer número real).

Em programação, uma função é um bloco de código que realiza um processamento e retorna um resultado. Uma função pode ou não ter parâmetros. Quando existirem, cada parâmetro terá um tipo esperado para especificação de valores potencialmente válidos. A implementação pode impor limitações adicionais para valores aceitos ou restritos, determinando quais são os valores válidos.

Assim, de forma geral, uma função possui uma estrutura semelhante com o pseudocódigo a seguir.

funcao nome_da_funcao(parametro1: tipo1, parametro2: tipo2, ...) tipo retorno
inicio
    // Variáveis locais
    tipo retorno resultado
    // ...
    // Implementação
    // ...
    retorne resultado
fim

A sintaxe exata tender a variar entre linguagens de programação. Por exemplo, uma possível implementação da função módulo em JavaScript poderia corresponder ao código a seguir. Para explorá-lo, você pode usar o console embutido de seu navegador (atalho: F12). Caso prefira testar a implementação em Python, Lua ou GDScript, o código também está disponível na aba correspondente.

function f(x) {
    let y
    if (x >= 0) {
        y = x
    } else {
        y = -x
    }
    return y
}
def f(x):
    if (x >= 0):
        y = x
    else:
        y = -x
    return y
function f(x)
    local y
    if (x >= 0) then
        y = x
    else
        y = -x
    end
    return y
end
func f(x):
    var y
    if (x >= 0):
        y = x
    else:
        y = -x
    return y

Após definida a função f(), pode-se usá-la sempre que se quiser obter o valor não-negativo correspondente ao valor passado como parâmetro para a função. Ou seja, ao invés de repetir o código, basta usar a função criada. O uso de uma função chama-se chamada de função.

Para o próximos exemplo, o código da definição deve aparecer antes da chamada. Ele deve existir apenas uma vez por programa.

// ...

console.log(f(1))
console.log(f(0))
console.log(f(-1))
# ...

print(f(1))
print(f(0))
print(f(-1))
-- ...

print(f(1))
print(f(0))
print(f(-1))
extends Node

# ...

func _ready():
    # ...

    print(f(1))
    print(f(0))
    print(f(-1))

Quando se faz uma chamada de função, o programa realiza um salto para o código da definição, realiza o processamento definido, obtém o resultado para, em seguida, realizar um salto para o código que chamou a função para fornecer o resultado calculado. Em linguagens de programação, todos os passos anteriores são feitos pelo interpretador ou compilador. Para a programadora ou para o programador, basta chamar a função usando o nome.

Assim, ao invés de um nome genérico, é conveniente adotar nomes auto-explicativos para subrotinas. Embora o nome seja indiferente para o compilador ou interpretador (embora deva ser único), nomes claros e descritivos são mais apropriados para as pessoas que programam o sistema.

Por exemplo, ao invés de f(), uma função que calcula o valor absoluto de um número poderia chamar-se valor_absoluto(). Da mesma forma, ao invés de chamar-se x, o parâmetro poderia chamar-se numero ou valor. O resultado calculado poderia chamar-se resultado.

// Definição.
function valor_absoluto(valor) {
    let resultado
    if (valor >= 0) {
        resultado = valor
    } else {
        resultado = -valor
    }
    return resultado
}

// Uso.
console.log(valor_absoluto(1))
console.log(valor_absoluto(0))
console.log(valor_absoluto(-1))
# Definição.
def valor_absoluto(valor):
    if (valor >= 0):
        resultado = valor
    else:
        resultado = -valor
    return resultado

# Uso.
print(valor_absoluto(1))
print(valor_absoluto(0))
print(valor_absoluto(-1))
-- Definição.
function valor_absoluto(valor)
    local resultado
    if (valor >= 0) then
        resultado = valor
    else
        resultado = -valor
    end
    return resultado
end

-- Uso.
print(valor_absoluto(1))
print(valor_absoluto(0))
print(valor_absoluto(-1))
extends Node

# Definição.
func valor_absoluto(valor):
    var resultado
    if (valor >= 0):
        resultado = valor
    else:
        resultado = -valor
    return resultado

func _ready():
    # Uso.
    print(valor_absoluto(1))
    print(valor_absoluto(0))
    print(valor_absoluto(-1))

O resultado é um código mais simples de ler e entender.

Além das diferenças de sintaxe, existem linguagens de programação (como C, C++, JavaScript e GDScript) que definem que uma função deva retornar um único valor. Outras linguagens de programação (como Lua e Python) permitem que uma função retorne múltiplos valores de uma vez (por exemplo, return x, y).

Na prática, linguagens com limitações de retorno um valor por função podem definir tipos compostos de dados para retornar vários valores em uma única variável. Assim, a limitação será mais branda após a introdução de estruturas de dados e registros.

Procedimentos

Na Matemática, cria-se se funções para cálculos de resultados. O objetivo sempre é a obtenção do resultado calculado.

Em programação, algumas vezes o objetivo para a definição de uma subrotina é um efeito colateral (side effect) ao invés de um resultado. Efeitos colaterais podem incluir, dentre outros, mudança de estado (valores de variáveis) em um programa, escrita de dados, leitura de dados, espera por um intervalo de tempo.

Para distingüir subrotinas que retornam valores de subrotinas cujo interesse é o efeito colateral resultante, pode-se usar o termo função para a primeira categoria e procedimento para a segunda. Contudo, a distinção costumam ser mais teórica que prática; a terminologia pode variar entre autores. De fato, existem linguagens de programação que utilizam o termo função para qualquer subrotina (ou função vazia ou função void para procedimentos). Da mesma forma, existem linguagens que utilizam o termo procedimento (procedure) para toda subrotina.

Para explicitar o objetivo de cada subrotina, este material utiliza os termos função e procedimento.

Assim, de forma geral, um procedimento terá o seguinte formato:

procedimento nome_do_procedimento(parametro1: tipo1, parametro2: tipo2, ...)
inicio
    // Variáveis locais
    // ...
    // Implementação
    // ...
fim

Pode-se notar que não existe informações sobre o tipo de retorno, nem um valor de retorno.

Para um exemplo, poder-se-ia definir um procedimento para escrever o resultado do valor absoluto de um número usando a função valor_absoluto() definida anteriormente. Embora você possa escolher o nome que quiser para suas subrotinas, é comum utilizar verbos no modo imperativo para os nomes. Assim, o procedimento poderia chamar-se algo como escreva_valor_absoluto(). Outra opção poderia ser imprima_valor_absoluto().

// Definição.
function escreva_valor_absoluto(valor) {
    // A função valor_absoluto() deve estar definida.
    let resultado = valor_absoluto(valor)
    console.log("|", valor, "| = ", resultado)
}

// Uso.
escreva_valor_absoluto(1)
escreva_valor_absoluto(0)
escreva_valor_absoluto(-1)
# Definição.
def escreva_valor_absoluto(valor):
    # A função valor_absoluto() deve estar definida.
    resultado = valor_absoluto(valor)
    print("|", valor, "| = ", resultado)

# Uso.
escreva_valor_absoluto(1)
escreva_valor_absoluto(0)
escreva_valor_absoluto(-1)
-- Definição.
function escreva_valor_absoluto(valor)
    -- A função valor_absoluto() deve estar definida.
    local resultado = valor_absoluto(valor)
    print("|", valor, "| = ", resultado)
end

-- Uso.
escreva_valor_absoluto(1)
escreva_valor_absoluto(0)
escreva_valor_absoluto(-1)
extends Node

# Definição.
func escreva_valor_absoluto(valor):
    # A função valor_absoluto() deve estar definida.
    var resultado = valor_absoluto(valor)
    print("|", valor, "| = ", resultado)

func _ready():
    # Uso.
    escreva_valor_absoluto(1)
    escreva_valor_absoluto(0)
    escreva_valor_absoluto(-1)

Assim, subrotinas podem outras usar subrotinas definidas previamente, tornando o código cada vez mais modular. Subrotinas reusáveis também poder ser agrupadas em um arquivo próprio para definir uma biblioteca. Quando você utiliza um comando como include ou import em uma linguagem de programação, você carrega, dentre outros, definições de subrotinas para usar em seus programas.

Deste momento em diante, você pode começar a criar suas própria bibliotecas caso reúna as subrotinas que você criar um arquivo. Por exemplo, sua biblioteca pode chamar-se seu_nome.js, seu_nome.lua, seu_nome.py e/ou seu_nome.gd. Quando quiser usá-las, basta importar o arquivo para obter o código-fonte e a definição. Dependendo da linguagem de programação e do interpretador (ou compilador), pode ser necessário modificar o comando para gerar o projeto. Caso esteja usando um IDE, é provável que ele faça isso automaticamente para você (desde que a criação dos arquivos tenha sido feita usando a ferramenta).

Métodos

Em linguagens de programação orientadas a objetos (paradigma da Programação Orientada a Objetos ou POO, do inglês, Object-Oriented Programming ou OOP), funções ou procedimentos que pertençam a uma classe são chamadas de métodos. Uma diferença prática que distingüe métodos dos demais tipos de subrotinas é a possibilidade de alterar o estado interno de objetos de uma classe, armazenado em variáveis (chamadas de atributos).

Neste momento, como o objetivo não é aprender OOP, não é necessário preocupar-se com a terminologia. Basta saber que, quando se faz algo como variavel.chamada(parametro) ao invés de chamada(variavel, parametro), o primeiro caso utiliza um método, enquanto o segundo caso utiliza uma função ou procedimento.

Portanto, agora você entende porque existem subrotinas em linguagens de programação que utilizam um valor ou uma variável antes da chamada: trata-se de uma chamada de método.

Terminologia e Técnicas

Subrotinas são importantes para programação. Existem, inclusive, paradigmas de programação (como Programação Funcional) que focam na construção de programas como composições de funções.

Dada a importância, convém conhecer mais sobre a terminologia relacionada e técnicas para definição e uso de funções, procedimentos e métodos.

Declaração, Assinatura, Documentação, Protótipo e Forward Declaration

A assinatura de uma subrotina é a combinação de nome, tipo de retorno e parâmetros (nomes e tipos). A leitura da assinatura permite identificar o que se deve passar para a função.

Para funções (ou procedimentos) simples ou bom bons nomes de parâmetros, pode-se identificar os parâmetros pelo nome. Para funções (ou procedimentos) complexas ou cujos nomes não sejam suficientes para entendê-las, é usual documentar:

  1. Uma descrição do que a subrotina faz;
  2. Uma descrição dos parâmetros esperados (incluindo valores válidos e inválidos);
  3. O valor retornado, se houver.

Existem alguns formatos especiais para documentar subrotinas, adotados por ferramentas que geram documentação por meio de marcações. Algumas opções populares são:

O suporte para linguagens de programação varia de ferramenta para ferramenta. Uma vantagem das ferramentas é a possibilidade de criar páginas HTML com a documentação. Por exemplo, a documentação da Interface de Programação de Aplicações (Application Programming Interface; API) do KDE é criada usando Doxygen (em conjunto com KApiDox, criado pelo KDE).

Algumas linguagens de programação também permitem definir doc strings, que são cadeias de caracteres fornecidas na definição de uma subrotina para documentá-la.

Python fornece doc strings e é suportada por Doxygen. O exemplo a seguir ilustra a criação de uma doc string em formato Doxygen. Para mais informações, pode-se consultar a documentação do Doxygen para Python.

def valor_absoluto(valor):
    """! Função matemática para cálculo do valor absoluto (módulo) de um número.

    @param valor  Um número inteiro ou real a ser utilizado no cálculo.

    @return  O valor absoluto de valor.
    """

    if (valor >= 0):
        resultado = valor
    else:
        resultado = -valor

    return resultado

Existem outras marcações (por exemplo, para exemplos de uso). Uma boa documentação informa sobre o uso esperado para uma subrotina, eliminando a necessidade de leitura do código-fonte para saber o que ela faz.

def valor_absoluto(valor):
    """! Função matemática para cálculo do valor absoluto (módulo) de um número.

    @param valor  Um número inteiro ou real a ser utilizado no cálculo.

    @return  O valor absoluto de valor.
    """

    # ...

Com a leitura anterior, pode-se inferir que um uso válido para valor_absoluto() poderia ser algo como valor_absoluto(-1.234) para o cálculo de .

A declaração de uma subrotina é, normalmente, a combinação de assinatura com implementação. É comum que linguagens de programação permitam apenas o uso de funções definidas em um ponto anterior à (antes da) chamada. Em algumas linguagens de programação (como C e C++), existem conceitos como protótipo e forward declaration, que permitem notificar o compilador da existência de uma função (via assinatura) antes da implementação. Protótipos e forward declaration fornecem um recurso para contornar o problema de definição antes de chamada.

Argumentos e Parâmetros

Tecnicamente, existem dois termos para valores passados e recebidos por subrotinas. Um argumento é o valor passado na chamada de uma subrotina. Um parâmetro é o valor recebido pela subrotina após a chamada.

Na prática, os termos são comumente usados indistintamente.

Passagem de Parâmetros

Algumas linguagens de programação permitem passar parâmetros para subrotinas de formas diferentes. Os dois tipos mais comuns de passagem são passagem por valor e passagem por referência, embora existam outros. Por exemplo, um terceiro tipo freqüente é chamado do passagem por atribuição (usada, por exemplo, em Python).

Na passagem por valor, o valor recebido pela subrotina é uma cópia do valor original. Em outras palavras, a subrotina pode alterar o valor recebido sem modificar o valor original. Pode-se pensar no parâmetro como se fosse uma variável local adicional da subrotina.

Na passagem por referência, o valor recebido é um endereço de memória ou uma referência para a variável que armazena o valor original. Assim, caso a subrotina modifique o valor recebido, o valor também será alterado no código que realizou a chamada da subrotina. Em outras palavras, todas as mudanças de valores serão permanentes e afetarão o resto do programa.

Na passagem por atribuição, o comportamento do parâmetro varia de acordo com o que um operador de atribuição faria com a variável. Por exemplo, caso a alteração fosse feita por referência (usando um endereço de memória), a alteração do valor afetará a variável original. Caso contrário, a variável será considerada uma cópia.

Linguagens de programação também podem variar o comportamento para passagem de parâmetros de acordo com o tipo de dados. Em muitas linguagens de programação, é comum que a passagem de tipos primitivos seja considerada como por valor ou equivalente. Tipos compostos de dados e estruturas de dados, por outro lado, são comumente passados por referência ou equivalente.

Neste momento, todos os tipos passados para subrotinas serão de tipos primitivos. Assim, pode-se assumir, por enquanto, que todas as passagens serão por valor, exceto caso se comente o contrário.

Polimorfismo

Em programação, polimorfismo permite, dentre outros, definir versões de uma subrotina com o mesmo nome com diferentes tipos (ou quantidades) de parâmetros. Esse tipo de polimorfismo é chamado de ad-hoc (coerção é outro exemplo de polimorfismo ad-hoc).

Por exemplo, ao invés de definir uma subrotina para somar números reais (some_reais(x: real, y: real): real) e uma subrotina para números inteiros (some_inteiros(x: inteiro, y: inteiro): inteiro), é possível definir ambas as subrotinas com um mesmo nome, mas parâmetros de tipos diferentes. Ou seja, some(x: inteiro, y: inteiro): inteiro e some(x: real, y: real): real.

A possibilidade é prática porque permite criar uma interface de programação comum para diferentes tipos de dados. Em linguagens que não permitem o uso desse tipo de polimorfismo, prefixar ou sufixar o nome da subrotina com o tipo pode ser necessário.

Função de Primeira Classe (ou Funções como Cidadãs de Primeira Classe) e Funções Anônimas (Lambda)

Algumas linguagens (como Lua, JavaScript e Python) permitem armazenar referências para funções em variáveis, passá-las como parâmetros para outras subrotinas ou retorná-las de outras subrotinas. Em casos assim, diz-se que a linguagem trata funções (ou subrotinas, em geral) como cidadãs de primeira classe.

A passagem de subrotinas como parâmetros para outras subrotinas ou o retorno de uma subrotina são também chamadas de high-order functions, algo como "função de ordem mais alta". Não se trata de passar o resultado de uma chamada de subrotina, mas do código da própria subrotina.

Algumas linguagens também fornecem um recurso chamado função anônima, mais conhecida como lambda (letra grega ou ).

let escreva_minusculas = function(texto) {
    console.log(texto.toLowerCase())
}

function escreva_maiusculas(texto) {
    console.log(texto.toUpperCase())
}

function escreva_ola_franco(funcao_escrita) {
    funcao_escrita("Olá, Franco!")
}

escreva_ola_franco(escreva_minusculas)
escreva_ola_franco(escreva_maiusculas)
escreva_minusculas = lambda texto: print(texto.lower())

def escreva_maiusculas(texto):
    print(texto.upper())

def escreva_ola_franco(funcao_escrita):
    funcao_escrita("Olá, Franco!")

escreva_ola_franco(escreva_minusculas)
escreva_ola_franco(escreva_maiusculas)
-- A omissão de local define escreva_minusculas como uma função global.
escreva_minusculas = function(texto)
    print(texto:lower())
end

function escreva_maiusculas(texto)
    print(texto:upper())
end

function escreva_ola_franco(funcao_escrita)
    funcao_escrita("Olá, Franco!")
end

escreva_ola_franco(escreva_minusculas)
escreva_ola_franco(escreva_maiusculas)
# Atenção:
# Este bloco de código *não funcionará* em versões atuais (2021) do motor.
# A versão 3 de Godot não suporte funções anônimas (lambda).
# A versão 4 planeja suporte para funções lambda.
# O exemplo a seguir, portanto, requer a versão 4 (em desenvolvimento) para uso.
# Consultar:
# - <https://godotengine.org/article/gdscript-progress-report-feature-complete-40>
# - <https://github.com/godotengine/godot-proposals/issues/2431>
# - <https://github.com/godotengine/godot/pull/38645>

extends Node

var escreva_minusculas = func(texto):
    print(texto.to_lower())

func escreva_maiusculas(texto):
    print(texto.to_upper())

func escreva_ola_franco(funcao_escrita):
    funcao_escrita.call("Olá, Franco!")

func _ready():
    escreva_ola_franco(escreva_minusculas)
    escreva_ola_franco(escreva_maiusculas)

O exemplo anterior ilustra:

  • A definição de uma função armazenada em uma variável;
  • A passagem de uma função como parâmetro para outra função;
  • A chamada da função armazenada em uma variável.

No caso, todas as funções são procedimentos, mas a idéia é a mesma.

Embora o recurso possa ser mais avançado, é interessante conhecê-lo. Além de comum em linguagens do paradigma funcional, alguns frameworks modernos, como React, empregam funções de primeira classe como técnica de programação. Por exemplo, elas podem ser usadas como substitutas para estruturas de condição e repetições para filtrar dados (isto é, selecionar elementos com determinados valores).

Testes

O uso de subrotinas pode facilitar testes e depuração de programas de várias maneiras.

A definição de subrotinas centraliza a implementação de uma funcionalidade em um único local. Isso facilita modificações no código-fonte e restringe áreas de possíveis erros. Por exemplo, se uma função retornou um valor incorreto, sabe-se que existe um problema na definição da função em questão, em uma outra subrotina usada pela função, ou no parâmetro passado.

Além disso, existem técnicas de teste que podem ser usadas para funções. Uma das mais populares chama-se teste unitário. O teste unitário trata a implementação da função como uma caixa preta. Para cada caso de teste em teste unitário, define-se combinações de parâmetros com resultados esperados para a chamada usando os valores definidos. Caso o resultado obtido seja diferente do esperado, sabe-se que existem problemas na implementação.

Por exemplo, para valor_absoluto(), poder-se-ia definir os seguintes casos de teste:

  • : entrada , saída esperada: ;
  • : entrada , saída esperada: ;
  • : entrada , saída esperada: ;
  • : entrada , saída esperada .
  • : entrada , saída esperada .
console.log(valor_absoluto(-1))
console.log(valor_absoluto(-1.23))
console.log(valor_absoluto(0))
console.log(valor_absoluto(1))
console.log(valor_absoluto(1.23))
print(valor_absoluto(-1))
print(valor_absoluto(-1.23))
print(valor_absoluto(0))
print(valor_absoluto(1))
print(valor_absoluto(1.23))
print(valor_absoluto(-1))
print(valor_absoluto(-1.23))
print(valor_absoluto(0))
print(valor_absoluto(1))
print(valor_absoluto(1.23))
print(valor_absoluto(-1))
print(valor_absoluto(-1.23))
print(valor_absoluto(0))
print(valor_absoluto(1))
print(valor_absoluto(1.23))

Pode-se notar que existem casos em que é possível definir casos de teste antes mesmo de implementar a subrotina. De fato, existe um processo de desenvolvimento de software chamado de test-driven development (TDD), que recomenda a criação de testes antes da implementação de um programa (ou de cada nova funcionalidade).

Combinando-se chamadas de função com operadores relacionais, é possível começar a automatizar o processo de testes. Por exemplo, nos próximos casos, qualquer impressão de False ou false indicaria a falha de um caso de teste.

console.log(valor_absoluto(-1) === 1)
console.log(valor_absoluto(-1.23) === 1.23)
console.log(valor_absoluto(0) === 0)
console.log(valor_absoluto(1) === 1)
console.log(valor_absoluto(1.23) === 1.23)
print(valor_absoluto(-1) == 1)
print(valor_absoluto(-1.23) == 1.23)
print(valor_absoluto(0) == 0)
print(valor_absoluto(1) == 1)
print(valor_absoluto(1.23) == 1.23)
print(valor_absoluto(-1) == 1)
print(valor_absoluto(-1.23) == 1.23)
print(valor_absoluto(0) == 0)
print(valor_absoluto(1) == 1)
print(valor_absoluto(1.23) == 1.23)
print(valor_absoluto(-1) == 1)
print(valor_absoluto(-1.23) == 1.23)
print(valor_absoluto(0) == 0)
print(valor_absoluto(1) == 1)
print(valor_absoluto(1.23) == 1.23)

Para continuar aprimorando a solução, é possível criar uma subrotina para teste. O exemplo abaixo define um procedimento chamado teste_unitario() que compara se um resultado obtido é igual ao resultado esperado pelo teste. Caso os resultados sejam diferentes, o procedimento escreve uma mensagem informando sobre a falha no caso de teste. Para demonstrar o uso, modificou-se a implementação de valor_absoluto() para retornar um valor incorreto para números negativos.

function teste_unitario(nome, resultado_obtido, resultado_esperado) {
    if (resultado_obtido !== resultado_esperado) {
        console.log(nome, " falhou. Resultado esperado: ", resultado_esperado, ". Resultado obtido: ", resultado_obtido)
    }
}

function valor_absoluto(valor) {
    let resultado
    if (valor >= 0) {
        resultado = valor
    } else {
        resultado = -12345 // Incorreto!
    }
    return resultado
}

teste_unitario("|-1|", valor_absoluto(-1), 1)
teste_unitario("|-1.23|", valor_absoluto(-1.23), 1.23)
teste_unitario("|0|", valor_absoluto(0), 0)
teste_unitario("|1|", valor_absoluto(1), 1)
teste_unitario("|1.23|", valor_absoluto(1.23), 1.23)
def teste_unitario(nome, resultado_obtido, resultado_esperado):
    if (resultado_obtido != resultado_esperado):
        print(nome, " falhou. Resultado esperado: ", resultado_esperado, ". Resultado obtido: ", resultado_obtido)

def valor_absoluto(valor):
    if (valor >= 0):
        resultado = valor
    else:
        resultado = -12345 # Incorreto!
    return resultado

teste_unitario("|-1|", valor_absoluto(-1), 1)
teste_unitario("|-1.23|", valor_absoluto(-1.23), 1.23)
teste_unitario("|0|", valor_absoluto(0), 0)
teste_unitario("|1|", valor_absoluto(1), 1)
teste_unitario("|1.23|", valor_absoluto(1.23), 1.23)
function teste_unitario(nome, resultado_obtido, resultado_esperado)
    if (resultado_obtido ~= resultado_esperado) then
        print(nome, " falhou. Resultado esperado: ", resultado_esperado, ". Resultado obtido: ", resultado_obtido)
    end
end

function valor_absoluto(valor)
    local resultado
    if (valor >= 0) then
        resultado = valor
    else
        resultado = -12345 -- Incorreto!
    end
    return resultado
end

teste_unitario("|-1|", valor_absoluto(-1), 1)
teste_unitario("|-1.23|", valor_absoluto(-1.23), 1.23)
teste_unitario("|0|", valor_absoluto(0), 0)
teste_unitario("|1|", valor_absoluto(1), 1)
teste_unitario("|1.23|", valor_absoluto(1.23), 1.23)
extends Node

func teste_unitario(nome, resultado_obtido, resultado_esperado):
    if (resultado_obtido != resultado_esperado):
        print(nome, " falhou. Resultado esperado: ", resultado_esperado, ". Resultado obtido: ", resultado_obtido)

func valor_absoluto(valor):
    var resultado
    if (valor >= 0):
        resultado = valor
    else:
        resultado = -12345 # Incorreto!
    return resultado

func _ready():
    teste_unitario("|-1|", valor_absoluto(-1), 1)
    teste_unitario("|-1.23|", valor_absoluto(-1.23), 1.23)
    teste_unitario("|0|", valor_absoluto(0), 0)
    teste_unitario("|1|", valor_absoluto(1), 1)
    teste_unitario("|1.23|", valor_absoluto(1.23), 1.23)

O procedimento teste_unitario() é limitado, servindo apenas para ilustrar a prática. Por exemplo, uma limitação da implementação é que ela espera que os resultados sejam iguais. Existem casos em que poderia ser preferível utilizar outros operadores lógicos e/ou relacionais para a comparação.

Muitas linguagens de programação possuem frameworks para facilitar a criação de casos de teste e verificação automática. Por exemplo, Python possui o módulo unittest (documentação) para definição de testes unitários. Muitos IDEs também possuem integração com frameworks para teste, facilitando ainda mais para se testar um projeto.

Refatoração

Além de testes, subrotinas são comumente usadas para refatorações de sistemas. Uma refatoração modifica o código original para transformá-lo em um código equivalente que seja mais simples, eficiente, fácil de entender, e/ou de manter (manutenibilidade).

Uma técnica de refatoração comum empregada com subrotinas chama-se extração de função (ou procedimento ou método). Para um exemplo, pode-se considerar o código a seguir.

let x = -1
if (x < 0) {
    x = -x
}

let y = 2
if (y < 0) {
    y = -y
}

let z = -3
if (z < 0) {
    z = -z
}

console.log(x)
console.log(y)
console.log(z)
x = -1
if (x < 0):
    x = -x

y = 2
if (y < 0):
    y = -y

z = -3
if (z < 0):
    z = -z

print(x)
print(y)
print(z)
local x = -1
if (x < 0) then
    x = -x
end

local y = 2
if (y < 0) then
    y = -y
end

local z = -3
if (z < 0) then
    z = -z
end

print(x)
print(y)
print(z)
extends Node

func _ready():
    var x = -1
    if (x < 0):
        x = -x

    var y = 2
    if (y < 0):
        y = -y

    var z = -3
    if (z < 0):
        z = -z

    print(x)
    print(y)
    print(z)

O código faz a mesma operações três vezes, com variáveis diferentes. Pode-se observar que uma implementação como a usada para valor_absoluto() geraria código equivalente.

function valor_absoluto(valor) {
    let resultado
    if (valor >= 0) {
        resultado = valor
    } else {
        resultado = -valor
    }
    return resultado
}

let x = valor_absoluto(-1)
let y = valor_absoluto(2)
let z = valor_absoluto(-3)

console.log(x)
console.log(y)
console.log(z)
def valor_absoluto(valor):
    if (valor >= 0):
        resultado = valor
    else:
        resultado = -valor
    return resultado

x = valor_absoluto(-1)
y = valor_absoluto(2)
z = valor_absoluto(-3)

print(x)
print(y)
print(z)
function valor_absoluto(valor)
    local resultado
    if (valor >= 0) then
        resultado = valor
    else
        resultado = -valor
    end
    return resultado
end

local x = valor_absoluto(-1)
local y = valor_absoluto(2)
local z = valor_absoluto(-3)

print(x)
print(y)
print(z)
extends Node

func valor_absoluto(valor):
    var resultado
    if (valor >= 0):
        resultado = valor
    else:
        resultado = -valor
    return resultado

func _ready():
    var x = valor_absoluto(-1)
    var y = valor_absoluto(2)
    var z = valor_absoluto(-3)

    print(x)
    print(y)
    print(z)

Assim, o código comum foi refatorado em uma função.

Em geral, exceto caso a implementação seja trivial, é conveniente extrair código comum em subrotinas para evitar duplicações de código-fonte. Alguns IDEs fornecem recursos para extração de funções, procedimentos e métodos. Quando disponíveis, pode-se selecionar uma região do código com o código desejado e procurar por uma opção como Refatorar: Extrair método (ou equivalente).

Recursividade

Subrotinas podem chamar outras subrotinas. Em particular, a chamada de uma subrotina pela mesma subrotina recebe um nome especial: recursão.

Recursividade não é uma característica exclusiva de programação. Ela é comum na Matemática e também pode ser encontrada na natureza. Por exemplo, existem plantas como árvores que assemelham estruturas recursivas. Outro exemplo são fractais.

Uma subrotina recursiva possui duas partes:

  1. Um caso base (ou mais), também chamado de condição de parada, que define um resultado imediato para o término da recursão;
  2. Um passo recursivo, fórmula recursiva ou equação de recorrência, que simplifica o problema progressivamente em função dele mesmo, até se chegar no caso base.

Uma piada comum para explicar recursão é a criação de um link que leva a ele mesmo, como este. A piada funciona porque o navegador seguirá o link uma única vez, servindo como chamada recursiva e caso base para parada.

Um dos exemplos mais famosos de equação recursiva são números fatoriais. Um número fatorial é um número gerado pela expressão , sendo (lê-se zero fatorial) definido como e um número natural. Por exemplo, .

O fatorial tem a seguinte definição:

Assim como programas podem ter mais de um ponto de saída, uma subrotina pode ter mais de uma possibilidade para retorno (embora cada chamada retorne uma única vez). Isso permite a escrita de código recursivo.

function fatorial(x) {
    if (x === 0) {
        return 1
    }
    else {
        return x * fatorial(x - 1)
    }
}
def fatorial(x):
    if (x == 0):
        return 1
    else:
        return x * fatorial(x - 1)
function fatorial(x)
    if (x == 0) then
        return 1
    else
        return x * fatorial(x - 1)
    end
end
extends Node

func fatorial(x):
    if (x == 0):
        return 1
    else:
        return x * fatorial(x - 1)

O código anterior é equivalente ao código a seguir, pois o uso de return encerra a chamada atual da função assim que executado.

function fatorial(x) {
    if (x === 0) {
        return 1
    }

    return x * fatorial(x - 1)
}
def fatorial(x):
    if (x == 0):
        return 1

    return x * fatorial(x - 1)
function fatorial(x)
    if (x == 0) then
        return 1
    end

    return x * fatorial(x - 1)
end
extends Node

func fatorial(x):
    if (x == 0):
        return 1

    return x * fatorial(x - 1)

O uso de recursões permitem definir código que se repete, fornecendo uma alternativa ao uso de estruturas de repetição (laços ou loops). Algo que se deve atentar é que existe um limite máximo para chamadas recursivas; caso ele seja excedido, ocorre um erro chamado de estouro de pilha (stack overflow).

Para iniciantes, um estouro de pilha comumente sugere implementação incorreta de um algoritmo recursivo. Por exemplo, em JavaScript:

function fatorial_incorreto(x) {
    if (x === 0) {
        return 1
    }

    return x * fatorial_incorreto(x) // Incorreto!}

Uma chamada do código anterior (por exemplo, fatorial_incorreto(5)) nunca terminaria, pois a chamada recursiva não simplifica o problema. Ela utiliza o mesmo valor recebido como parâmetro, ao invés de aproximá-lo do caso base com uma subtração. Na prática, ele terminará com um estouro de pilha, pois a memória de um computador é finita, obrigando um limite máximo de chamadas recursivas que podem ser armazenadas.

Existe um caso particular de recursão chamada de recursão de cauda (tail recursion, também chamada de tail-call optimization). Ela permite otimizar código recursivo para, potencialmente, minimizar o consumo de memória, melhorar o desempenho da subrotina e evitar estouro de pilha. Potencialmente porque nem toda linguagem de programação faz otimizações para recursão de cauda. Por exemplo, Lua e JavaScript fazem otimizações para recursão de cauda, mas Python não faz. Embora não tenha certeza, acredito que GDScript também não faça.

Como a explicação de recursão de cauda requereria a explicação sobre uma estrutura de dados chamada de pilha, este tópico apenas fornecerá um exemplo.

function fatorial_recursao_cauda(x, resultado) {
    if (x === 0) {
        return resultado
    }

    return fatorial_recursao_cauda(x - 1, x * resultado)
}

function fatorial(x) {
    return fatorial_recursao_cauda(x, 1)
}
def fatorial_recursao_cauda(x, resultado):
    if (x == 0):
        return resultado

    return fatorial_recursao_cauda(x - 1, x * resultado)

def fatorial(x):
    return fatorial_recursao_cauda(x, 1)
function fatorial_recursao_cauda(x, resultado)
    if (x == 0) then
        return resultado
    end

    return fatorial_recursao_cauda(x - 1, x * resultado)
end

function fatorial(x)
    return fatorial_recursao_cauda(x, 1)
end
extends Node

func fatorial_recursao_cauda(x, resultado):
    if (x == 0):
        return resultado

    return fatorial_recursao_cauda(x - 1, x * resultado)

func fatorial(x):
    return fatorial_recursao_cauda(x, 1)

Para usar o exemplo, poder-se-ia fazer uma chamada como fatorial_recursao_cauda(5, 1), fornecendo sempre o resultado inicial 1. Contudo, para evitar erros e tornar o uso mais conveniente, a segunda definição (fatorial()) provê a chamada correta pré-definada. Assim, pode-se chamar diretamente fatorial() para o uso. Por exemplo, fatorial(5). A criação da função auxiliar para uso é conveniente em outros contextos, como para manipulação de estruturas de dados.

A propósito, o uso da função fatorial é útil para demonstrar overflow em números inteiros, como comentado em Aritmética e Matemática Básica. Em linguagens nas quais números inteiros são compostos por número fixos de bytes, como 32 bits (4 bytes) ou 64 bits (8 bytes), ocorrerá overflow rapidamente. Você pode verificar a ocorrência fazendo chamadas como:

  • fatorial(5);
  • fatorial(6);
  • fatorial(7);
  • fatorial(8);
  • fatorial(12);
  • fatorial(13);
  • fatorial(14);
  • fatorial(17);
  • fatorial(20);
  • fatorial(21);
  • fatorial(22).

Dependendo da linguagem, alguns valores obtidos para chamadas posteriores serão menores que o valor anterior. Os valores podem, inclusive, tornarem-se negativos. Isso ocorre em decorrência de overflow.

Asserções

O exemplo para fatorial utiliza números naturais, mas linguagens de programação trabalham com números inteiros. Como evitar o cálculo para números negativos?

Uma opção é utilizar um resultado impossível para marcar erro. Por exemplo, retornar -1 para a entrada de um parâmetro negativo.

function fatorial(x) {
    if (x < 0) {
        return -1
    } else if (x === 0) {
        return 1
    } else {
        return x * fatorial(x - 1)
    }
}

let resultado = fatorial(-5)
if (resultado !== -1) {
    console.log(resultado)
} else {
    console.log("Parâmetro inválido.")
}
def fatorial(x):
    if (x < 0):
        return -1
    elif (x == 0):
        return 1
    else:
        return x * fatorial(x - 1)

resultado = fatorial(-5)
if (resultado != -1):
    print(resultado)
else:
    print("Parâmetro inválido.")
function fatorial(x)
    if (x < 0) then
        return -1
    elseif (x == 0) then
        return 1
    else
        return x * fatorial(x - 1)
    end
end

local resultado = fatorial(-5)
if (resultado ~= -1) then
    print(resultado)
else
    print("Parâmetro inválido.")
end
extends Node

func fatorial(x):
    if (x < 0):
        return -1
    elif (x == 0):
        return 1
    else:
        return x * fatorial(x - 1)

func _ready():
    var resultado = fatorial(-5)
    if (resultado != -1):
        print(resultado)
    else:
        print("Parâmetro inválido.")

Um segunda possibilidade é usar exceções, em linguagem que forneçam suporte.

Outra possibilidade é usar uma asserção (assert) para indicar uso incorreto da função. Uma asserção define uma condição que deve ser verdadeira para uso correto da subrotina. Ela define um contrato: espera-se que a condição seja válida para que o resultado seja correto. Caso a condição falhe, o programa travará com uma mensagem de erro.

Deve-se notar que asserções não devem ser usadas para validação de programas usados por usuários finais (por exemplo, para validação de entrada), mas apenas para indicar erros em tempo de desenvolvimento. Em muitas implementações, asserções são desabilitadas para versões finais de programas (chamadas de release, feitas para distribuição). Em outras palavras, não se verifica a condição da asserção em tempo de uso em versões release. Portanto, a forma correta de tratar erros envolve o uso de estruturas condicionais ou exceções.

Com ciência da limitação, asserções são úteis durante o desenvolvimento. Muitas linguagens de programação implementam asserções como um comando ou uma função.

  • JavaScript: a linguagem não fornece uma implementação padrão para assert(). Existe console.assert() (documentação), mas a subrotina não interrompe a execução do programa em caso de erro. Ou seja, no caso, a asserção serve apenas como um aviso, limitando sua utilidade.
  • Python: assert() (documentação);
  • Lua: assert() (documentação);
  • GDScript: assert() (documentação).
function fatorial(x) {
    console.assert(x >= 0, "Fatorial não pode ser calculado para números negativos")

    if (x === 0) {
        return 1
    } else {
        return x * fatorial(x - 1)
    }
}

fatorial(5) // OK
// fatorial(-5) // Erro; comentado para evitar repetição infinita / estouro de pilha.
def fatorial(x):
    assert x >= 0, "Fatorial não pode ser calculado para números negativos"

    if (x == 0):
        return 1
    else:
        return x * fatorial(x - 1)

fatorial(5) # OK
fatorial(-5) # Erro; programa travaré e apresentará a mensagem de erro.
function fatorial(x)
    assert(x >= 0, "Fatorial não pode ser calculado para números negativos")

    if (x == 0) then
        return 1
    else
        return x * fatorial(x - 1)
    end
end

fatorial(5) -- OK
fatorial(-5) -- Erro; programa travaré e apresentará a mensagem de erro.
extends Node

func fatorial(x):
    assert(x >= 0, "Fatorial não pode ser calculado para números negativos")

    if (x == 0):
        return 1
    else:
        return x * fatorial(x - 1)

func _ready():
    fatorial(5) # OK
    fatorial(-5) # Erro; programa travaré e apresentará a mensagem de erro.

Em Python, Lua e GDScript, o programa travará quando a função for usada com um número negativo, emitindo a mensagem definida. Em JavaScript, embora a mensagem seja apresentada, o código continuará em execução até que seja manualmente interrompido (por exemplo, fechando a aba ou a janela do navegador) ou em caso de stack overflow -- o que ocorrer primeiro. Para que você não tenha que fechar a aba ou seu navegador, ou para evitar que ele trave por estouro de pilha, a linha com a chamada usando um número negativo está comentada.

Asserções também poder ser usadas para verificar o tipo do parâmetro recebido, como garantia adicional de uso correto.

Early Return versus Single Entry, Single Return

Algumas linguagens de programação restringem o uso de um único uso de retorne por subrotina. Outras permitem usar múltiplos.

Subrotinas que empregam um único retorne são chamadas de single entry, single return (algo como entrada única, retorno único). Subrotinas que utilizam mais de um retorne são ditas com early return (algo como retorno precoce).

Em particular, o uso de early return pode gerar código mais simples de ler. A idéia é eliminar condições de erro no início da definição da subrotina, permitindo que o restante da implementação foque no caso geral. Isso permite evitar o uso de múltiplos se aninhados.

Para comparar as duas abordagens, pode-se considerar o exemplo de cálculo de fatorial retornando -1 em caso de número negativo.

Na versão com único retorno, deve-se declarar uma variável para armazenar o resultado (no caso, chamada de resultado). Todos os possíveis resultados serão atribuídos nela. No caso de Python, usa-se None para indicar ausência de valor até a primeira atribuição real, porque a declaração de variáveis na linguagem é feita via atribuição. Outra possibilidade seria declarar a variável diretamente na atribuição do valor real.

function fatorial(x) {
    let resultado
    if (x < 0) {
        resultado = -1
    } else if (x === 0) {
        resultado = 1
    } else {
        resultado = x * fatorial(x - 1)
    }

    return resultado
}
def fatorial(x):
    resultado = None
    if (x < 0):
        resultado = -1
    elif (x == 0):
        resultado = 1
    else:
        resultado = x * fatorial(x - 1)

    return resultado
function fatorial(x)
    local resultado
    if (x < 0) then
        resultado = -1
    elseif (x == 0) then
        resultado = 1
    else
        resultado = x * fatorial(x - 1)
    end

    return resultado
end
func fatorial(x):
    var resultado
    if (x < 0):
        resultado = -1
    elif (x == 0):
        resultado = 1
    else:
        resultado = x * fatorial(x - 1)

    return resultado

Na versão com early return, todos os casos retornam imediatamente após tratados. A implementação fica mais linear, com menos ramificações. Caso se deseje, é possível, inclusive, omitir senão após cada retorno.

function fatorial(x) {
    if (x < 0) {
        return -1
    }

    if (x === 0) {
        return 1
    }

    let resultado = x * fatorial(x - 1)

    return resultado
}
def fatorial(x):
    if (x < 0):
        return -1

    if (x == 0):
        return 1

    resultado = x * fatorial(x - 1)

    return resultado
function fatorial(x)
    if (x < 0) then
        return -1
    end

    if (x == 0) then
        return 1
    end

    local resultado = x * fatorial(x - 1)

    return resultado
end
func fatorial(x):
    if (x < 0):
        return -1

    if (x == 0):
        return 1

    var resultado = x * fatorial(x - 1)
    return resultado

A propósito, é possível definir early return em procedimentos. Para isso, basta usar um retorne sem valor (vazio).

function escreva_se_franco(nome) {
    if (nome.toLowerCase() !== "franco") {
        return
    }

    console.log("Franco")
}
def escreva_se_franco(nome):
    if (nome.lower() != "franco"):
        return

    print("Franco")
function escreva_se_franco(nome)
    if (nome:lower() ~= "franco") then
        return
    end

    print("Franco")
end
func escreva_se_franco(nome):
    if (nome.to_lower() != "franco"):
        return

    print("Franco")

Pessoalmente, eu prefiro escrever código com early return. Na versão com early return, todos os casos excepcionais retornam imediatamente após identificados ou tratados.

Existem casos em que a versão escrita com um único retorno pode ser mais rápida; entretanto, ao invés de assumir, o melhor é identificar um gargalo e usar um profiler (ferramenta para verificação de desempenho) para comparar o desempenho, caso seja realmente necessário.

Subrotinas em Programação

As seções anteriores forneceram alguns exemplos de uso de subrotinas em linguagens de programação. As próximas seções apresentam alguns exemplos adicionais como um resumo, ilustrando o uso com fluxogramas e linguagens de programação visual.

Fluxogramas: Flowgorithm

Para criar subrotinas em Flowgorithm, você pode usar a opção Programa (Program) e escolher Acrescentar subprograma... (Add Function...). Outra opção é clicar no ícone com o nome Principal (Main) e escolher (Acrescentar subprograma...) Add Function.... Este mesmo ícone permite alternar entre subrotinas existentes e o programa principal.

Escolha um nome para a subrotina; caso necessário, defina parâmetros:

  • Acrescentar (Add) cria um novo parâmetro. Ao escolher a opção, você deverá nomear o novo parâmetro e escolher um tipo para ele;
  • Editar (Edit) modifica um parâmero existente;
  • Remover (Remove) apaga um parâmetro.

Por fim, escolha tipo de retorno para a subrotina. Caso seja um procedimento, escolha Nenhum (None). Caso contrário, escolha um tipo e um nome para a variável que será retornada (por exemplo, resultado).

Para voltar ao programa principal, escolha Principal (Main) no ícone com o nome da subrotina recém-criada. Para adicionar uma chamada de função, clique em uma seta e escolha Chamada (Call). No bloco criado, digite o nome da subrotina desejada. Caso ela tenha parâmetros, digite os valores entre parênteses. Por exemplo, escrevaMensagem("Olá, meu nome é Franco!").

Também é possível chamar subrotinas em blocos de saída e atribuição. Para isso, basta escrever o nome da subrotina, fornecendo parâmetros entre parênteses.

Exemplo uso de subrotinas em Flowgorithm com interface em Português.

Na imagem, cada bloco iniciado por uma elipse roxa representa uma subrotina (chamada de subprograma em Flowgorithm). Ela inicia-se de um nome (como Principal ou escrevaMensagem) e termina em um Fim (caso não tenha retorno, definindo um procedimento) ou em um Retornar, caso retorne um valor (definindo uma função).

O programa principal da imagem é composto apenas pelas subrotinas criadas: escrevaMensagem(), some(), escrevaSoma() e escrevaContagemRegressiva().

Os trechos de código a seguir contêm a transcrição do texto da imagem. A imagem contém cinco fluxogramas.

  1. escrevaMensagem:

    escrevaMensagem(Caracteres mensagem)
    
    Saída mensagem
    
    Fim
  2. some:

    some(Inteiro x, Inteiro y)
    
    Inteiro resultado
    resultado = x + y
    
    Retornar Inteiro resultado
  3. escrevaSoma:

    escrevaSoma(Inteiro x, Inteiro y)
    
    Saída x & " + " & y & " = " & add(x, y)
    
    Fim
  4. escrevaContagemRegressiva:

    escrevaContagemRegressiva(Inteiro contador)
    
    Saída contador
    Alternativa contador > 0
        Falso
        Verdadeiro
            escrevaContagemRegressiva(contador - 1)
    
    Fim
  5. Principal:

    Principal
    
    escrevaMensagem("Olá, meu nome é Franco!")
    escrevaMensagem(ToString(some(1, 2)))
    escrevaMensagem(ToString(some(-1, -2)))
    escrevaSoma(1, 2)
    escrevaSoma(-1, -2)
    escrevaContagemRegressiva(5)
    escrevaMensagem("Até mais!")
    
    Fim

Linguagens de Programação Visual: Scratch

Para criar subrotinas em Scratch, acesse Meus Blocos (My Blocks) no menu lateral, depois Criar um bloco (Make a Block). Troque nome do bloco (block name) pelo nome de sua subrotina. Para definir parâmetros, escolha Adicionar uma entrada número ou texto (Add an input number or text), para valores dos tipos cadeia de caracteres, inteiro ou real, ou Adicionar uma entrada booleano (Add an input boolean), para que a subrotina receba um bloco com uma expressão relacional ou lógica. Pode-se adicionar quantos parâmetros forem necessários. Após adicionar um parâmetro, escolha um nome para ele. Caso queira removê-lo, clique no ícone de lixeira (🗑). Após definir todos os parâmetros, clique em OK.

Com a assinatura da subrotina criada, pode-se inserir blocos nela para definir a implementação. Para usar um parâmetro, deve-se arrastar o nome do parâmetro da assinatura para o local de uso.

Infelizmente, a criação de blocos para subrotinas em Scratch é limitada. Uma primeira limitação é a impossibilidade de definir valores de retorno. Uma segunda é impossibilidade de declarar variáveis locais.

Como alternativas (leia-se gambiarras), pode-se definir variáveis globais e usá-las em subrotinas. Não é uma solução ideal, mas é uma solução possível para programas simples. Como o nome de toda variável deve ser único, nenhuma outra variável poderá ter o nome escolhido para a usada em uma subrotina. Assim, minha recomendação seria prefixar o nome das variáveis com o nome da subrotina. Por exemplo, poder-se-ia nomear uma variável local resultado para a subrotina some() como some_resultado. Com a convenção de que nome_funcao_resultado seja o valor retornado, pode-se simular a construção de funções.

Exemplo uso de subrotinas em Scratch em página em Português.

Usando-se variáveis globais como variáveis locais e retornos de subrotinas, deve-se tomar cuidado especial com funções recursivas. Resultados parciais não podem usar as variáveis improvisadas (caso contrário, o valor será alterado na chamada recursiva, pois existe uma única variável compartilhada por todas as chamadas). Alternativas incluem transformar o resultado parcial em um parâmetro ou implementar a recursão de cauda. Uma opção mais avançada seria armazenar variáveis locais em cada nível de recursão usando uma lista (usada como pilha), mas seria uma alternativa mais complexa. Ao invés de tentar contornar as limitações da linguagem, é melhor usar Scratch apenas para fins ilustrativos e usar uma linguagem mais adequada para programas mais complexos. Afinal, linguagens de programação são ferramentas.

Linguagens de Programação Textual: JavaScript, Python, Lua e GDScript

As seções anteriores forneceram exemplos em JavaScript, Python, Lua e GDScript. Assim, a implementação das novas subrotinas usadas em recursos visuais deve ser relativamente simples neste momento.

function escreva_mensagem(mensagem) {
    console.log(mensagem)
}

function some(x, y) {
    let resultado = x + y
    return resultado
}

function escreva_soma(x, y) {
    escreva_mensagem(x + " + " + y + " = " + some(x,y))
}

function escreva_contagem_regressiva(contador) {
    console.log(contador)
    if (contador > 0) {
        escreva_contagem_regressiva(contador - 1)
    }
}

escreva_mensagem("Olá, meu nome é Franco!")
escreva_mensagem(some(1, 2))
escreva_mensagem(some(-1, -2))
escreva_soma(1, 2)
escreva_soma(-1, -2)
escreva_contagem_regressiva(5)
escreva_mensagem("Até mais!")
def escreva_mensagem(mensagem):
    print(mensagem)

def some(x, y):
    resultado = x + y
    return resultado

def escreva_soma(x, y):
    escreva_mensagem(str(x) + " + " + str(y) + " = " + str(some(x,y)))

def escreva_contagem_regressiva(contador):
    print(contador)
    if (contador > 0):
        escreva_contagem_regressiva(contador - 1)

escreva_mensagem("Olá, meu nome é Franco!")
escreva_mensagem(some(1, 2))
escreva_mensagem(some(-1, -2))
escreva_soma(1, 2)
escreva_soma(-1, -2)
escreva_contagem_regressiva(5)
escreva_mensagem("Até mais!")
function escreva_mensagem(mensagem)
    print(mensagem)
end

function some(x, y)
    local resultado = x + y
    return resultado
end

function escreva_soma(x, y)
    escreva_mensagem(x .. " + " .. y .. " = " .. some(x,y))
end

function escreva_contagem_regressiva(contador)
    print(contador)
    if (contador > 0) then
        escreva_contagem_regressiva(contador - 1)
    end
end

escreva_mensagem("Olá, meu nome é Franco!")
escreva_mensagem(some(1, 2))
escreva_mensagem(some(-1, -2))
escreva_soma(1, 2)
escreva_soma(-1, -2)
escreva_contagem_regressiva(5)
escreva_mensagem("Até mais!")
extends Node

func escreva_mensagem(mensagem):
    print(mensagem)

func some(x, y):
    var resultado = x + y
    return resultado

func escreva_soma(x, y):
    escreva_mensagem(str(x) + " + " + str(y) + " = " + str(some(x,y)))

func escreva_contagem_regressiva(contador):
    print(contador)
    if (contador > 0):
        escreva_contagem_regressiva(contador - 1)

func _ready():
    escreva_mensagem("Olá, meu nome é Franco!")
    escreva_mensagem(some(1, 2))
    escreva_mensagem(some(-1, -2))
    escreva_soma(1, 2)
    escreva_soma(-1, -2)
    escreva_contagem_regressiva(5)
    escreva_mensagem("Até mais!")

Novos Itens para Seu Inventário

Ferramentas:

  • Geração de documentação;
  • Testes;
  • Refatoração;
  • Asserções.

Habilidades:

  • Criação e uso de funções;
  • Criação e uso de procedimentos;
  • Recursão.

Conceitos:

  • Modularização;
  • Subrotinas: funções, procedimentos e métodos;
  • Bibliotecas;
  • Assinatura;
  • Argumentos e parâmetros;
  • Passagem de parâmetros;
  • Função de primeira classe;
  • Teste unitário;
  • Refatoração;
  • Early return e single entry, single return.

Recursos de programação:

  • Subrotinas;
  • Bibliotecas.

Pratique

Para os exercícios matemáticos, basta saber converter as fórmulas em expressões aritméticas. Não se preocupe com símbolos. Defina variáveis quando necessário e utilize os operadores requeridos. Em alguns casos, você precisará usar estruturas condicionais para evitar erros ou separar expressões.

Além disso, comece a criar casos de teste. Quando possível, antes mesmo de começar a implementar sua solução. Entender um problema é um passo importante para resolvê-lo.

  1. Escreva um procedimento que receba um texto com um interesse como parâmetro. Escreva Gosto de {INTERESSE}. Em seguida, escreva algumas chamadas para testar seu procedimento. Por exemplo:

    ParâmetroResultado Esperado
    programarGosto de programar
    computadoresGosto de computadores
  2. Adicione um valor lógico novo parâmetro ao procedimento do exercício anterior. Se o valor lógico for Verdadeiro, escreva Gosto de {INTERESSE}. Caso contrário, escreva Não gosto de {INTERESSE}.

    InteresseValor LógicoResultado Esperado
    programarVerdadeiroGosto de programar
    computadoresFalsoNão gosto de computadores
  3. Modifique o exercício anterior. Converta o procedimento para uma função que retorne uma cadeia de caracteres. Para combinar o texto, use concatenações.

  4. Escreva funções para as quatro operações aritméticas básicas com as seguintes assinaturas:

    • some(x, y)
    • subtraia(x, y)
    • multiplique(mutiplicando, multiplicador)
    • divida(dividendo, divisor)

    Caso necessário, crie uma versão para números inteiros e outra para números reais. Teste a divisão usando divisor 0 (não permite a ocorrência de erros).

  5. Escreva uma função que compare duas cadeias de caracteres de forma insensível à caixa. Você pode escolher converter as cadeias de caracteres para minúsculas ou maiúsculas.

  6. Escreva uma função que calcule o resultado de uma função de primeiro grau (função linear).

    O resultado a ser calculado corresponde a y (f(x)) na função. A assinatura da função pode ser funcao_linear(a, b, x). Os parâmetros e resultados devem ser números reais. Por exemplo, funcao_linear(1, 2, 3) = 5, pois corresponderia a .

  7. Escreva um procedimento que escreva o resultado de uma função de segundo grau (função quadrática).

  8. Escreva um procedimento que resolva uma equação de segundo grau.

    Para o cálculo de equação do segundo grau da forma que admita raízes reais, o discriminante não pode ser negativo. No caso, o resultado desejado é x.

    Se , escreva A equação não possui raízes reais. Caso contrário, escreva as raízes. O cálculo das raízes pode ser feito usando a expressão:

    Para isso, você pode calcular cada raiz separadamente:

  9. Escreva uma função que calcule uma combinação matemática.

    e são números naturais, com . A exclamação representa números fatoriais.

    Para conferir sua solução, você pode utilizar os casos de teste a seguir.

    nkResultado Esperado ()
    001
    501
    515
    5210
    5310
    545
    551
    107120

    Valores negativos devem apresentar uma mensagem de erro.

  10. A série de Fibonacci é definida como:

    Escreva uma subrotina que calcule o número de Fibonacci , sendo um número natural. Para verificar se seu programa está correto, você pode considerar os seguintes casos de teste:

    nResultado Esperado ()
    00
    11
    21
    32
    43
    55
    68
    713
    821
    934
    1055
  11. Em uma subrotina com passagem de valores por valor, o que ocorre caso se altere o valor de um parâmetro na definição da subrotina? Pense e tente responder à pergunta. Em seguida, teste os programas a seguir e explique os resultados. O procedimento troca() funciona corretamente? Por que?

    function troca(x, y) {
        let temporario = x
        x = y
        y = x
        console.log("troca() ", x, y)
    }
    
    let a = 1
    let b = 2
    troca(a, b)
    console.log(a, b)
    
    let x = 1
    let y = 2
    troca(x, y)
    console.log(x, y)
    def troca(x, y):
        temporario = x
        x = y
        y = x
        print("troca() ", x, y)
    
    a = 1
    b = 2
    troca(a, b)
    print(a, b)
    
    x = 1
    y = 2
    troca(x, y)
    print(x, y)
    function troca(x, y)
        local temporario = x
        x = y
        y = x
        print("troca() ", x, y)
    end
    
    local a = 1
    local b = 2
    troca(a, b)
    print(a, b)
    
    local x = 1
    local y = 2
    troca(x, y)
    print(x, y)
    extends Node
    
    func troca(x, y):
        var temporario = x
        x = y
        y = x
        print("troca() ", x, y)
    
    func _ready():
        var a = 1
        var b = 2
        troca(a, b)
        print(a, b)
    
        var x = 1
        var y = 2
        troca(x, y)
        print(x, y)

    Em linguagens que permitem a passagem de parâmetros por referência, seria possível definir um verdadeiro procedimento troca(). Contudo, em linguagens de programação no qual o operador igual pode fazer atribuições para múltiplas variáveis de uma única vez (como Python e Lua), bastaria fazer x, y = y, x.

  12. Em linguagens de programação que não definam um ponto de entrada com nome específico, você pode criar sua própria função ou procedimento main() como subrotina inicial de seu programa.

    function main() {
        console.log("Olá, meu nome é Franco!")
    }
    
    main()
    def main():
        print("Olá, meu nome é Franco!")
    
    main()
    function main()
        print("Olá, meu nome é Franco!")
    end
    
    main()
    extends Node
    
    # GDScript define _init() e _ready().
    func _ready():
        print("Olá, meu nome é Franco!")

    Ao invés de adicionar instruções após a definição das subrotinas, chama-se a subrotina proposta como início do programa. O nome escolhido foi main(), mas você poderia chamá-la do nome que quisesse (por exemplo, inicio() ou franco()). Todas as instruções do programa que não fossem subrotinas poderiam estar dentro de main().

    Em Python, é possível usar uma estrutura condicional para tornar a função um verdadeiro ponto de entrada para o programa.

    def main():
        print("Olá, meu nome é Franco!")
    
    if (__name__ == "__main__"):
        main()

    Quais vantagens você citaria para a definição de pontos de entrada específicos?

Próximos Passos

Subrotinas são um recurso importante de linguagens de programação. Dada a importância, este tópico detalhou algumas técnicas para usá-las em seus projetos.

Subrotinas permitem organizar projetos, modularizar soluções, reusar código e testar implementações com maior facilidade. Com nomes adequados e boa modularização, elas também podem facilitar a leitura e entendimento do código de um projeto.

Além disso, a recursividade permite definir repetições. Contudo, ela não é a única forma.

Como adiantado em estruturas condicionais, existem também estruturas de repetição (também chamada de laços ou loops) para a criação de código que se repete mediante uma condição. Elas serão o próximo tópico de estudo.

  1. Introdução;
  2. Ponto de entrada e estrutura de programa;
  3. Saída (para console ou terminal);
  4. Tipos de dados;
  5. Variáveis e constantes;
  6. Entrada (para console ou terminal);
  7. Aritmética e Matemática básica;
  8. Operações relacionais e comparações;
  9. Operações lógicas e Álgebra Booleana;
  10. Estruturas de condição (ou condicionais ou de seleção);
  11. Subrotinas: funções e procedimentos;
  12. Estruturas de repetição (ou laços ou loops);
  13. Vetores (arrays), cadeias de caracteres (strings), coleções (collections) e estruturas de dados;
  14. Registros (structs ou records);
  15. Arquivos e serialização (serialization ou marshalling);
  16. Bibliotecas;
  17. Entrada em linha de comando;
  18. Operações bit-a-bit (bitwise operations);
  19. Testes e depuração.
  • Informática
  • Programação
  • Iniciante
  • Pensamento Computacional
  • Aprenda a Programar
  • Python
  • Lua
  • Javascript
  • Godot
  • Gdscript
  • Scratch
  • Flowgorithm