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: Registros (Structs ou Records)

Exemplos de uso de registros em quatro linguagens de programação: 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.

Abstração e Decomposição de Dados

Uma coleção (como um vetor ou um dicionário) permite armazenar valores em uma única variável. Algumas linguagens (sobretudo linguagens de alto nível) permitem armazenar valores de diferentes tipos em uma mesma variável de coleção (por exemplo, em um vetor). Outras linguagens (especialmente linguagens de nível mais baixo, como C e C++) admitem armazenar um único tipo de dados em uma variável de coleção.

Algo similar ocorre com funções (ou subrotinas). Algumas linguagens (como Lua e Python) permitem o retorno de múltiplos valores; outras (como JavaScript e GDScript) impõe que uma função possa retornar um único valor.

No caso de subrotinas, uma forma de retornar vários valores consiste no uso de coleções. No caso de estruturas de dados, pode-se usar técnicas como vetores paralelos como forma de distribuir diversos valores para uma mesma entidade.

Todavia, existe uma forma alternativa para evitar o problema, que é aplicável tanto para o caso de subrotinas quanto para coleções. Registros (structures, structs ou records) permitem agrupar múltiplos dados para a composição de um novo tipo composto de dados. Uma variável do tipo de um registro combina várias outras variáveis em uma só.

Assim como subrotinas permitem realizar decomposição funcional, registros fornecem uma forma de exercer uma decomposição de dados de um programa (ela também possa ser pensada como uma composição de dados, dependendo do sentido escolhido). Isso permite pensar nos dados de um programa em um nível mais alto, expandindo o conceito de abstração de dados. Por exemplo, ao invés de pensar em dados de uma pessoa como nome, idade e gênero, pode-se definir um tipo de dados Pessoa que seja composto por nome, idade e gênero. Toda variável criada usando o tipo Pessoa terá os dados pertinentes para uma pessoa no problema ou domínio considerado.

Registros (Structs ou Records) e Classes

Muitas linguagens de programação fornecem recursos para que uma programadora ou um programador possa criar seus próprios tipos de dados. Os recursos, contudo, podem variar.

Linguagens imperativas (paradigma procedural ou imperativo) costumam permitir a definição de registros ou estruturas (structures, structs ou records). Em geral, registros permitem combinar dados de tipos pré-existentes em um novo tipo. Os dados de um registro são comumente chamados de atributos (attributes) ou campos (fields). Os valores de atributos ou campos definem o estado do registro. Essa é a abordagem mais simples e básica para a criação de tipos compostos.

Em linguagens que permitam armazenar referências para subrotinas em variáveis, um registro pode conter dados e subrotinas. Em linguagens que não permitem, um registro pode ter apenas dados. Para diferenciar ambas, pode-se usar o termo Plain Old Data (POD) ou Passive Data Structure (PDS) para se referir a registros que contenham apenas dados. A expressão plain old pode ser entendida como "bom e velho"; ou seja, os bons e velhos dados -- simples, confiáveis e sem extravagâncias. Em outras palavras, sem surpresas: dados puros, não misturados com código.

Linguagens orientadas a objeto (paradigma da Programação Orientada a Objetos (POO) ou Object-Oriented Programming (OOP)) permitem criar classes. Uma classe permitem combinar atributos com subrotinas para processá-los, chamadas métodos. Uma classe tende a ser contrário de um POD, porque ela permite misturar dados e código em uma única estrutura, chamada objeto. Um objeto é uma instância de uma classe.

Classes podem ser muito mais poderosas, sofisticadas e versáteis que PODs (doravante chamados registros). Em classes, pode-se usar técnicas como data hiding (que engloba o conceito de encapsulamento), herança, polimorfismo e delegação para definir processamentos complexos. Por exemplo, é possível permitir ou restringir o acesso a dados dependendo de como a classe é definida; em potencial, é possível restringir alterações de dados apenas usando métodos específicos. Assim, em classes, os dados podem ser meros detalhes. A manipulação de um objeto pode ser feita por vias exclusivas de métodos. Em outras palavras, pode-se abstrair os dados e operá-los usando-se interfaces. Em boas implementações usando OOP, pode-se manter-se uma mesma interface e modificar-se a definição de dados. O programa continuará a funcionar como se nunca mudara.

Em registros, existem apenas dados como atributos. Dados são variáveis que podem ser alteradas em qualquer lugar. Pode-se criar subrotinas para manipulá-los, mas nada impede modificações diretas. Atributos em registros são, portanto, modificáveis em qualquer local em que a variável seja acessível. Os atributos são reunidos apenas em um registro para conveniência de uso e acesso. Trata-se de um agrupamento ou composição, simples e sem surpresas.

Em pseudocódigo, um registro pode ser tão simples como a seguinte representação:

registro nome_do_registro
inicio
    atributo1: tipo1
    atributo2: tipo2
    // ...
    atributoN: tipoN
fim

variavel variavel_registro: nome_do_registro
variavel_registro.atributo1 = ...
variavel_registro.atributo2 = ...
// ...
variavel_registro.atributoN = ...

O tipo de um atributo pode ser qualquer tipo definido anteriormente. Isso significa que ele pode ser um tipo primitivo, um tipo composto (como os estudados anteriormente), e, em muitas linguagens de programação, de um tipo de registro declarado anteriormente.

Em geral, registros podem ser entendidos como um tipo de dados composto por outros tipos de dados. Linguagens de programação fornecerão um operador (tipicamente um ponto como ., no qual o nome da variável precede o ponto, e o nome do atributo sucede o ponto) para acesso a cada atributo definido.

Para o exemplo de um tipo Pessoa, o pseudocódigo ficaria:

registro Pessoa
inicio
    nome: caracteres
    idade: inteiro
    genero: caracteres
fim

variavel franco: Pessoa
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"

variavel voce: Pessoa
voce.nome = "???"
voce.idade = 4321
voce.genero = "???"

Pessoa poderia ter atributos para pronomes, para uma solução mais inclusiva.

Neste tópico, segue-se a convenção de que todo nome de tipo definido por registro será iniciado por uma letra maiúscula. Essa é uma convenção usual em muitas linguagens de programação e permite distingüir rapidamente o nome de um tipo do um identificador (como o nome uma variável ou subrotina).

OOP e Mau Uso de Objetos

Registros são mais simples; simplicidade é conveniente para o aprendizado inicial. Portanto, mesmo em linguagens que permitam programação orientada o objetos (como Python, JavaScript e GDScript), este tópico usará classes como se fossem registros. A escolha requer maturidade para uso e pode não ser exatamente recomendável para iniciantes. De fato, pessoas acostumadas a OOP podem discordar da abordagem ou sentirem-se incomodadas pelo (mau) uso de objetos como registros; as reações seriam justas e aceitáveis. Contudo, elas restringiriam o potencial para aprendizado.

Como habitualmente, minha opinião é que linguagens de programação são ferramentas. Paradigmas de programação também são ferramentas. Paradigmas são como lentes que auxiliam entender, modelar e resolver problemas. Eles não são soluções, embora possam influenciar como ocorrerá a construção de uma solução. As lentes destacam certos detalhes em detrimento de outros. Computação sempre possui contrapartidas.

A orientação a objetos não é um paradigma superior a todos os outros. Como qualquer outro paradigma, OOP tem benefícios e limitações. Entender registros como dados poderá permitir, posteriormente, usar classes e objetos com maior simplicidade e elegância, ao invés de explorar excessivamente idiomas e padrões de OOP mesmo quando eles não sejam realmente adequados ou necessários.

Assim, a abordagem adotada neste tópico não é um bom exemplo de OOP, principalmente nos exemplos iniciais. A escolha é intencional, pois OOP poderá ser detalhada no futuro com profundidade adequada. Ao invés de orientação a objetos, os paradigmas explorados neste tópico são imperativo e procedural.

Neste momento, o foco são os dados armazenados em tipos compostos. Em qualquer paradigma de programação, é fundamental entender o fluxos de dados de um programa. Compreender um problema e modelá-los em dados faz parte de toda solução. Planejar e conceber o fluxo de dados do programa é importante; para boas arquiteturas de software, entender o fluxo contribui para a criação de boas abstrações. Por outro lado, começar a criar abstrações antes de entender o problema pode levar a problemas de arquitetura a médio ou longo prazo. Criar classes e tipos de dados indiscriminadamente também, especialmente caso eles sejam forçadas para partes ou situações em que são inadequadas ou desnecessárias.

Dados não requerem OOP; para processá-los, pode-se usar todos os recursos explorados anteriormente. A abordagem escolhida permitirá explicar como se criar um modelo simples de orientação a objetos, com alguns recursos básicos. Para isso, bastará explorar conceitos já discutidos. Da simplicidade para a complexidade, em passos incrementais.

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

JavaScript, Python e GDScript permitem programação orientada a objetos. Lua não possui um conceito para registros nem para classes, embora seja possível implementar muitos recursos de OOP por meio de tabelas.

class Pessoa {
    constructor() {
        this.nome = ""
        this.idade = 0
        this.genero = ""
    }
}

// Também é possível fazer:
// class Pessoa {
//    nome = ""
//    idade = 0
//    genero = ""
//}

let franco = new Pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"

console.log(franco)
console.log(franco.nome)
console.log(franco.idade)
console.log(franco.genero)
// O nome poderia ser nova_pessoa() ou new_pessoa() ou Pessoa() para ficar
// mais próximo de outras linguagens de programação.
function crie_pessoa() {
    // JavaScript Object.
    return {
        "nome": "",
        "idade": 0,
        "genero": ""
    }
    // Ou:
    // return {
    //     nome: "",
    //     idade: 0,
    //     genero: ""
    // }
}

let franco = crie_pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"

console.log(franco)
console.log(franco.nome)
console.log(franco.idade)
console.log(franco.genero)
class Pessoa:
    def __init__(self):
        self.nome = ""
        self.idade = 0
        self.genero = ""

franco = Pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"

print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)
-- O nome poderia ser nova_pessoa() ou new_pessoa() ou Pessoa() para ficar
-- mais próximo de outras linguagens de programação.
function crie_pessoa()
    return {
        nome = "",
        idade = 0,
        genero = ""
    }
end

local franco = crie_pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"

print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)
extends Node

# Inner class.
class Pessoa:
    # Caso se omita um valor inicial, ele será null.
    var nome = ""
    var idade = 0
    var genero = ""

    # Um método _init() é opcional, como construtor.

func _ready():
    var franco = Pessoa.new()
    franco.nome = "Franco"
    franco.idade = 1234
    franco.genero = "Masculino"

    print(franco)
    print(franco.nome)
    print(franco.idade)
    print(franco.genero)
extends Node

# Arquivo como classe.
# Nome do arquivo é o nome da classe.
# Também é possível escolher um nome personalizado para a classe usando:
# class_name Pessoa

# Caso se omita um valor inicial, ele será null.
var nome
var idade
var genero

func _init():
    nome = ""
    idade = 0
    genero = ""

func _ready():
    nome = "Franco"
    idade = 1234
    genero = "Masculino"

    var franco = self
    printt(self, franco)
    printt(nome, self.nome, franco.nome)
    printt(idade, self.idade, franco.idade)
    printt(genero, self.genero, franco.genero)

Documentação para criação de registros ou classes:

JavaScript, Python e GDScript permitem a definição de classes. Algo que se pode notar é a existência de uma nova palavra reservada para referenciar a própria instância do objeto manipulado pela classe. Ela chama-se this em JavaScript, e self em Python e GDScript (Lua também propõe self como convenção para a própria variável, usada quando se chama uma subrotina usando o operador :; mais detalhes quando apropriado). Em linguagens como GDScript, em casos em que não exista ambigüidades para se determinar a variável (por exemplo, não exista um parâmetro de subrotina não tenha o mesmo nome de um atributo), é possível omitir o uso the self. Caso contrário, o uso é obrigatório para se referenciar o atributo da classe ao invés da outra variável.

Em JavaScript, a introdução de classes é relativamente recente (ECMAScript 2015). Antes de 2015, objetos em JavaScript eram criados como JavaScript Objects, previamente apresentados em Coleções como uma das alternativas de uso de dicionários. Atualmente, pode-se escolher usar classes nativas (definidas como class) ou JavaScript Objects.

Em GDScript, todo arquivo de código-fonte define uma classe. Em outras palavras, todos os programas escritos até agora na linguagem definiam classes. Variáveis declaradas fora de uma subrotina são atributos da classe (ao invés de variáveis globais). Todas as subrotinas declaradas em um arquivo na linguagem são métodos. Também é possível a criação de classes internas (inner classes), que podem ser usadas como registros. Uma classe interna é uma classe aninhada, ou seja, uma classe definida dentro de outra classe.

Nos códigos que definem classes, um método especial chamado construtor realiza a alocação e inicialização de um objeto (uma variável) do tipo da classes. Um construtor é chamado automaticamente toda vez que um objeto da classe é construído, para fazer a inicialização com os valores definidos. Isso permite que objetos em POO sempre tenham um estado inicial válido e conhecido (ao invés de aleatório, como pode ocorrer com variáveis de tipos primitivos). Em JavaScript, o construtor chama-se constructor() e é opcional. Em Python, ele chama-se __init__(), usando dois underscores antes e depois da palavra, e é obrigatório. Atributos em Python devem ser declarados na definição de __init()__ (caso contrário, eles serão compartilhados entre todas as instâncias da classe, algo conhecido como atributo estático). Em GDScript, ele chama-se _init() e é opcional.

Além disso, em códigos com classe, a linguagem pode exigir o uso de uma palavra reservada para a criação de um objeto. Em JavaScript, ela é new(). Em Python, deve-se usar parênteses após o nome da classe. Em GDScript, deve-se usar .new() após o nome da classe (interna).

Em Lua, registros podem ser criados como tabelas. A forma mais simples é declarar uma tabela com algumas chaves do tipo cadeia de caracteres. Para conveniência, pode-se definir uma função para criar um registro vazio. Isso foi feito em crie_pessoa() (tanto em Lua, quanto para JavaScript Object). Embora opcional, a construção simplifica a criação de vários registros com os mesmos atributos. Ao invés de duplicar código, basta chamar a subrotina. Isso é prático porque facilita a alteração de todas as variáveis do tipo quando se alterar a definição do registro (embora referências às variáveis antigas ainda precisem ser corrigidas manualmente).

Em Python, Lua e GDScript, escrever uma variável do tipo de um registro com print() escreverá o endereço da referência. Em JavaScript, console.log() escreverá os valores armazenados no objeto.

Passagem por Referência ou Passagem por Valor?

Para o uso de registros (ou classes como registros), os parágrafos anteriores são quase suficientes para começar a usá-los em programas. Um importante detalhe remanescente é saber que existem linguagens de programação que passam registros ou objetos para subrotinas por valor (por exemplo, C e C++), enquanto outras passam por referência (caso de JavaScript, Python, Lua e GDScript). Como JavaScript, Python, Lua e GDScript passam registros e objetos por referência, deve-se atentar que mudanças de valores de um parâmetro em uma subrotina afetarão a variável original passada como parâmetro, e serão persistentes após o término da chamada.

class Pessoa {
    constructor() {
        this.nome = ""
        this.idade = 0
        this.genero = ""
    }
}

function inicialize_pessoa(pessoa) {
    pessoa.nome = "Franco"
    pessoa.idade = 1234
    pessoa.genero = "Masculino"
}

let franco = new Pessoa()
console.log(franco)
console.log(franco.nome)
console.log(franco.idade)
console.log(franco.genero)

inicialize_pessoa(franco)
console.log(franco)
console.log(franco.nome)
console.log(franco.idade)
console.log(franco.genero)
function crie_pessoa() {
    // JavaScript Object.
    return {
        "nome": "",
        "idade": 0,
        "genero": ""
    }
}

function inicialize_pessoa(pessoa) {
    pessoa.nome = "Franco"
    pessoa.idade = 1234
    pessoa.genero = "Masculino"
}

let franco = crie_pessoa()
console.log(franco)
console.log(franco.nome)
console.log(franco.idade)
console.log(franco.genero)

inicialize_pessoa(franco)
console.log(franco)
console.log(franco.nome)
console.log(franco.idade)
console.log(franco.genero)
class Pessoa:
    def __init__(self):
        self.nome = ""
        self.idade = 0
        self.genero = ""

def inicialize_pessoa(pessoa):
    pessoa.nome = "Franco"
    pessoa.idade = 1234
    pessoa.genero = "Masculino"

franco = Pessoa()
print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)

inicialize_pessoa(franco)
print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)
function crie_pessoa()
    return {
        nome = "",
        idade = 0,
        genero = ""
    }
end

function inicialize_pessoa(pessoa)
    pessoa.nome = "Franco"
    pessoa.idade = 1234
    pessoa.genero = "Masculino"
end

local franco = crie_pessoa()
print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)

inicialize_pessoa(franco)
print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)
extends Node

class Pessoa:
    var nome
    var idade
    var genero

func inicialize_pessoa(pessoa):
    pessoa.nome = "Franco"
    pessoa.idade = 1234
    pessoa.genero = "Masculino"

func _ready():
    var franco = Pessoa.new()
    print(franco)
    print(franco.nome)
    print(franco.idade)
    print(franco.genero)

    inicialize_pessoa(franco)
    print(franco)
    print(franco.nome)
    print(franco.idade)
    print(franco.genero)
extends Node

var nome
var idade
var genero

func _init():
    nome = ""
    idade = 0
    genero = ""

func inicialize_pessoa():
    nome = "Franco"
    idade = 1234
    genero = "Masculino"

func inicialize_pessoa_com_parametro(pessoa):
    pessoa.nome = "Franco Garcia"
    pessoa.idade = 4321
    pessoa.genero = "MASCULINO"

func _ready():
    print(self)
    printt(nome, self.nome)
    printt(idade, self.idade)
    printt(genero, self.genero)

    # A chamada alterará o próprio objeto.
    # No caso, a alteração de valores é um efeito colateral da chamada.
    inicialize_pessoa()
    print(self)
    printt(nome, self.nome)
    printt(idade, self.idade)
    printt(genero, self.genero)

    # O código anterior é equivalente ao uso de self como parâmetro.
    inicialize_pessoa_com_parametro(self)
    print(self)
    printt(nome, self.nome)
    printt(idade, self.idade)
    printt(genero, self.genero)

O termo inicialize é usado como initialize, comumente abreviado como init (ou setup) em programação. Um segundo detalhe importante refere-se à comparação em linguagens que tratam objetos como referências.

Comparações: Igualdade e Diferença

Como variáveis que armazenam os valores em JavaScript, Python, Lua e GDScript armazenam referências, é importante tomar cuidado com comparações de igualdade e diferença. Elas devem ser realizadas membro a membro, ao invés de usar o operador de cada linguagem. Caso um atributo seja uma referência, ele também deverá ser comparado adequadamente.

class Pessoa {
    constructor() {
        this.nome = ""
        this.idade = 0
        this.genero = ""
    }
}

function pessoas_iguais(pessoa1, pessoa2) {
    return ((pessoa1.nome === pessoa2.nome) &&
            (pessoa1.idade === pessoa2.idade) &&
            (pessoa1.genero === pessoa2.genero))
}

function pessoas_diferentes(pessoa1, pessoa2) {
    return (!pessoas_iguais(pessoa1, pessoa2))
}

let pessoa1 = new Pessoa()
let pessoa2 = new Pessoa()
console.log(pessoa1 === pessoa2, pessoas_iguais(pessoa1, pessoa2))
console.log(pessoa1 !== pessoa2, pessoas_diferentes(pessoa1, pessoa2))
function crie_pessoa() {
    // JavaScript Object.
    return {
        "nome": "",
        "idade": 0,
        "genero": ""
    }
}

function pessoas_iguais(pessoa1, pessoa2) {
    return ((pessoa1.nome === pessoa2.nome) &&
            (pessoa1.idade === pessoa2.idade) &&
            (pessoa1.genero === pessoa2.genero))
}

function pessoas_diferentes(pessoa1, pessoa2) {
    return (!pessoas_iguais(pessoa1, pessoa2))
}

let pessoa1 = crie_pessoa()
let pessoa2 = crie_pessoa()
console.log(pessoa1 === pessoa2, pessoas_iguais(pessoa1, pessoa2))
console.log(pessoa1 !== pessoa2, pessoas_diferentes(pessoa1, pessoa2))
class Pessoa:
    def __init__(self):
        self.nome = ""
        self.idade = 0
        self.genero = ""

def pessoas_iguais(pessoa1, pessoa2):
    return ((pessoa1.nome == pessoa2.nome) and
            (pessoa1.idade == pessoa2.idade) and
            (pessoa1.genero == pessoa2.genero))

def pessoas_diferentes(pessoa1, pessoa2):
    return (not pessoas_iguais(pessoa1, pessoa2))

pessoa1 = Pessoa()
pessoa2 = Pessoa()
print(pessoa1 == pessoa2, pessoas_iguais(pessoa1, pessoa2))
print(pessoa1 != pessoa2, pessoas_diferentes(pessoa1, pessoa2))
function crie_pessoa()
    return {
        nome = "",
        idade = 0,
        genero = ""
    }
end

function pessoas_iguais(pessoa1, pessoa2)
    return ((pessoa1.nome == pessoa2.nome) and
            (pessoa1.idade == pessoa2.idade) and
            (pessoa1.genero == pessoa2.genero))
end

function pessoas_diferentes(pessoa1, pessoa2)
    return (not pessoas_iguais(pessoa1, pessoa2))
end

local pessoa1 = crie_pessoa()
local pessoa2 = crie_pessoa()
print(pessoa1 == pessoa2, pessoas_iguais(pessoa1, pessoa2))
print(pessoa1 ~= pessoa2, pessoas_diferentes(pessoa1, pessoa2))
extends Node

class Pessoa:
    var nome
    var idade
    var genero

func pessoas_iguais(pessoa1, pessoa2):
    return ((pessoa1.nome == pessoa2.nome) and
            (pessoa1.idade == pessoa2.idade) and
            (pessoa1.genero == pessoa2.genero))

func pessoas_diferentes(pessoa1, pessoa2):
    return (not pessoas_iguais(pessoa1, pessoa2))

func _ready():
    var pessoa1 = Pessoa.new()
    var pessoa2 = Pessoa.new()
    printt(pessoa1 == pessoa2, pessoas_iguais(pessoa1, pessoa2))
    printt(pessoa1 != pessoa2, pessoas_diferentes(pessoa1, pessoa2))
extends Node

class_name Pessoa

var nome
var idade
var genero

func _init():
    nome = ""
    idade = 0
    genero = ""

func pessoas_iguais(pessoa):
    return ((self.nome == pessoa.nome) and
            (self.idade == pessoa.idade) and
            (self.genero == pessoa.genero))

func pessoas_diferentes(pessoa):
    return (not pessoas_iguais(pessoa))

func _ready():
    var pessoa2 = get_script().new()
    printt(self == pessoa2, pessoas_iguais(pessoa2))
    printt(self != pessoa2, pessoas_diferentes(pessoa2))

    # Não é possível usar class_name para este caso.
    # var pessoa3 = Pessoa.new()
    # printt(self == pessoa3, pessoas_iguais(pessoa3))
    # printt(self != pessoa3, pessoas_diferentes(pessoa3))

A versão potencialmente mais confusa é a em GDScript utilizando o próprio arquivo como classe. O método get_script() documentação permite referenciar o próprio arquivo de código-fonte, usando para instanciar uma nova variável.

Em todos os casos anteriores, a comparação usando operadores retorna o valor incorreto, pois compara endereços. As duas variáveis devem ser iguais, pois ambas foram inicializadas com os mesmos valores (cadeias de caracteres vazias para nome e genero e 0 para idade). A comparação usando as subrotinas retorna o valor correto. Como os operadores de igualdade e diferença são o reverso um do outro, pode-se definir um dos dois. Em seguida, pode-se definir o segundo como a negação do primeiro.

Para ordenação de valores em vetores de registros, também pode ser interessante definir subrotinas que informem se um valor é menor que outro (para ordem crescente), ou maior que outro (para ordem decrescente). Por exemplo, um critério para ordenação de uma variável do tipo Pessoa poderia ser ordem alfabética para nome. Outro critério poderia ser idade ou gênero. Também é possível definir prioridades para a ordenação (por exemplo, primeiro por nome, depois por idade, depois por gênero). Para isso, basta realizar a próxima comparação com o critério imediatamente menos importante caso os valores comparados anteriormente sejam iguais.

Recursos Mais Avançados

Esta seção apresenta alguns recursos mais avançados para uso com registros e/ou classes. Eles são mais complexos, mas fornecem conveniências e praticidades para atividades de programação. Além disso, nem todo recurso estará disponível em toda linguagem de programação. Caso as subseções pareçam muito complexas, pode-se avançar para a seção Técnicas Usando Registros.

Sobrecarga de Operadores

Linguagens com recursos para sobrecarga de operadores permitir redefinir o comportamento de operadores para registros, potencialmente tornando o código mais legível. A sobrecarga de operadores foi mencionada previamente (por exemplo, em Operações Relacionais e Comparações). Neste momento, a técnica pode ser incorporada para outros operadores, como aritméticos, lógicos e relacionais.

Das linguagens consideradas para exemplos, Python e Lua permitem sobrecarregar operadores aritméticos, lógicos e relacionais. O exemplo a seguir sobrecarrega operador de igualdade e do operador de diferença para o registro Pessoa.

class Pessoa:
    def __init__(self):
        self.nome = ""
        self.idade = 0
        self.genero = ""

    # É importante notar a indentação.
    # A subrotina (método) está definida dentro da classe, no mesmo nível de
    # __init__().
    def __eq__(self, pessoa):
        if (type(self) != type(pessoa)):
            return false

        return ((self.nome == pessoa.nome) and
                (self.idade == pessoa.idade) and
                (self.genero == pessoa.genero))

    def __nq__(self, pessoa):
        return (not self == pessoa)

pessoa1 = Pessoa()
pessoa2 = Pessoa()
print(pessoa1 == pessoa2)
print(pessoa1 != pessoa2)
function pessoas_iguais(pessoa1, pessoa2)
    if (type(pessoa1) ~= type(pessoa2)) then
        return nil
    end

    return ((pessoa1.nome == pessoa2.nome) and
            (pessoa1.idade == pessoa2.idade) and
            (pessoa1.genero == pessoa2.genero))
end

function crie_pessoa()
    local dados = {
        nome = "",
        idade = 0,
        genero = ""
    }
    local metatable = {
        __eq = pessoas_iguais
    }

    local pessoa = setmetatable(dados, metatable)

    return pessoa
end

local pessoa1 = crie_pessoa()
local pessoa2 = crie_pessoa()
print(pessoa1 == pessoa2)
print(pessoa1 ~= pessoa2)

Python permite sobrecarregar todos os operadores descritos na documentação. No caso dos operadores de igualdade e diferença, caso se defina apenas o operador de igualdade, Python utiliza a negação dele automaticamente para criar o operador de diferença (ou vice-versa).

Lua utiliza metatable para sobrecarga de operadores. O conceito já foi comentado previamente (por exemplo, em Coleções). Este é um bom momento para explicá-lo. Lua usa metatable (meta-tabela) como uma forma de permitir redefinir o comportamento de operadores (e algumas operações) da linguagem. Para isso, deve-se definir uma tabela com chaves e funções para as operações desejadas, e associá-las a outra tabela (a que receberá as operações) usando setmetatable() (documentação).

Operadores e operações disponíveis variam de acordo com a versão da linguagem. Versões mais recentes possuem mais operadores. Por exemplo, documentação para versão 5.1 e documentação para versão 5.4. No caso de igualdade e diferença, Lua requer apenas a implementação do operador igualdade. O operador de diferença é criado automaticamente como a negação do operador de igualdade. Além disso, versões anteriores à 5.3 podem exigir uma mesma referência para a função. Por exemplo, o código a seguir funciona corretamente na versão atual (5.4) de Lua, mas não funciona até a versão 5.2. Até a versão 5.2, como a função anônima definida teria endereços diferentes, o operador sobrecarregado não seria chamado.

-- Requer Lua 5.3 ou mais recente.
function crie_pessoa()
    local dados = {
        nome = "",
        idade = 0,
        genero = ""
    }
    local metatable = {
        __eq = function(pessoa1, pessoa2)
            if (type(pessoa1) ~= type(pessoa2)) then
                return nil
            end

            return ((pessoa1.nome == pessoa2.nome) and
                    (pessoa1.idade == pessoa2.idade) and
                    (pessoa1.genero == pessoa2.genero))
        end
    }

    local pessoa = setmetatable(dados, metatable)
    -- Os endereços de __eq serão diferentes.
    -- print(metatable, getmetatable(pessoa), getmetatable(pessoa).__eq)

    return pessoa
end

local pessoa1 = crie_pessoa()
local pessoa2 = crie_pessoa()
print(pessoa1 == pessoa2)
print(pessoa1 ~= pessoa2)

A linha comentada em ambos os exemplos de Lua permite obter os endereços da função __eq() criada.

Uma alternativa possível é declarar a metatable de forma global ou local para o arquivo e usá-la para todas as instâncias criadas (como memória compartilhada). Como todas as variáveis criadas em crie_pessoa() terão a mesma metatable chamada metatable_pessoa, os endereços definidos serão os mesmos. Assim, o código a seguir é válido em versões anteriores à Lua 5.3 (e também em mais recentes). Uma segunda vantagem da abordagem é que se economiza memória.

local metatable_pessoa = {
    __eq = function(pessoa1, pessoa2)
        if (type(pessoa1) ~= type(pessoa2)) then
            return nil
        end

        return ((pessoa1.nome == pessoa2.nome) and
                (pessoa1.idade == pessoa2.idade) and
                (pessoa1.genero == pessoa2.genero))
    end
}

function crie_pessoa()
    local dados = {
        nome = "",
        idade = 0,
        genero = ""
    }

    local pessoa = setmetatable(dados, metatable_pessoa)

    return pessoa
end

local pessoa1 = crie_pessoa()
local pessoa2 = crie_pessoa()
print(pessoa1 == pessoa2)
print(pessoa1 ~= pessoa2)

Embora nem toda linguagem de programação forneça recursos para sobrecarga de operadores, eles podem ser úteis. Em particular, eles tendem a tornar a leitura de código mais simples. Por exemplo, ao invés de somar duas matrizes com some_matrizes(x, y), poder-se-ia sobrecarregar o operador + para permitir escrever x + y como soma de matrizes. A vantagem ficaria mais clara ao se considerar x + y + z, que seria escrito como some_matrizes(some_matrix(x, y), z) sem sobrecarga de operadores. A primeira versão é mais imediata (embora possa ocultar o custo computacional caso não se lembre que se trata de uma operação personalizada ao invés de uma soma convencional). Além disso, deve-se escolher operadores adequados. Por exemplo, sobrecarregar o operador / para realizar a soma das matrizes seria confuso (embora possível).

Polimorfismo

OOP define um conceito chamado polimorfismo, também comentado previamente (por exemplo, em Subrotinas (Funções e Procedimentos)). Com classes e herança em OOP, é possível fazer com que classes filhas (ou classes derivadas) possam redefinir métodos das respectivas classes pai (ou superclasse).

Como o objetivo atual não é introduzir detalhadamente OOP, os códigos a seguir são exemplos rápidos de como usar polimorfismo com tipos compostos para redefinir o operador para conversão de dados de um registro (classe) para cadeias de caracteres.

class Pessoa {
    constructor() {
        this.nome = ""
        this.idade = 0
        this.genero = ""
    }

    toString() {
        let resultado = this.nome + " (" + this.idade + " anos, gênero " + this.genero + ")"

        return resultado
    }
}

let franco = new Pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"

console.log(franco)
console.log("" + franco)

let texto = "Olá, " + franco + "!"
console.log(texto)
function Pessoa() {
    this.nome = ""
    this.idade = 0
    this.genero = ""
}

Pessoa.prototype.toString = function() {
    let resultado = this.nome + " (" + this.idade + " anos, gênero " + this.genero + ")"

    return resultado
}

let franco = new Pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"

console.log(franco)
console.log("" + franco)

let texto = "Olá, " + franco + "!"
console.log(texto)
class Pessoa:
    def __init__(self):
        self.nome = ""
        self.idade = 0
        self.genero = ""

    def __str__(self):
        resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"

        return resultado

    def __eq__(self, pessoa):
        if (type(self) != type(pessoa)):
            return false

        return ((self.nome == pessoa.nome) and
                (self.idade == pessoa.idade) and
                (self.genero == pessoa.genero))

    def __nq__(self, pessoa):
        return (not self == pessoa)

franco = Pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"

print(franco)

texto = "Olá, " + str(franco) + "!"
print(texto)
function pessoas_iguais(pessoa1, pessoa2)
    if (type(pessoa1) ~= type(pessoa2)) then
        return nil
    end

    return ((pessoa1.nome == pessoa2.nome) and
            (pessoa1.idade == pessoa2.idade) and
            (pessoa1.genero == pessoa2.genero))
end

function pessoa_para_string(pessoa)
    local resultado = pessoa.nome .. " (" .. pessoa.idade .. " anos, gênero " .. pessoa.genero .. ")"

    return resultado
end

function concatene(x, y)
    local resultado = tostring(x) .. tostring(y)
    return resultado
end

local metatable_pessoa = {
    __eq = pessoas_iguais,
    __tostring = pessoa_para_string,
    __concat = concatene
}

function crie_pessoa()
    local dados = {
        nome = "",
        idade = 0,
        genero = ""
    }

    local pessoa = setmetatable(dados, metatable_pessoa)

    return pessoa
end

local franco = crie_pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"

print(franco)
print(tostring(franco))
print("" .. franco)

texto = "Olá, " .. franco .. "!"
print(texto)
extends Node

class Pessoa:
    var nome
    var idade
    var genero

    func _to_string():
        var resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"

        return resultado

func _ready():
    var franco = Pessoa.new()
    franco.nome = "Franco"
    franco.idade = 1234
    franco.genero = "Masculino"

    print(franco)

    var texto = "Olá, " + str(franco) + "!"
    print(texto)
extends Node

var nome
var idade
var genero

func _init():
    nome = ""
    idade = 0
    genero = ""

func _ready():
    nome = "Franco"
    idade = 1234
    genero = "Masculino"

    var franco = self
    printt(self, franco)

    var texto = "Olá, " + str(franco) + "!"
    print(texto)

    texto = "Olá, " + str(self) + "!"
    print(texto)

func _to_string():
    var resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"

    return resultado

Em JavaScript, deve-se implementar o método toString() (documentação) para converter automaticamente um classe para cadeia de caracteres. Em Python, deve-se implementar __str__() (documentação). Em Lua, deve-se definir __tostring e __concat para a metatable (__tostring: documentação; __concat: documentação para versão 5.1 e documentação para versão 5.4). Em GDScript, deve-se implementar _to_string() (documentação).

O uso de polimorfismo para implementar a subrotina para conversão de registro (ou objeto) para cadeia de caracteres facilita conversões implícitas (ou explícitas). Uma segunda vantagem é comumente permitir a escrita dos conteúdos de um objeto em texto usando o comando ou subrotina padrão para escrita, como print().

Construtores

Um construtor é uma subrotina que aloca memória e inicializa valores iniciais de um objeto em OOP. Para registros, um construtor pode ser uma simples função. A função crie_pessoa(), por exemplo, inicializa os atributos do registro com valores considerados zeros (zero para números, Falso para valores lógicos, cadeia de caracteres vazia para texto). Com alguns parâmetros, ela poderia inicializar a variável recém-criada com valores fornecidos pela chamada.

class Pessoa {
    constructor(nome = "", idade = 0, genero = "") {
        this.nome = nome
        this.idade = idade
        this.genero = genero
    }

    toString() {
        let resultado = this.nome + " (" + this.idade + " anos, gênero " + this.genero + ")"

        return resultado
    }
}

let franco = new Pessoa("Franco", 1234, "Masculino")
console.log(franco)
console.log("" + franco)

let texto = "Olá, " + franco + "!"
console.log(texto)
function Pessoa(nome = "", idade = 0, genero = "") {
    this.nome = nome
    this.idade = idade
    this.genero = genero
}

Pessoa.prototype.toString = function() {
    let resultado = this.nome + " (" + this.idade + " anos, gênero " + this.genero + ")"

    return resultado
}

let franco = new Pessoa("Franco", 1234, "Masculino")
console.log(franco)
console.log("" + franco)

let texto = "Olá, " + franco + "!"
console.log(texto)
class Pessoa:
    def __init__(self, nome = "", idade = 0, genero = ""):
        self.nome = nome
        self.idade = idade
        self.genero = genero

    def __str__(self):
        resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"

        return resultado

    def __eq__(self, pessoa):
        if (type(self) != type(pessoa)):
            return false

        return ((self.nome == pessoa.nome) and
                (self.idade == pessoa.idade) and
                (self.genero == pessoa.genero))

    def __nq__(self, pessoa):
        return (not self == pessoa)

franco = Pessoa("Franco", 1234, "Masculino")
print(franco)

texto = "Olá, " + str(franco) + "!"
print(texto)
function pessoas_iguais(pessoa1, pessoa2)
    if (type(pessoa1) ~= type(pessoa2)) then
        return nil
    end

    return ((pessoa1.nome == pessoa2.nome) and
            (pessoa1.idade == pessoa2.idade) and
            (pessoa1.genero == pessoa2.genero))
end

function pessoa_para_string(pessoa)
    local resultado = pessoa.nome .. " (" .. pessoa.idade .. " anos, gênero " .. pessoa.genero .. ")"

    return resultado
end

function concatene(x, y)
    local resultado = tostring(x) .. tostring(y)
    return resultado
end

local metatable_pessoa = {
    __eq = pessoas_iguais,
    __tostring = pessoa_para_string,
    __concat = concatene
}

function crie_pessoa(nome, idade, genero)
    local dados = {
        nome = nome or "",
        idade = idade or 0,
        genero = genero or ""
    }

    local pessoa = setmetatable(dados, metatable_pessoa)

    return pessoa
end

local franco = crie_pessoa("Franco", 1234, "Masculino")
print(franco)
print(tostring(franco))
print("" .. franco)

texto = "Olá, " .. franco .. "!"
print(texto)
extends Node

class Pessoa:
    var nome
    var idade
    var genero

    func _init(nome = "", idade = 0, genero = ""):
        self.nome = nome
        self.idade = idade
        self.genero = genero

    func _to_string():
        var resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"

        return resultado

func _ready():
    var franco = Pessoa.new("Franco", 1234, "Masculino")
    print(franco)

    var texto = "Olá, " + str(franco) + "!"
    print(texto)
extends Node

var nome
var idade
var genero

func _init(nome = "", idade = 0, genero = ""):
    self.nome = nome
    self.idade = idade
    self.genero = genero

func _ready():
    nome = "Franco"
    idade = 1234
    genero = "Masculino"

    var franco = self
    printt(self, franco)

    var texto = "Olá, " + str(franco) + "!"
    print(texto)

    texto = "Olá, " + str(self) + "!"
    print(texto)

func _to_string():
    var resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"

    return resultado

Em muitas linguagens de programação, o nome de um parâmetro do construtor não precisa ser igual ao nome do atributo. Eles podem ser diferentes; além disso, os parâmetros da assinatura podem ser diferentes dos atributos declarados. Contudo, é bastante comum usar o mesmo nome para parâmetro e variável, quando se trata de inicialização direta. Aliás, quando todos os parâmetros são atribuídos diretamente aos atributos sem nenhum processamento (como validação), deve-se considerar se o uso de um simples registro seria melhor que a adoção de uma classe.

De qualquer forma, o uso de um mesmo nome gera ambigüidades, pois existem duas variáveis diferentes em um mesmo escopo com nomes idênticos. Nos exemplos anteriores, o uso de this ou self remove ambigüidades em casos assim. O uso de self ou this referencia explicitamente o valor armazenado na classe ou no registro, ao invés da variável local ou do parâmetro (que é referenciada sem o uso da palavra reservada).

Ademais, o construtor pode ter código adicional para validação de dados (ao invés de simples atribuições de valores), caso necessário. Além disso, em linguagens de programação que permitam a definição de valores padrão para parâmetros, eles podem ser usados para criar campos opcionais em construtores.

Por exemplo, em JavaScript:

  • new Pessoa() cria uma variável com nome "", idade 0 e gênero "".
  • new Pessoa("Franco") cria uma variável com nome "Franco", idade 0 e gênero "".
  • new Pessoa("Franco", 1234) cria uma variável com nome "Franco", idade 1234 e gênero "".
  • new Pessoa("Franco", 1234, "Masculino") cria uma variável com nome "Franco", idade 1234 e gênero "Masculino".

É importante notar que não se pode omitir parâmetros intermediários. Por exemplo, new Pessoa("Franco", "Masculino") atribuiria "Masculino" à idade. Como JavaScript não faz verificação de tipos, a atribuição seria válida. Uma forma de evitar o problema seria usar asserções, como será comentado em uma subseção.

Atenção. Em Python, a inicialização de um parâmetro padrão não pode ser um tipo de referência; todo tipo de referência deve ser inicializado para cada objeto. Caso contrário, os dados podem ser compartilhados. Assim, exceto caso o valor seja de um tipo primitivo, é melhor usar None como valor padrão, para, então, inicializar o valor desejado (por exemplo, um vetor, dicionário ou objeto) no construtor.

Subrotinas como Construtores

Pode ser interessante observar que, caso se alterasse o nome de crie_pessoa() para Pessoa(), o resultado em Lua tornar-se-ia bastante similar a construtores em linguagens de programação com suporte para classes.

-- Restante da implementação...

function Pessoa(nome, idade, genero)
    local dados = {
        nome = nome or "",
        idade = idade or 0,
        genero = genero or ""
    }

    local pessoa = setmetatable(dados, metatable_pessoa)

    return pessoa
end

local franco = Pessoa("Franco", 1234, "Masculino")

O resultado é uma subrotina para criar um registro de forma similar a Python e JavaScript. Outra possibilidade seria incorporar uma subrotina para construção (por exemplo, new()) em uma tabela chamada Pessoa.

-- Restante da implementação...

local Pessoa = {
    new = function(nome, idade, genero)
      local dados = {
          nome = nome or "",
          idade = idade or 0,
          genero = genero or "",
      }

      local pessoa = setmetatable(dados, metatable_pessoa)

      return pessoa
    end
}

local franco = Pessoa.new("Franco", 1234, "Masculino")

No segundo caso, o resultado é semelhante à instanciação de objetos feita em GDScript: Pessoa.new().

Assim, conhecendo-se os fundamentos e técnicas de programação, pode-se usar recursos existentes em uma linguagem para incorporar algumas funcionalidades presentes em outras. De certa forma, é como criar sua própria linguagem personalizada (ou dialeto personalizado) usando a linguagem original como matéria-prima. Embora o resultado nem sempre seja idiomático, incorporar recursos que permitam a você usar uma linguagem com mais eficiência é algo conveniente e poderoso.

Como de costume, linguagens de programação são ferramentas. Em potencial, elas são ferramentas que podem criar novas ferramentas. Meta-ferramentas, por assim dizer. Adapte-as conforme suas necessidades para que elas possam atendê-la ou atendê-lo cada vez melhor.

Parâmetros Nomeados

Embora não seja possível da forma convencional (usando parâmetros posicionais), existem formas de omitir parâmetros intermediários. A primeira delas é mais restrita, embora a melhor alternativa. Algumas linguagens de programação (como Python) fornecem um recurso chamado parâmetro nomeado (named parameter). Por exemplo, em Python:

  • Pessoa() cria uma variável com nome "", idade 0 e gênero "".
  • Pessoa("Franco") cria uma variável com nome "Franco", idade 0 e gênero "".
  • Pessoa("Franco", 1234) cria uma variável com nome "Franco", idade 1234 e gênero "".
  • Pessoa("Franco", 1234, "Masculino") cria uma variável com nome "Franco", idade 1234 e gênero "Masculino".
  • Pessoa(nome = "Franco") cria uma variável com nome "Franco", idade 0 e gênero "".
  • Pessoa(nome = "Franco", idade = 1234) cria uma variável com nome "Franco", idade 1234 e gênero "".
  • Pessoa(nome = "Franco", genero = "Masculino") cria uma variável com nome "Franco", idade 0 e gênero "Masculino".

Com parâmetros nomeados, a ordem e a quantidade de parâmetros não importa.

class Pessoa:
    def __init__(self, nome = "", idade = 0, genero = ""):
        self.nome = nome
        self.idade = idade
        self.genero = genero

    def __str__(self):
        resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"

        return resultado

    def __eq__(self, pessoa):
        if (type(self) != type(pessoa)):
            return false

        return ((self.nome == pessoa.nome) and
                (self.idade == pessoa.idade) and
                (self.genero == pessoa.genero))

    def __nq__(self, pessoa):
        return (not self == pessoa)

print(Pessoa())
print(Pessoa("Franco"))
print(Pessoa("Franco", 1234))
print(Pessoa("Franco", 1234, "Masculino"))
print(Pessoa(nome = "Franco"))
# Exemplos com parâmetros fora de ordem.
print(Pessoa(idade = 1234, nome = "Franco"))
print(Pessoa(genero = "Masculino", idade = 1234))
print(Pessoa(genero = "Masculino", nome = "Franco", idade = 1234))

Todos as chamadas do construtor Pessoa() no exemplo anterior são válidas e inicializam um novo objeto com os valores escolhidos para cada parâmetro nomeado.

Em linguagens sem parâmetros nomeados, é possível passar um dicionário para simular a técnica. Como a ordem de chaves em um dicionário é irrelevante, a passagem pode ser feita em qualquer ordem. Da mesma forma, caso não se defina a chave, pode-se usar um valor padrão para inicializar o valor ignorado.

Em particular, a técnica é comum em JavaScript e possui um recurso para facilitar o uso, chamado de atribuição via desestruturação (destructuring assignment; documentação). A atribuição via desestruturação permite fazer:

let {x, y} = {"x": 1, "y": 2}
console.log(x)
console.log(y)

Ao invés de:

let d = {"x": 1, "y": 2}
let x = d["x"]
let y = d["y"]
console.log(x)
console.log(y)

Assim, a versão em JavaScript será mais legível que a definida para Lua e GDScript. Lua e GDScript precisam usar a versão usual de acesso a valores em dicionários. Como Python permite o uso de parâmetros nomeados, omitiu-se uma implementação para a linguagem.

class Pessoa {
    constructor({nome, idade, genero} = {nome: "", idade: 0, genero: ""}) {
        // Se nome for undefined ou null, inicializa com o valor padrão.
        this.nome = (nome) ? nome : ""
        this.idade = (idade) ? idade : 0
        this.genero = (genero) ? genero : ""
    }

    toString() {
        let resultado = this.nome + " (" + this.idade + " anos, gênero " + this.genero + ")"

        return resultado
    }
}

let franco = new Pessoa({
    nome: "Franco",
    idade: 1234,
    genero: "Masculino"
})
console.log(franco)
console.log("" + franco)

let texto = "Olá, " + franco + "!"
console.log(texto)
function Pessoa({nome, idade, genero} = {nome: "", idade: 0, genero: ""}) {
    // Se nome for undefined ou null, inicializa com o valor padrão.
    this.nome = (nome) ? nome : ""
    this.idade = (idade) ? idade : 0
    this.genero = (genero) ? genero : ""
}

Pessoa.prototype.toString = function() {
    let resultado = this.nome + " (" + this.idade + " anos, gênero " + this.genero + ")"

    return resultado
}

let franco = new Pessoa({
    nome: "Franco",
    idade: 1234,
    genero: "Masculino"
})
console.log(franco)
console.log("" + franco)

let texto = "Olá, " + franco + "!"
console.log(texto)
function pessoas_iguais(pessoa1, pessoa2)
    if (type(pessoa1) ~= type(pessoa2)) then
        return nil
    end

    return ((pessoa1.nome == pessoa2.nome) and
            (pessoa1.idade == pessoa2.idade) and
            (pessoa1.genero == pessoa2.genero))
end

function pessoa_para_string(pessoa)
    local resultado = pessoa.nome .. " (" .. pessoa.idade .. " anos, gênero " .. pessoa.genero .. ")"

    return resultado
end

function concatene(x, y)
    local resultado = tostring(x) .. tostring(y)
    return resultado
end

local metatable_pessoa = {
    __eq = pessoas_iguais,
    __tostring = pessoa_para_string,
    __concat = concatene
}

function crie_pessoa(dados)
    dados = dados or {}
    local dados_pessoa = {
        nome = dados.nome or "",
        idade = dados.idade or 0,
        genero = dados.genero or ""
    }

    local pessoa = setmetatable(dados_pessoa, metatable_pessoa)

    return pessoa
end

local franco = crie_pessoa({
    nome = "Franco",
    idade = 1234,
    genero = "Masculino"
})
print(franco)
print(tostring(franco))
print("" .. franco)

texto = "Olá, " .. franco .. "!"
print(texto)
extends Node

class Pessoa:
    var nome
    var idade
    var genero

    func _init(dados = {}):
        self.nome = dados.get("nome", "")
        self.idade = dados.get("idade", 0)
        self.genero = dados.get("genero", "")

    func _to_string():
        var resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"

        return resultado

func _ready():
    var franco = Pessoa.new({
        nome = "Franco",
        idade = 1234,
        genero = "Masculino"
    })
    print(franco)

    var texto = "Olá, " + str(franco) + "!"
    print(texto)
extends Node

var nome
var idade
var genero

func _init(dados = {}):
    self.nome = dados.get("nome", "")
    self.idade = dados.get("idade", 0)
    self.genero = dados.get("genero", "")

func _ready():
    _init({
        nome = "Franco",
        idade = 1234,
        genero = "Masculino"
    })

    var franco = self
    printt(self, franco)

    var texto = "Olá, " + str(franco) + "!"
    print(texto)

    texto = "Olá, " + str(self) + "!"
    print(texto)

func _to_string():
    var resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"

    return resultado

Em GDScript usando o arquivo como classe, o exemplo fica um pouco estranho. Uma alternativa melhor seria exportar variáveis para edição visual no editor, usando a palavra reservada export antes de declaração de cada variável (documentação; a versão em desenvolvimento também possui um tutorial com novos recursos que aparecerão na versão 4 de Godot Engine). Como ainda não se detalhou o uso do editor, a informação serve como curiosidade neste momento.

Exceto em JavaScript, que permite a inicialização usando atribuição via desestruturação, eu particularmente não recomendaria o uso da técnica. A razão é que ela oculta os parâmetros esperados da definição da subrotina, exigindo documentação ou o uso de um valor padrão como exemplo.

Métodos

Subrotinas podem ser funções, procedimentos ou métodos. Funções e procedimentos são subrotinas independentes. Em OOP, métodos são funções ou procedimentos atrelados a classes, que podem acessar e modificar o estado (os atributos) da classe.

Uma forma de pensar um método é como se fosse uma função que recebe uma variável do tipo da classe como primeiro parâmetro. Esse parâmetro pode ser chamado, por exemplo, de this ou self. Em algumas linguagens de programação, isso é exatamente o que ocorre.

const IDADE_MINIMA_MAIORIDADE_CIVIL = 18

class Pessoa {
    constructor(nome = "", idade = 0, genero = "") {
        this.nome = nome
        this.idade = idade
        this.genero = genero
    }

    possui_maioridade_civil() {
        return (this.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
    }
}

let franco = new Pessoa("Franco", 1234, "Masculino")
console.log(franco.possui_maioridade_civil())
const IDADE_MINIMA_MAIORIDADE_CIVIL = 18

function possui_maioridade_civil(pessoa) {
    if (!pessoa.idade) {
        return undefined
    }

    return (pessoa.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
}

function crie_pessoa(nome = "", idade = 0, genero = "") {
    return {
        "nome": nome,
        "idade": idade,
        "genero": genero,
        "possui_maioridade_civil": possui_maioridade_civil
    }
}

let franco = crie_pessoa("Franco", 1234, "Masculino")
console.log(franco.possui_maioridade_civil(franco))
from typing import Final

IDADE_MINIMA_MAIORIDADE_CIVIL: Final = 18

class Pessoa:
    def __init__(self, nome = "", idade = 0, genero = ""):
        self.nome = nome
        self.idade = idade
        self.genero = genero

    def possui_maioridade_civil(self):
        return (self.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)

franco = Pessoa("Franco", 1234, "Masculino")
print(franco.possui_maioridade_civil())
local IDADE_MINIMA_MAIORIDADE_CIVIL <const> = 18

function possui_maioridade_civil(pessoa)
    if (pessoa.idade == nil) then
        return false
    end

    return (pessoa.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
end

function crie_pessoa(nome, idade, genero)
    local pessoa = {
        nome = nome or "",
        idade = idade or 0,
        genero = genero or "",
        -- Chave                  Referência para função
        possui_maioridade_civil = possui_maioridade_civil
    }

    return pessoa
end

local franco = crie_pessoa("Franco", 1234, "Masculino")
print(franco.possui_maioridade_civil(franco))
-- O operador : passa a própria variável como primeiro parâmetro para o método chamado.
print(franco:possui_maioridade_civil())
extends Node

const IDADE_MINIMA_MAIORIDADE_CIVIL = 18

class Pessoa:
    var nome
    var idade
    var genero

    func _init(nome = "", idade = 0, genero = ""):
        self.nome = nome
        self.idade = idade
        self.genero = genero

    func possui_maioridade_civil():
        # Ou return (self.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
        return (idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)

func _ready():
    var franco = Pessoa.new("Franco", 1234, "Masculino")
    print(franco.possui_maioridade_civil())
extends Node

const IDADE_MINIMA_MAIORIDADE_CIVIL = 18

var nome
var idade
var genero

func _init(nome = "", idade = 0, genero = ""):
    self.nome = nome
    self.idade = idade
    self.genero = genero

func _ready():
    nome = "Franco"
    idade = 1234
    genero = "Masculino"

    var franco = self
    printt(self, franco)
    printt(possui_maioridade_civil(), franco.possui_maioridade_civil())

func possui_maioridade_civil():
    # Ou: return (self.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
    return (idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)

Nos exemplos, linguagens com suporte a classes (e usando class, no caso de JavaScript) permitem definir subrotinas dentro da definição da classe. Isso foi feito previamente para polimorfismo e sobrecarga de operadores. A inclusão em uma classe de uma subrotina arbitrária definida por uma programadora ou um programador define um método.

No exemplo para JavaScript Object, a chamada deve incluir a própria variável: franco.possui_maioridade_civil(franco). O mesmo ocorre em um dos exemplos em Lua.

Para comodidade de uso, Lua define o operador : para uso com table. O operador passa a própria variável usada como primeiro parâmetro para a subrotina chamada. É por isso que subrotinas como table.insert(minha_tabela, valor) podem ser chamadas como minha_tabela:insert(valor). A criação de uma table em Lua incluir insert() e outras subrotinas para manipulação de tabelas como atributos (são variáveis que armazenam a função genérica por referência). O mesmo ocorre com subrotinas para string em Lua.

Caso se quisesse fazer algo similar ao que ocorre em Lua para JavaScript, uma possibilidade seria definir uma função anônima (lambda) que chamasse a função original.

const IDADE_MINIMA_MAIORIDADE_CIVIL = 18

function possui_maioridade_civil(pessoa) {
    if (!pessoa.idade) {
        return undefined
    }

    return (pessoa.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
}

function crie_pessoa(nome = "", idade = 0, genero = "") {
    let pessoa = {
        "nome": nome,
        "idade": idade,
        "genero": genero
    }
    pessoa["possui_maioridade_civil"] = function() {
        return possui_maioridade_civil(pessoa)
    }

    return pessoa
}

let franco = crie_pessoa("Franco", 1234, "Masculino")
console.log(franco.possui_maioridade_civil())

Com a alteração, a chamada franco.possui_maioridade_civil() passaria a funcionar com JavaScript Object. JavaScript define fechamentos (closures; documentação) para subrotinas, permitindo o uso da variável local pessoa definida em crie_pessoa() na função anônima criada.

Contudo, isso não é possível em toda linguagem de programação. Para fazer isso em outras linguagens de programação, é necessário que a linguagem forneça funções anônimas, e recursos como closures ou captura de variáveis.

Programação Orientada a Objetos (OOP) em Lua

Com as informações atuais, é possível começar a prototipar um sistemas simples de orientação a objetos em Lua. Esta seção serve mais como curiosidade neste momento; não é necessário entendê-la para seguir o restante deste tópico.

Com metatable, pode-se tornar a versão do código em Lua mais próximo de código em OOP. A abordagem é detalhada em Programming in Lua (Capítulo 16) e (Seção 16.1). O código a seguir é uma adaptação da abordagem.

local MINIMUM_CIVIL_IDADE_OF_MAJORITY = 18

-- Pessoa serve como tipo e como metatable para o tipo.
local Pessoa = {
    nome = "",
    idade = 0,
    genero = ""
}

function Pessoa:tem_maioridade_civil()
    -- self é o parâmetro implícito provido pelo operador :.
    if (self.idade == nil) then
        return false
    end

    return (self.idade >= MINIMUM_CIVIL_IDADE_OF_MAJORITY)
end

-- Este é o construtor.
function Pessoa:new(nome, idade, genero)
    local result = {}
    -- self atua como metatable para Pessoa.
    setmetatable(result, self)
    self.__index = self

    result.nome = nome or self.nome
    result.idade = idade or self.idade
    result.genero = genero or self.genero

    return result
end

-- NOTA A criação precisa usar : ao invés de .
local franco = Pessoa:new("Franco", 1234, "Male")
print(franco.nome, franco.idade, franco.genero)

local voce = Pessoa:new("Voce", 4321, "???")
print(voce.nome, voce.idade, voce.genero)

-- Todas essas chamadas acessam dados de franco.
print(franco.nome, franco.idade, franco.genero)
print(franco.tem_maioridade_civil(franco))
print(franco:tem_maioridade_civil())

A definição da subrotina usando : (como em Pessoa:tem_maioridade_civil() e Pessoa:new()) adiciona um parâmetro implícito self na assinatura. Assim, por exemplo, Pessoa:new() equivale a Pessoa:new(self).

A metatable usa Pessoa como definição da classe. A configuração de __index modifica o comportamento da indexação de tabelas. Ela permite usar subrotinas e atributos de Pessoa em uma nova variável (objeto) criada por Pessoa.new(). Como nome, idade e genero são atributos de tipos primitivos, o resultado são cópias independentes. Entretanto, se um atributo fosse uma tabela (por exemplo, um vetor ou dicionário), a variável seria compartilhada, requerendo o a criação de uma cópia profunda.

O próximo exemplo destaca alternativas para a definição de métodos em Lua. Em particular, deve-se evitar uma das formas: NomeClasse.nome_metodo(), porque ela usa a metatable NomeClasse ao invés da variável criada por NomeClasse:new(). As outras formas são equivalentes.

local MINIMUM_CIVIL_IDADE_OF_MAJORITY = 18

-- Pessoa serve como tipo e como metatable para o tipo.
local Pessoa = {
    nome = "",
    idade = 0,
    genero = "",
    -- Este é um método virtual.
    tem_maioridade_civil = function(self)
                             if (self.idade == nil) then
                                 return false
                             end

                             return (self.idade >= MINIMUM_CIVIL_IDADE_OF_MAJORITY)
                         end
}

-- Este é um método virtual.
Pessoa.tem_maioridade_civil_alternativa_1 = function(self)
    if (self.idade == nil) then
        return false
    end

    return (self.idade >= MINIMUM_CIVIL_IDADE_OF_MAJORITY)
end

-- Este é um método virtual.
function Pessoa:tem_maioridade_civil_alternativa_2()
    -- self é o parâmetro implícito provido pelo operador :.
    if (self.idade == nil) then
        return false
    end

    return (self.idade >= MINIMUM_CIVIL_IDADE_OF_MAJORITY)
end

-- Este é um método da classe base.
-- Ele terá um resultado diferente.
-- Em OOP, ele acessaria os dados da classe pai (superclasse).
function Pessoa.tem_maioridade_civil_alternativa_3()
    -- Pessoa is the local variable defined previously.
    if (Pessoa.idade == nil) then
        return false
    end

    -- Isso significa que idade é 0.
    -- print(Pessoa.idade)

    return (Pessoa.idade >= MINIMUM_CIVIL_IDADE_OF_MAJORITY)
end

-- Este é o construtor.
function Pessoa:new(nome, idade, genero)
    local result = {}
    -- self atua como metatable para Pessoa.
    setmetatable(result, self)
    self.__index = self

    result.nome = nome or self.nome
    result.idade = idade or self.idade
    result.genero = genero or self.genero

    return result
end

-- NOTA A criação precisa usar : ao invés de .
local franco = Pessoa:new("Franco", 1234, "Male")
print(franco.nome, franco.idade, franco.genero)

local voce = Pessoa:new("Voce", 4321, "???")
print(voce.nome, voce.idade, voce.genero)

-- Todas essas chamadas acessam dados de franco.
print(franco.nome, franco.idade, franco.genero)
print(franco.tem_maioridade_civil(franco))
print(franco:tem_maioridade_civil())
print(franco.tem_maioridade_civil_alternativa_1(franco))
print(franco:tem_maioridade_civil_alternativa_1())
print(franco.tem_maioridade_civil_alternativa_2(franco))
print(franco:tem_maioridade_civil_alternativa_2())

-- Estas não fazem a chamada usando franco, elas realizam a chamada usando
-- valores da metatable Pessoa.
print(franco.tem_maioridade_civil_alternativa_3())
print(franco:tem_maioridade_civil_alternativa_3())
print(Pessoa.tem_maioridade_civil(Pessoa))
print(Pessoa:tem_maioridade_civil())
print(Pessoa.tem_maioridade_civil_alternativa_1(Pessoa))
print(Pessoa:tem_maioridade_civil_alternativa_1())
print(Pessoa.tem_maioridade_civil_alternativa_2(Pessoa))
print(Pessoa:tem_maioridade_civil_alternativa_2())
print(Pessoa.tem_maioridade_civil_alternativa_3())
print(Pessoa:tem_maioridade_civil_alternativa_3())

Para uma lista de bibliotecas para programação OOP em Lua, pode-se consultar esta entrada de lua-users.org.

Técnicas Usando Registros

Nesta seção, retorna-se aos básicos de registros como agrupamentos de dados. Recursos adicionais como polimorfismo e sobrecarga de operadores não são necessários para entender as técnicas. Construtores com ou sem parâmetros podem ser usados como se subrotinas simples.

Para focar em dados ao invés de OOP, a versão de implementação em JavaScript usará class e a versão em GDScript usará classes internas. Você pode escolher entre definir subrotinas como métodos, se preferir. Os exemplos implementarão subrotinas como funções e procedimentos independentes.

Vetor de Registros (Array of Structures)

Uma forma de relacionar dados em diferentes vetores consiste em usar a técnica de vetores paralelos (structure of arrays). Por sinal, agora é possível explorar a parte structure do nome. Entretanto, a técnica mais comum é definir um vetor de registros (vetor de estruturas ou array of structures), especialmente em OOP.

Um vetor de registros combina todos os dados que devem ser representados sobre uma entidade um registro. Ao invés de compartilhar dados usando um índice, todos os dados da entidade estarão no registro definido.

Por exemplo, para armazenar o nome, a extensão e um exemplo de uma linguagem de programação arbitrária, poder-se-ia definir três vetores paralelos: linguagens_nome, linguagens_extensao e linguagens_exemplo. No caso, adotou-se linguagens_ como prefixo para os vetores.

Com um vetor de registros, pode-se definir um único vetor que contenha dados em um único registro (por exemplo, Linguagem ou LinguagemProgramacao) que tenha três campos: nome, extensao e exemplo. Cada posição do vetor armazenará um registro. Todos os dados estarão armazenados em conjunto no registro.

class LinguagemProgramacao {
    constructor(nome = "", extensao = "", exemplo = "") {
        this.nome = nome
        this.extensao = extensao
        this.exemplo = exemplo
    }
}

let linguagens = [
    new LinguagemProgramacao("JavaScript", ".js", "console.log(\"Olá, meu nome é Franco!\")"),
    new LinguagemProgramacao("Python", ".py", "print(\"Olá, meu nome é Franco!\")"),
    new LinguagemProgramacao("Lua", ".lua", "print(\"Olá, meu nome é Franco!\")"),
    new LinguagemProgramacao("GDScript", ".gd", "extends Node\nfunc _ready():\n    print(\"Olá, meu nome é Franco!\")"),
]

for (let linguagem of linguagens) {
    console.log("Linguagem de Programação: " + linguagem.nome)
    console.log("Extensão: " + linguagem.extensao)
    console.log("Exemplo:")
    console.log(linguagem.exemplo)
    console.log("---")
}
class LinguagemProgramacao:
    def __init__(self, nome = "", extensao = "", exemplo = ""):
        self.nome = nome
        self.extensao = extensao
        self.exemplo = exemplo

linguagens = [
    LinguagemProgramacao("JavaScript", ".js", "console.log(\"Olá, meu nome é Franco!\")"),
    LinguagemProgramacao("Python", ".py", "print(\"Olá, meu nome é Franco!\")"),
    LinguagemProgramacao("Lua", ".lua", "print(\"Olá, meu nome é Franco!\")"),
    LinguagemProgramacao("GDScript", ".gd", "extends Node\nfunc _ready():\n    print(\"Olá, meu nome é Franco!\")"),
]

for linguagem in linguagens:
    print("Linguagem de Programação: " + linguagem.nome)
    print("Extensão: " + linguagem.extensao)
    print("Exemplo:")
    print(linguagem.exemplo)
    print("---")
function crie_linguagem_programacao(nome, extensao, exemplo)
    local resultado = {
        nome = nome or "",
        extensao = extensao or "",
        exemplo = exemplo or ""
    }

    return resultado
end

local linguagens = {
    crie_linguagem_programacao("JavaScript", ".js", "console.log(\"Olá, meu nome é Franco!\")"),
    crie_linguagem_programacao("Python", ".py", "print(\"Olá, meu nome é Franco!\")"),
    crie_linguagem_programacao("Lua", ".lua", "print(\"Olá, meu nome é Franco!\")"),
    crie_linguagem_programacao("GDScript", ".gd", "extends Node\nfunc _ready():\n    print(\"Olá, meu nome é Franco!\")"),
}

for _, linguagem in ipairs(linguagens) do
    print("Linguagem de Programação: " .. linguagem.nome)
    print("Extensão: " .. linguagem.extensao)
    print("Exemplo:")
    print(linguagem.exemplo)
    print("---")
end
extends Node

class LinguagemProgramacao:
    var nome
    var extensao
    var exemplo

    func _init(nome = "", extensao = "", exemplo = ""):
        self.nome = nome
        self.extensao = extensao
        self.exemplo = exemplo

func _ready():
    var linguagens = [
        LinguagemProgramacao.new("JavaScript", ".js", "console.log(\"Olá, meu nome é Franco!\")"),
        LinguagemProgramacao.new("Python", ".py", "print(\"Olá, meu nome é Franco!\")"),
        LinguagemProgramacao.new("Lua", ".lua", "print(\"Olá, meu nome é Franco!\")"),
        LinguagemProgramacao.new("GDScript", ".gd", "extends Node\nfunc _ready():\n    print(\"Olá, meu nome é Franco!\")"),
    ]

    for linguagem in linguagens:
        print("Linguagem de Programação: " + linguagem.nome)
        print("Extensão: " + linguagem.extensao)
        print("Exemplo:")
        print(linguagem.exemplo)
        print("---")

Vetores de registros são comuns em programação orientada a objetos. Para pessoas iniciantes em programação, eles fornecem uma boa forma de organizar e modelar soluções para problemas.

Registro de Vetores (Structure of Arrays) ou Vetores Paralelos em Registro

Para um registro de vetores propriamente dito, pode-se modificar a implementação usando vetores paralelos. Todos os vetores representado todas as entidades estarão armazenados em um único registro; cada entidade é acessada pelo índice compartilhado entre os diferentes vetores. Em outras palavras, a abordagem é contrária à abstração de dados proposta por OOP.

class LinguagensProgramacao {
    constructor(nomes = [], extensoes = [], exemplos = []) {
        this.nomes = nomes
        this.extensoes = extensoes
        this.exemplos = exemplos
    }
}

let linguagens = new LinguagensProgramacao(
    ["JavaScript", "Python", "Lua", "GDScript"],
    [".js", ".py", ".lua", ".gd"],
    [
        "console.log(\"Olá, meu nome é Franco!\")",
        "print(\"Olá, meu nome é Franco!\")",
        "print(\"Olá, meu nome é Franco!\")",
        "extends Node\nfunc _ready():\n    print(\"Olá, meu nome é Franco!\")"
    ]
)

for (let indice_linguagem in linguagens.nomes) {
    console.log("Linguagem de Programação: " + linguagens.nomes[indice_linguagem])
    console.log("Extensão: " + linguagens.extensoes[indice_linguagem])
    console.log("Exemplo:")
    console.log(linguagens.exemplos[indice_linguagem])
    console.log("---")
}
class LinguagensProgramacao:
    def __init__(self, nomes = "", extensoes = "", exemplos = ""):
        self.nomes = nomes
        self.extensoes = extensoes
        self.exemplos = exemplos

linguagens = LinguagensProgramacao(
    ["JavaScript", "Python", "Lua", "GDScript"],
    [".js", ".py", ".lua", ".gd"],
    [
        "console.log(\"Olá, meu nome é Franco!\")",
        "print(\"Olá, meu nome é Franco!\")",
        "print(\"Olá, meu nome é Franco!\")",
        "extends Node\nfunc _ready():\n    print(\"Olá, meu nome é Franco!\")"
    ]
)

for indice_linguagem in range(len(linguagens.nomes)):
    print("Linguagem de Programação: " + linguagens.nomes[indice_linguagem])
    print("Extensão: " + linguagens.extensoes[indice_linguagem])
    print("Exemplo:")
    print(linguagens.exemplos[indice_linguagem])
    print("---")
function crie_linguagens_programacao(nomes, extensoes, exemplos)
    local resultado = {
        nomes = nomes or {},
        extensoes = extensoes or {},
        exemplos = exemplos or {}
    }

    return resultado
end

local linguagens = crie_linguagens_programacao(
    {"JavaScript", "Python", "Lua", "GDScript"},
    {".js", ".py", ".lua", ".gd"},
    {
        "console.log(\"Olá, meu nome é Franco!\")",
        "print(\"Olá, meu nome é Franco!\")",
        "print(\"Olá, meu nome é Franco!\")",
        "extends Node\nfunc _ready():\n    print(\"Olá, meu nome é Franco!\")"
    }
)

for indice_linguagem = 1, #linguagens.nomes do
    print("Linguagem de Programação: " .. linguagens.nomes[indice_linguagem])
    print("Extensão: " .. linguagens.extensoes[indice_linguagem])
    print("Exemplo:")
    print(linguagens.exemplos[indice_linguagem])
    print("---")
end
extends Node

class LinguagensProgramacao:
    var nomes
    var extensoes
    var exemplos

    func _init(nomes = "", extensoes = "", exemplos = ""):
        self.nomes = nomes
        self.extensoes = extensoes
        self.exemplos = exemplos

func _ready():
    var linguagens = LinguagensProgramacao.new(
        ["JavaScript", "Python", "Lua", "GDScript"],
        [".js", ".py", ".lua", ".gd"],
        [
            "console.log(\"Olá, meu nome é Franco!\")",
            "print(\"Olá, meu nome é Franco!\")",
            "print(\"Olá, meu nome é Franco!\")",
            "extends Node\nfunc _ready():\n    print(\"Olá, meu nome é Franco!\")"
        ]
    )

    for indice_linguagem in range(len(linguagens.nomes)):
        print("Linguagem de Programação: " + linguagens.nomes[indice_linguagem])
        print("Extensão: " + linguagens.extensoes[indice_linguagem])
        print("Exemplo:")
        print(linguagens.exemplos[indice_linguagem])
        print("---")

Dependendo das características de uso e iteração sobre dados, o desempenho de uma implementação com vetor de registros ou de um registro de vetores pode variar significativamente. Por exemplo, em jogos digitais com milhões de entidades, o uso de registro de vetores pode ter desempenho superior, por armazenar dados de cada vetor de forma seqüencial em memória. Isso contribui para otimização de uso de memória cache em laços com repetições para milhares ou milhões de entidades em um jogo. A abordagem é bastante usada em uma arquitetura de software chamada Entity-Component-System (ECS).

Por outro lado, sistemas empresarias podem processar uma entidade por vez (ao invés de milhares de entidades semelhantes por vez). Neste caso, um vetor de registros pode ter desempenho melhor, caso todos os dados da mesma entidade estejam em seqüência (todos eles poderiam ser obtidos de uma única vez). Contudo, na prática, isso nem sempre ocorre, porque objetos são comumente modelados como hierarquias ou composições de outros objetos. Ou seja, pode não haver garantia de que eles ocupem posições contíguas de memória. O uso de bons sistemas gerenciadores de bancos de dados (SGDBs) pode amenizar o problema, embora seja necessário tomar cuidado ao armazenar os dados em memória primária após a recuperação.

No geral, como em todos os casos, o ideal é medir e comparar o desempenho usando uma ferramenta como um profiler ao invés de assumir o comportamento para um programa em particular. Cada caso é um caso.

Composição de Registros: Registro Com Registros

Um registro pode ser definido por tipos primitivos e por tipos compostos. Em potencial, pode-se compor um registro por outros registros pré-existentes. Para isso, basta adicionar um atributo ao registro que seja do tipo de outro registro.

Por exemplo, uma receita pode ter um nome, modo_preparo, e uma lista de ingredientes. Cada ingrediente pode ter um nome, uma quantidade e uma unidade de medida (por exemplo, massa ou volume -- unidade_medida). Pode-se definir dois registros para a modelagem: um para Ingrediente, um para Receita. O registro para Receita pode ter um vetor (ou uma lista) de Ingrediente.

class Ingrediente {
    constructor(nome = "", quantidade = 0.0, unidade_medida = "") {
        this.nome = nome
        this.quantidade = quantidade
        this.unidade_medida = unidade_medida
    }
}

class Receita {
    constructor(nome = "", modo_preparo = "", ingredientes = []) {
        this.nome = nome
        this.modo_preparo = modo_preparo
        this.ingredientes = ingredientes
    }
}

let agua = new Ingrediente("Água", 3.0, "Xícaras")
let farinha = new Ingrediente("Farinha", 4.0, "Xícaras")
let sal = new Ingrediente("Sal", 2.0, "Colheres de Sopa")
let fermento = new Ingrediente("Fermento", 2.0, "Colheres de Chá")

let pao = new Receita("Pão")
pao.ingredientes.push(agua)
pao.ingredientes.push(farinha)
pao.ingredientes.push(sal)
pao.ingredientes.push(fermento)
pao.modo_preparo = "..."

console.log(pao.nome)
console.log("Ingredientes:")
for (let ingrediente of pao.ingredientes) {
    console.log("- " + ingrediente.nome + ": " + ingrediente.quantidade + " " + ingrediente.unidade_medida)
}

console.log("Modo de preparo:")
console.log(pao.modo_preparo)
class Ingrediente:
    def __init__(self, nome = "", quantidade = 0.0, unidade_medida = ""):
        self.nome = nome
        self.quantidade = quantidade
        self.unidade_medida = unidade_medida

class Receita:
    def __init__(self, nome = "", modo_preparo = "", ingredientes = None):
        self.nome = nome
        self.modo_preparo = modo_preparo
        self.ingredientes = ingredientes if (ingredientes != None) else []

agua = Ingrediente("Água", 3.0, "Xícaras")
farinha = Ingrediente("Farinha", 4.0, "Xícaras")
sal = Ingrediente("Sal", 2.0, "Colheres de Sopa")
fermento = Ingrediente("Fermento", 2.0, "Colheres de Chá")

pao = Receita("Pão")
pao.ingredientes.append(agua)
pao.ingredientes.append(farinha)
pao.ingredientes.append(sal)
pao.ingredientes.append(fermento)
pao.modo_preparo = "..."

print(pao.nome)
print("Ingredientes:")
for ingrediente in pao.ingredientes:
    print("- " + ingrediente.nome + ": " + str(ingrediente.quantidade) + " " + ingrediente.unidade_medida)

print("Modo de preparo:")
print(pao.modo_preparo)
function crie_ingrediente(nome, quantidade, unidade_medida)
    local resultado = {
        nome = nome or "",
        quantidade = quantidade or 0.0,
        unidade_medida = unidade_medida or ""
    }

    return resultado
end

function crie_receita(nome, modo_preparo, ingredientes)
    local resultado = {
        nome = nome or "",
        modo_preparo = quantidade or "",
        ingredientes = ingredientes or {}
    }

    return resultado
end

local agua = crie_ingrediente("Água", 3.0, "Xícaras")
local farinha = crie_ingrediente("Farinha", 4.0, "Xícaras")
local sal = crie_ingrediente("Sal", 2.0, "Colheres de Sopa")
local fermento = crie_ingrediente("Fermento", 2.0, "Colheres de Chá")

local pao = crie_receita("Pão")
table.insert(pao.ingredientes, agua)
table.insert(pao.ingredientes, farinha)
table.insert(pao.ingredientes, sal)
table.insert(pao.ingredientes, fermento)
pao.modo_preparo = "..."

print(pao.nome)
print("Ingredientes:")
for _, ingrediente in ipairs(pao.ingredientes) do
    print("- " .. ingrediente.nome .. ": " .. ingrediente.quantidade .. " " .. ingrediente.unidade_medida)
end

print("Modo de preparo:")
print(pao.modo_preparo)
extends Node

class Ingrediente:
    var nome
    var quantidade
    var unidade_medida

    func _init(nome = "", quantidade = 0.0, unidade_medida = ""):
        self.nome = nome
        self.quantidade = quantidade
        self.unidade_medida = unidade_medida

class Receita:
    var nome
    var modo_preparo
    var ingredientes

    func _init(nome = "", modo_preparo = "", ingredientes = []):
        self.nome = nome
        self.modo_preparo = modo_preparo
        self.ingredientes = ingredientes

func _ready():
    var agua = Ingrediente.new("Água", 3.0, "Xícaras")
    var farinha = Ingrediente.new("Farinha", 4.0, "Xícaras")
    var sal = Ingrediente.new("Sal", 2.0, "Colheres de Sopa")
    var fermento = Ingrediente.new("Fermento", 2.0, "Colheres de Chá")

    var pao = Receita.new("Pão")
    pao.ingredientes.append(agua)
    pao.ingredientes.append(farinha)
    pao.ingredientes.append(sal)
    pao.ingredientes.append(fermento)
    pao.modo_preparo = "..."

    print(pao.nome)
    print("Ingredientes:")
    for ingrediente in pao.ingredientes:
        print("- " + ingrediente.nome + ": " + str(ingrediente.quantidade) + " " + ingrediente.unidade_medida)

    print("Modo de preparo:")
    print(pao.modo_preparo)

É possível modificar o exemplo para alterar a inicialização. Por exemplo, ao invés de inserir cada Ingrediente no vetor ingredientes, poder-se-ia fazer a inicialização diretamente na subrotina usada como construtor. Por exemplo:

pao = new Receita("Pão",
                  "...",
                  [
                      agua,
                      farinha,
                      sal,
                      fermento
                  ])

Em particular, seria ainda mais conveniente definir subrotinas para operações válidas para manipulação dos registros, como será comentado em uma das próximas subseções.

Abstração de Dados e Granularidade da Modelagem

Caso se quisesse, poder-se-ia definir registros para unidade de medida (UnidadeMedida, que poderia combinar quantidade e unidade_medida um único tipo) ou para o modo de preparo (por exemplo, ele poderia ser dividido em preparos_iniciais, instrucoes, tempo_preparo, dicas, como_servir...).

Assim, sempre é possível criar mais (ou menos) tipos de dados para abstrair valores. Quanto mais tipos, maior a abstração de dados e a granularidade da modelagem. A granularidade define o detalhamento e a estruturação do tipo de dados.

A escolha da granularidade depende do problema a ser modelado. Maior granularidade nem sempre é melhor; o ideal é encontrar um equilíbrio. Quanto maior a granularidade, mais pormenorizada e compartimentada será a solução, facilitando o acesso a dados (pois existirão mais campos para acesso e consulta). Embora mais estruturado, o programa pode ser mais difícil de usar e trabalhoso para implementar. Quanto menor a granularidade, mais próximo o tipo será de um tipo primitivo. Embora mais simples par ser armazenar, a extração de dados será mais complexa (pois, possivelmente, exigirá o processamento do tipo primitivo).

Em particular, um critério útil para definir granularidade é determinar quais valores serão buscados em um programa. Variáveis que representem um mesmo objeto ou uma mesma entidade podem ter uma mesma instância de um tipo, para facilitar buscas.

Por exemplo, pode-se imaginar a modelagem de um endereço. Um endereço pode ser, simplesmente, uma cadeia de caracteres. Ele também poderia ser um registro composto por campos como rua, numero, cidade, estado, pais, codigo_postal, complemento... No primeiro caso, a extração da rua precisaria manipular a cadeia de caracteres. Caso não existisse um padrão para definir um formato de endereço, a extração poderia ser complexa ou mesmo impossível (onde está a cidade na cadeia de caracteres? Ela pode estar em qualquer lugar). No segundo caso, a extração é simples: endereco.rua. Por outro lado, a construção do registro requer mais operações (tanto para implementação inicial, quanto para o uso do sistema). Na forma mais simples, ao invés de solicitar a entrada de um único valor (o endereço completo), dever-se-ia ler um valor para rua, outro para numero, e assim por diante.

É interessante observar formulários em páginas da Internet ou em programas. Cada campo fornecido em um cadastro permite inferir como os dados são armazenados e processados no sistema, assim como são feitas consultas a cada um deles.

Por exemplo, na segunda abordagem, um formulário e modelo de dados mais sofisticados poderiam continuar a composição. Poder-se-ia criar uma estrutura Pais, composta por uma coleção de Estado, por sua vez composta por uma coleção de Cidade... Quando mais estruturas únicas, maior a granularidade. Também é possível economizar memória instanciando cada valor uma única vez, e referenciando a variável criada sempre que necessário. Um benefício é centralizar o local de atualizações: por exemplo, para atualizar informações de uma Cidade em todos os endereços que a mencionem, basta alterar a referência original (ao invés de cada endereço individual).

Registro Com Referência para o Próprio Registro

Assim como existem subrotinas recursivas, é possível definir tipos de dados recursivos (ou estruturas recursivas ou classes recursivas). Em outras palavras, um tipo de dados que possui um atributo do próprio tipo de dados.

Por exemplo, um sistema pode ser composto de subsistemas (que também são sistemas). Uma seção pode ser composta por subseções (que também são seções). Uma lista pode ser composta por sublistas (que também são listas e já foram usadas ao longo do tópico).

De fato, estruturas de dados são comumente implementadas com tipos de dados recursivos. Pode-se pensar uma lista como um registro que armazene um valor e uma referência para o próprio tipo da lista (a próxima entrada).

Em algumas linguagens de programação, pode ser necessário realizar uma forward declaration do novo tipo antes de adicionar um atributo do próprio registro. Isso ocorre porque alguns compiladores e interpretadores precisam saber da existência de um tipo antes de usá-lo (por exemplo, para saber a quantidade de memória necessária para alocar uma variável do tipo). A forward declaration avisa a ferramenta que o tipo existirá (em algum momento), mesmo que ainda não tenha implementação. Isso é possível porque referências são endereços de memória que costumam ter tamanho fixo para endereçamento (4 bytes em sistemas 32-bit, 8 bytes em sistemas 64-bit).

O exemplo a seguir apresenta um protótipo de registro para um tipo para um item de lista encadeada (ou lista ligada ou linked list). O tipo ItemLista possui dois campos: o valor armazenado e uma referência para o proximo item da lista. Uma entrada da qual o valor de proximo tenha como valor uma referência inválida ou vazia (null or nil, nas linguagens consideradas) significa o final da lista. Para facilitar a leitura, poder-se-ia definir uma constante FINAL_LISTA = null, embora o uso da referência vazia seja comum e idiomático.

class ItemLista {
    constructor(valor, proximo = null) {
        this.valor = valor
        this.proximo = proximo
    }
}

let numeros = new ItemLista(1,
                            new ItemLista(2,
                                          new ItemLista(3)))
let item_lista = numeros
while (item_lista !== null) {
    console.log(item_lista.valor)
    item_lista = item_lista.proximo
}
class ItemLista:
    def __init__(self, valor, proximo = None):
        self.valor = valor
        self.proximo = proximo

numeros = ItemLista(1,
                    ItemLista(2,
                              ItemLista(3)))
item_lista = numeros
while (item_lista != None):
    print(item_lista.valor)
    item_lista = item_lista.proximo
function crie_item_lista(valor, proximo)
    local resultado = {
        valor = valor,
        proximo = proximo or nil
    }

    return resultado
end

local numeros = crie_item_lista(1,
                                crie_item_lista(2,
                                                crie_item_lista(3)))
local item_lista = numeros
while (item_lista ~= nil) do
    print(item_lista.valor)
    item_lista = item_lista.proximo
end
extends Node

class ItemLista:
    var valor
    var proximo

    func _init(valor, proximo = null):
        self.valor = valor
        self.proximo = proximo

func _ready():
    var numeros = ItemLista.new(1,
                                ItemLista.new(2,
                                              ItemLista.new(3)))
    var item_lista = numeros
    while (item_lista != null):
        print(item_lista.valor)
        item_lista = item_lista.proximo

Uma lista encadeada é uma das estruturas de dados dinâmicas mais básicas que se pode implementar. O exemplo apresenta inserções apenas no final (push(), push_back() ou append()). Uma implementação completa adicionaria inserção em posições arbitrárias e remoções. Para isso, convém integrar registros com subrotinas.

Registros e Subrotinas

Registros podem abstrair dados (abstração de dados). Subrotinas podem abstrair processamentos (abstração funcional). A combinação de registros e subrotinas permite abstrair dados e processamentos. Por sinal, a combinação é uma das características fundamentais de OOP.

Ao invés de re-escrever operações para manipular cada registro criado como tipos de dados, pode-se criar subrotinas. Subrotinas para registos funcionam como subrotinas e operadores para tipos primitivos: elas podem definir operações pré-definidas para manipular os dados com comodidade e segurança. Em OOP, isso faz parte de operações criadas com métodos (e, possivelmente, operadores sobrecarregados) e de uma característica desejável chamada de encapsulamento.

Por exemplo, é bastante comum que implementações de programas em OOP definam código com métodos chamados getters e setters. Genericamente, esses métodos são chamados de métodos acessores (accessor methods). O objetivo de um método get() é obter o valor de atributo. O objetivo de um método set() é ajustar (inicializar ou modificar) um valor que seja válido para a classe, e proibir alterações inválidas. Em outras palavras, eles garantem que um objeto sempre tenha um estado válido, agregando consistência e segurança para uso.

Para um exemplo introdutório, pode-se considerar um elevador. Um elevador pode ter um andar_atual, um andar_minimo e um andar_maximo. O andar_atual deve ser menor ou igual ao andar_maximo. Ele também ser deve maior ou igual ao andar_minimo. Caso se considerasse o subsolo como andares negativos para andares, os valores de andares poderiam ser negativos.

class Elevador {
    constructor(andar_inicial = 0, andar_minimo = 0, andar_maximo = 5) {
        let maximo = andar_minimo
        let minimo = andar_minimo
        if (andar_minimo > andar_maximo) {
            minimo = andar_maximo
        } else {
            maximo = andar_maximo
        }

        let inicial = andar_inicial
        if (andar_inicial < minimo) {
            inicial = minimo
        } else if (andar_inicial > maximo) {
            inicial = maximo
        }

        this.andar_minimo = minimo
        this.andar_maximo = maximo
        this.andar_atual = inicial
    }
}

// Ou set_andar(), para um nome mais tradicional.
function alterar_andar(elevador, novo_andar) {
    if ((novo_andar >= elevador.andar_minimo) && (novo_andar <= elevador.andar_maximo)) {
        elevador.andar_atual = novo_andar
    }
}

function subir_um_andar(elevador) {
    alterar_andar(elevador, elevador.andar_atual + 1)
}

function descer_um_andar(elevador) {
    alterar_andar(elevador, elevador.andar_atual - 1)
}

function inteiro_aleatorio(minimo_inclusive, maximo_inclusive) {
    let minimo = Math.ceil(minimo_inclusive)
    let maximo = Math.floor(maximo_inclusive)

    return Math.floor(minimo + Math.random() * (maximo + 1 - minimo))
}

let elevador = new Elevador(0, 0, 5)
for (i = 0; i < 10; ++i) {
    let andar_inicial = elevador.andar_atual
    console.log("O elevador está no " + andar_inicial + "º andar")

    if (inteiro_aleatorio(0, 1) === 0) {
        console.log("Próximo comando: descer")
        descer_um_andar(elevador)
    } else {
        console.log("Próximo comando: subir")
        subir_um_andar(elevador)
    }

    let andar_final = elevador.andar_atual
    if (andar_inicial !== andar_final) {
        console.log("Vruuum!")
    } else {
        console.log("... Nada acontece.")
    }
}
console.log("O elevador está no " + elevador.andar_atual + "º andar")
import random

class Elevador:
    def __init__(self, andar_inicial = 0, andar_minimo = 0, andar_maximo = 5):
        maximo = andar_minimo
        minimo = andar_minimo
        if (andar_minimo > andar_maximo):
            minimo = andar_maximo
        else:
            maximo = andar_maximo

        inicial = andar_inicial
        if (andar_inicial < minimo):
            inicial = minimo
        elif (andar_inicial > maximo):
            inicial = maximo

        self.andar_minimo = minimo
        self.andar_maximo = maximo
        self.andar_atual = inicial

# Ou set_andar(), para um nome mais tradicional.
def alterar_andar(elevador, novo_andar):
    if ((novo_andar >= elevador.andar_minimo) and (novo_andar <= elevador.andar_maximo)):
        elevador.andar_atual = novo_andar

def subir_um_andar(elevador):
    alterar_andar(elevador, elevador.andar_atual + 1)

def descer_um_andar(elevador):
    alterar_andar(elevador, elevador.andar_atual - 1)

random.seed()
elevador = Elevador(0, 0, 5)
for i in range(10):
    andar_inicial = elevador.andar_atual
    print("O elevador está no " + str(andar_inicial) + "º andar")

    if (random.randint(0, 1) == 0):
        print("Próximo comando: descer")
        descer_um_andar(elevador)
    else:
        print("Próximo comando: subir")
        subir_um_andar(elevador)

    andar_final = elevador.andar_atual
    if (andar_inicial != andar_final):
        print("Vruuum!")
    else:
        print("... Nada acontece.")

print("O elevador está no " + str(elevador.andar_atual) + "º andar")
function crie_elevador(andar_inicial, andar_minimo, andar_maximo)
    andar_inicial = andar_inicial or 0
    andar_minimo = andar_minimo or 0
    andar_maximo = andar_maximo or 0

    local maximo = andar_minimo
    local minimo = andar_minimo
    if (andar_minimo > andar_maximo) then
        minimo = andar_maximo
    else
        maximo = andar_maximo
    end

    local inicial = andar_inicial
    if (andar_inicial < minimo) then
        inicial = minimo
    elseif (andar_inicial > maximo) then
        inicial = maximo
    end

    local resultado = {
        andar_minimo = minimo,
        andar_maximo = maximo,
        andar_atual = inicial
    }

    return resultado
end

-- Ou set_andar(), para um nome mais tradicional.
function alterar_andar(elevador, novo_andar)
    if ((novo_andar >= elevador.andar_minimo) and (novo_andar <= elevador.andar_maximo)) then
        elevador.andar_atual = novo_andar
    end
end

function subir_um_andar(elevador)
    alterar_andar(elevador, elevador.andar_atual + 1)
end

function descer_um_andar(elevador)
    alterar_andar(elevador, elevador.andar_atual - 1)
end

math.randomseed(os.time())
local elevador = crie_elevador(0, 0, 5)
for i = 1, 10 do
    local andar_inicial = elevador.andar_atual
    print("O elevador está no " .. andar_inicial .. "º andar")

    if (math.random(0, 1) == 0) then
        print("Próximo comando: descer")
        descer_um_andar(elevador)
    else
        print("Próximo comando: subir")
        subir_um_andar(elevador)
    end

    local andar_final = elevador.andar_atual
    if (andar_inicial ~= andar_final) then
        print("Vruuum!")
    else
        print("... Nada acontece.")
    end
end

print("O elevador está no " .. elevador.andar_atual .. "º andar")
extends Node

class Elevador:
    var andar_atual
    var andar_minimo
    var andar_maximo

    func _init(andar_inicial = 0, andar_minimo = 0, andar_maximo = 5):
        var maximo = andar_minimo
        var minimo = andar_minimo
        if (andar_minimo > andar_maximo):
            minimo = andar_maximo
        else:
            maximo = andar_maximo

        var inicial = andar_inicial
        if (andar_inicial < minimo):
            inicial = minimo
        elif (andar_inicial > maximo):
            inicial = maximo

        self.andar_minimo = minimo
        self.andar_maximo = maximo
        self.andar_atual = inicial

# Ou set_andar(), para um nome mais tradicional.
func alterar_andar(elevador, novo_andar):
    if ((novo_andar >= elevador.andar_minimo) and (novo_andar <= elevador.andar_maximo)):
        elevador.andar_atual = novo_andar

func subir_um_andar(elevador):
    alterar_andar(elevador, elevador.andar_atual + 1)

func descer_um_andar(elevador):
    alterar_andar(elevador, elevador.andar_atual - 1)

func inteiro_aleatorio(minimo_inclusive, maximo_inclusive):
    var minimo = ceil(minimo_inclusive)
    var maximo = floor(maximo_inclusive)

    # randi(): [0.0, 1.0[
    return randi() % int(maximo + 1 - minimo) + minimo

func _ready():
    randomize()
    var elevador = Elevador.new(0, 0, 5)
    for i in range(10):
        var andar_inicial = elevador.andar_atual
        print("O elevador está no " + str(andar_inicial) + "º andar")

        if (inteiro_aleatorio(0, 1) == 0):
            print("Próximo comando: descer")
            descer_um_andar(elevador)
        else:
            print("Próximo comando: subir")
            subir_um_andar(elevador)

        var andar_final = elevador.andar_atual
        if (andar_inicial != andar_final):
            print("Vruuum!")
        else:
            print("... Nada acontece.")

    print("O elevador está no " + str(elevador.andar_atual) + "º andar")

O exemplo fornece o construtor mais robusto apresentado neste tópico. A implementação assegura que todos os valores usados sejam válidos para garantir a consistência de dados para processamento. As verificações apresentadas garantem que os valores sejam inicializados conforme a especificação para o problema. Tecnicamente, como as linguagens dos exemplos possuem tipagem dinâmica, existiria a possibilidade de atribuir valores de tipos incorretos; isso será abordado em uma das próximas subseções.

Para uma possível melhoria, poder-se-ia definir um procedimento escreva_andar( ). Assim, ao invés de andar zero, poder-se-ia escrever térreo.

De qualquer forma, linguagens procedurais normalmente não garantem a integridade de dados contra atribuições diretas. Embora alterar_andar() não permita a definição de um valor inválido para a variável andar_atual, nada impediria o uso de elevador.andar_atual = 1234 (assumindo que 1234 seja maior que andar_maximo) ou elevador.andar_atual = "Franco". Em OOP, é possível impedir tal uso por meio de modificadores de acesso (access modifiers) ou especificadores de acesso (access specifiers). Três dos mais comuns são private (acesso privado), public (acesso público) e protected (acesso protegido), que aparecem, por exemplo, em linguagens como C++, Java e C#. Um atributo com acesso público é como um atributo de registro: ela pode ser acessada e modificada em qualquer parte do código na qual o objeto esteja em escopo. Um atributo com acesso privado pode ser modificado apenas pela classe que definir o atributo. Um atributo com acesso protegido pode ser modificado apenas pela classe que definir o atributo ou por suas classes derivadas (subclasses). Assim, private e protected poderiam ser usadas para impedir leituras e escritas de valores restritos (desde que usadas, que as subrotinas criadas fossem convertidas para métodos, e que se definisse um método obtenha_andar_atual() ou get_andar_atual() para acesso ao valor do atributo restrito).

Como o tópico não é sobre OOP, deve-se ter responsabilidade e maturidade para usar corretamente a implementação. As alterações devem ser feitas usando apenas as subrotinas definidas, mesmo que seja possível alterar valores diretamente. Isso é algo que deve ser autoimposto. De fato, muitas boas práticas de OOP podem ser aplicadas a outras linguagens de programação utilizando-se de bom senso e autocontrole.

Tipos Abstratos de Dados (Abstract Data Types ou ADTs)

A combinação de abstração de dados com abstração funcional pode ser usada para a criação de tipos abstratos de dados (abstract data type ou ADTs). Uma possível implementação de tipo abstrato de dados é chamada de tipo concreto de dados (concrete data type).

Um tipo abstrato de dados define uma interface para ocultar detalhes de implementação de um registro. Ao invés de manipular diretamente as variáveis internas, usa-se as subrotinas definidas para programar usando o ADT. A definição das subrotinas é a responsável por modificar corretamente os atributos. Em outras palavras, os dados e o código para processá-los são meros detalhes de implementação. O uso do tipo deve ser feito única e exclusivamente usando as subrotinas fornecidas.

A especificação de interfaces para estruturas de dados é um exemplo típico de tipo abstrato de dados. Por exemplo, uma lista pode incluir as seguintes operações:

  • Criar a lista;
  • Adicionar elemento à lista;
  • Remover elemento da lista;
  • Acessar elemento da lista;
  • Iterar pela lista.

Para demostrar o potencial da abordagem, pode-se retomar o exemplo do tipo ItemLista para a definição de um tipo Lista, que implemente uma lista encadeada (por isso chamada ListaEncadeada). Embora a implementação não use nenhum recurso não comentado anteriormente (em Python, del permite desalocar memória, como comentado para dicionários), ela é mais complexa que os demais exemplos apresentados até este momento. Para este tópico, o importante não é entendê-la completamente, mas constatar que operações definidas como subrotinas podem ocultar os detalhes de implementação de um registro.

class ItemLista {
    constructor(valor, proximo = null) {
        this.valor = valor
        this.proximo = proximo
    }
}

class ListaEncadeada {
    constructor() {
        this.inicio = null
        this.tamanho = 0
    }
}

function acesse_item_lista(lista, indice) {
    if ((indice < 0) || (indice >= lista.tamanho)) {
        return null
    }

    let indice_atual = 0
    let item_lista = lista.inicio
    while (indice_atual < indice) {
        item_lista = item_lista.proximo
        ++indice_atual
    }

    return item_lista
}

// Índice negativo insere no fim da lista.
function adicione_a_lista(lista, valor, indice = -1) {
    if (indice > lista.tamanho) {
        // Índice após o final da lista; erro de uso.
        return lista
    }

    let novo_item = new ItemLista(valor)
    if (!lista.inicio) {
        lista.inicio = novo_item
    } else {
        if (indice === 0) {
             novo_item.proximo = lista.inicio
             lista.inicio = novo_item
        } else {
            if (indice < 0) {
                indice = lista.tamanho
            }

            let item_anterior = acesse_item_lista(lista, indice - 1)
            novo_item.proximo = item_anterior.proximo
            item_anterior.proximo = novo_item
        }
    }

    ++lista.tamanho

    return lista
}

// Índice negativo remove do fim da lista.
function remova_da_lista(lista, indice = -1) {
    if (!lista.inicio) {
        // Lista vazia, não há o que remover.
        return lista
    } else if (indice >= lista.tamanho) {
        // Índice após o final da lista; erro de uso.
        return lista
    }

    if (indice === 0) {
        let item_remover = lista.inicio
        lista.inicio = lista.inicio.proximo
        item_remover = null
    } else {
        if (indice < 0) {
            indice = lista.tamanho - 1
        }

        let item_anterior = acesse_item_lista(lista, indice - 1)
        let item_remover = item_anterior.proximo
        item_anterior.proximo = item_remover.proximo
        item_remover = null
    }

    --lista.tamanho

    return lista
}

function escreva_lista(lista) {
    let item_lista = lista.inicio
    let texto = "["
    while (item_lista) {
        texto += item_lista.valor + ", "
        item_lista = item_lista.proximo
    }
    texto += "]"

    console.log(texto)
}

let numeros = new ListaEncadeada()
adicione_a_lista(numeros, 1)
adicione_a_lista(numeros, 2)
adicione_a_lista(numeros, 3)
adicione_a_lista(numeros, 0, 0)
adicione_a_lista(numeros, 1.5, 2)
adicione_a_lista(numeros, 2.5, 4)
adicione_a_lista(numeros, 4, 6)
adicione_a_lista(numeros, 3.5, 6)
escreva_lista(numeros)

remova_da_lista(numeros)
remova_da_lista(numeros, 0)
remova_da_lista(numeros, 1)
remova_da_lista(numeros, 3)
escreva_lista(numeros)
class ItemLista:
    def __init__(self, valor, proximo = None):
        self.valor = valor
        self.proximo = proximo

class ListaEncadeada:
    def __init__(self):
        self.inicio = None
        self.tamanho = 0

def acesse_item_lista(lista, indice):
    if ((indice < 0) or (indice >= lista.tamanho)):
        return None

    indice_atual = 0
    item_lista = lista.inicio
    while (indice_atual < indice):
        item_lista = item_lista.proximo
        indice_atual += 1

    return item_lista

# Índice negativo insere no fim da lista.
def adicione_a_lista(lista, valor, indice = -1):
    if (indice > lista.tamanho):
        # Índice após o final da lista; erro de uso.
        return lista

    novo_item = ItemLista(valor)
    if (not lista.inicio):
        lista.inicio = novo_item
    else:
        if (indice == 0):
             novo_item.proximo = lista.inicio
             lista.inicio = novo_item
        else:
            if (indice < 0):
                indice = lista.tamanho

            item_anterior = acesse_item_lista(lista, indice - 1)
            novo_item.proximo = item_anterior.proximo
            item_anterior.proximo = novo_item

    lista.tamanho += 1

    return lista

# Índice negativo remove do fim da lista.
def remova_da_lista(lista, indice = -1):
    if (not lista.inicio):
        # Lista vazia, não há o que remover.
        return lista
    elif (indice >= lista.tamanho):
        # Índice após o final da lista; erro de uso.
        return lista

    if (indice == 0):
        item_remover = lista.inicio
        lista.inicio = lista.inicio.proximo
        del item_remover
    else:
        if (indice < 0):
            indice = lista.tamanho - 1

        item_anterior = acesse_item_lista(lista, indice - 1)
        item_remover = item_anterior.proximo
        item_anterior.proximo = item_remover.proximo
        item_remover

    lista.tamanho -= 1

    return lista

def escreva_lista(lista):
    item_lista = lista.inicio
    texto = "["
    while (item_lista):
        texto += str(item_lista.valor) + ", "
        item_lista = item_lista.proximo

    texto += "]"

    print(texto)

numeros = ListaEncadeada()
adicione_a_lista(numeros, 1)
adicione_a_lista(numeros, 2)
adicione_a_lista(numeros, 3)
adicione_a_lista(numeros, 0, 0)
adicione_a_lista(numeros, 1.5, 2)
adicione_a_lista(numeros, 2.5, 4)
adicione_a_lista(numeros, 4, 6)
adicione_a_lista(numeros, 3.5, 6)
escreva_lista(numeros)

remova_da_lista(numeros)
remova_da_lista(numeros, 0)
remova_da_lista(numeros, 1)
remova_da_lista(numeros, 3)
escreva_lista(numeros)
function crie_item_lista(valor, proximo)
    local resultado = {
        valor = valor,
        proximo = proximo or nil
    }

    return resultado
end

function crie_lista_encadeada()
    local resultado = {
        inicio = nil,
        tamanho = 0
    }

    return resultado
end

function acesse_item_lista(lista, indice)
    if ((indice < 1) or (indice > lista.tamanho)) then
        return nil
    end

    local indice_atual = 1
    local item_lista = lista.inicio
    while (indice_atual < indice) do
        item_lista = item_lista.proximo
        indice_atual = indice_atual + 1
    end

    return item_lista
end

-- Índice zero ou negativo insere no fim da lista.
function adicione_a_lista(lista, valor, indice)
    indice = indice or -1

    if (indice > (lista.tamanho + 1)) then
        -- Índice após o final da lista; erro de uso.
        return lista
    end

    local novo_item = crie_item_lista(valor)
    if (not lista.inicio) then
        lista.inicio = novo_item
    else
        if (indice == 1) then
             novo_item.proximo = lista.inicio
             lista.inicio = novo_item
        else
            if (indice <= 0) then
                indice = lista.tamanho + 1
            end

            local item_anterior = acesse_item_lista(lista, indice - 1)
            novo_item.proximo = item_anterior.proximo
            item_anterior.proximo = novo_item
        end
    end

    lista.tamanho = lista.tamanho + 1

    return lista
end

-- Índice zero ou negativo remove do fim da lista.
function remova_da_lista(lista, indice)
    indice = indice or -1

    if (not lista.inicio) then
        -- Lista vazia, não há o que remover.
        return lista
    elseif (indice > lista.tamanho) then
        -- Índice após o final da lista; erro de uso.
        return lista
    end

    if (indice == 1) then
        local item_remover = lista.inicio
        lista.inicio = lista.inicio.proximo
        item_remover = nil
    else
        if (indice <= 0) then
            indice = lista.tamanho
        end

        local item_anterior = acesse_item_lista(lista, indice - 1)
        local item_remover = item_anterior.proximo
        item_anterior.proximo = item_remover.proximo
        item_remover = nil
    end

    lista.tamanho = lista.tamanho - 1

    return lista
end

function escreva_lista(lista)
    local item_lista = lista.inicio
    local texto = "["
    while (item_lista) do
        texto = texto .. tostring(item_lista.valor) .. ", "
        item_lista = item_lista.proximo
    end

    texto = texto .. "]"

    print(texto)
end

local numeros = crie_lista_encadeada()
adicione_a_lista(numeros, 1)
escreva_lista(numeros)
adicione_a_lista(numeros, 2)
escreva_lista(numeros)
adicione_a_lista(numeros, 3)
escreva_lista(numeros)
adicione_a_lista(numeros, 0, 1)
escreva_lista(numeros)
adicione_a_lista(numeros, 1.5, 3)
escreva_lista(numeros)
adicione_a_lista(numeros, 2.5, 5)
escreva_lista(numeros)
adicione_a_lista(numeros, 4, 7)
escreva_lista(numeros)
adicione_a_lista(numeros, 3.5, 7)
escreva_lista(numeros)

remova_da_lista(numeros)
remova_da_lista(numeros, 1)
remova_da_lista(numeros, 2)
remova_da_lista(numeros, 4)
escreva_lista(numeros)
extends Node

class ItemLista:
    var valor
    var proximo

    func _init(valor, proximo = null):
        self.valor = valor
        self.proximo = proximo

class ListaEncadeada:
    var inicio
    var tamanho

    func _init():
        self.inicio = null
        self.tamanho = 0

func acesse_item_lista(lista, indice):
    if ((indice < 0) or (indice >= lista.tamanho)):
        return null

    var indice_atual = 0
    var item_lista = lista.inicio
    while (indice_atual < indice):
        item_lista = item_lista.proximo
        indice_atual += 1

    return item_lista

# Índice negativo insere no fim da lista.
func adicione_a_lista(lista, valor, indice = -1):
    if (indice > lista.tamanho):
        # Índice após o final da lista; erro de uso.
        return lista

    var novo_item = ItemLista.new(valor)
    if (not lista.inicio):
        lista.inicio = novo_item
    else:
        if (indice == 0):
             novo_item.proximo = lista.inicio
             lista.inicio = novo_item
        else:
            if (indice < 0):
                indice = lista.tamanho

            var item_anterior = acesse_item_lista(lista, indice - 1)
            novo_item.proximo = item_anterior.proximo
            item_anterior.proximo = novo_item

    lista.tamanho += 1

    return lista

# Índice negativo remove do fim da lista.
func remova_da_lista(lista, indice = -1):
    if (not lista.inicio):
        # Lista vazia, não há o que remover.
        return lista
    elif (indice >= lista.tamanho):
        # Índice após o final da lista; erro de uso.
        return lista

    if (indice == 0):
        var item_remover = lista.inicio
        lista.inicio = lista.inicio.proximo
        # item_remover.unreference()
        item_remover = null
    else:
        if (indice < 0):
            indice = lista.tamanho - 1

        var item_anterior = acesse_item_lista(lista, indice - 1)
        var item_remover = item_anterior.proximo
        item_anterior.proximo = item_remover.proximo
        # item_remover.unreference()
        item_remover = null

    lista.tamanho -= 1

    return lista

func escreva_lista(lista):
    var item_lista = lista.inicio
    var texto = "["
    while (item_lista):
        texto += str(item_lista.valor) + ", "
        item_lista = item_lista.proximo

    texto += "]"

    print(texto)

func _ready():
    var numeros = ListaEncadeada.new()
    adicione_a_lista(numeros, 1)
    adicione_a_lista(numeros, 2)
    adicione_a_lista(numeros, 3)
    adicione_a_lista(numeros, 0, 0)
    adicione_a_lista(numeros, 1.5, 2)
    adicione_a_lista(numeros, 2.5, 4)
    adicione_a_lista(numeros, 4, 6)
    adicione_a_lista(numeros, 3.5, 6)
    escreva_lista(numeros)

    remova_da_lista(numeros)
    remova_da_lista(numeros, 0)
    remova_da_lista(numeros, 1)
    remova_da_lista(numeros, 3)
    escreva_lista(numeros)

O exemplo não fornece uma subrotina para iteração, embora ela pudesse ser definida, por exemplo, como uma função acessar_proximo_item().

Embora a implementação da lista encadeada seja mais complexa, a operação de uma variável do tipo ListaEncadeada é simples e similar ao uso de vetores e listas em linguagens como JavaScript, Python, GDScript e Lua:

  • A criação da lista usa o construtor fornecido pela linguagem ou a função de criação definida (crie_lista_encadeada());
  • A inserção de um valor à lista usa a função adicione_a_lista();
  • A remoção de um valor da lista usa a função remova_da_lista();
  • O acesso a um valor da lista usa a função acesse_item_lista().

Em linguagens que forneçam recursos de sobrecarga de operadores, seria possível, por exemplo, definir o operador colchetes para acessar o valor de um índice na lista. Isso tornaria a leitura de valores semelhante a feita em um vetor.

Registro como Parâmetro para Subrotina

Em problemas complexos, é possível que uma subrotina precise receber muitos parâmetros. Por exemplo, simulações complexas podem ter dezenas ou centenas de variáveis como parâmetros para configuração. Contudo, o uso de subrotinas com muitos parâmetros torna-se complexo. Em particular, introduzir ou remover um parâmetro da definição da subrotina pode tornar-se uma operação difícil.

Em casos assim, ao invés de uma lista de parâmetros longa, pode ser melhor definir um registro como único parâmetro da subrotina. Todos os parâmetros podem ser adicionados ao registro. Os valores padrão para o registro podem ser os valores mais usuais para uso da subrotina. Assim, bastaria alterar valores desejados em chamadas personalizadas.

class ParametrosFuncaoComplexa {
    constructor() {
        this.saudacao = "Olá"
        this.nome = "Franco"
        this.despedida = "Tchau."
    }
}

function minha_subrotina_complexa(parametros) {
    console.log(parametros.saudacao, parametros.nome, parametros.despedida)
}

var parametros = new ParametrosFuncaoComplexa()
parametros.saudacao = "Oi"
minha_subrotina_complexa(parametros)
class ParametrosFuncaoComplexa:
    def __init__(self):
        self.saudacao = "Olá"
        self.nome = "Franco"
        self.despedida = "Tchau."

def minha_subrotina_complexa(parametros):
    print(parametros.saudacao, parametros.nome, parametros.despedida)

parametros = ParametrosFuncaoComplexa()
parametros.saudacao = "Oi"
minha_subrotina_complexa(parametros)
function crie_parametros_funcao_complexa()
    local resultado = {
        saudacao = "Olá!",
        nome = "Franco",
        despedida = "Tchau."
    }

    return resultado
end

function minha_subrotina_complexa(parametros)
    print(parametros.saudacao, parametros.nome, parametros.despedida)
end

local parametros = crie_parametros_funcao_complexa()
parametros.saudacao = "Oi"
minha_subrotina_complexa(parametros)
extends Node

class ParametrosFuncaoComplexa:
    var saudacao = "Olá!"
    var nome = "Franco"
    var despedida = "Tchau."

func minha_subrotina_complexa(parametros):
    printt(parametros.saudacao, parametros.nome, parametros.despedida)

func _ready():
    var parametros = ParametrosFuncaoComplexa.new()
    parametros.saudacao = "Oi"
    minha_subrotina_complexa(parametros)

O exemplo define apenas três parâmetros, mas eles poderiam ser 10 ou 20, por exemplo. É importante notar que valores não são definidos no construtor; caso contrário, a utilidade da técnica reduzir-se-ia. Afinal, a única alteração foi no local do problema.

Além disso, a inicialização com valores pré-definidos pode comprometer o desempenho do programa, caso ela seja computacionalmente cara e todos os valores sejam redefinidos em seqüência. Portanto, é prudente equilibrar valores adotados como padrão e os que devem ser, obrigatoriamente, definidos antes da chamada. Contudo, antes de supor, o ideal é medir usando um profiler para verificar se a otimização realmente é necessária.

Verificação de Tipos para Parâmetros

Em linguagens de tipagem dinâmica, pode-se usar asserções para verificar os tipos de parâmetros usados na construção de um registro. Isso pode garantir a inicialização de valores com tipos corretos (dado que a linguagem não restringe atribuição quanto a tipos).

class MeuNumero {
    constructor(valor) {
        console.assert(typeof(valor) === "number", "valor deve ser um número.")
        this.valor = valor
    }
}

var valido = new MeuNumero(1)
var invalido = new MeuNumero("Franco")
class MeuNumero:
    def __init__(self, valor):
        assert isinstance(valor, (int, float)), "valor deve ser um número."
        self.valor = valor

valido = MeuNumero(1)
invalido = MeuNumero("Franco")
function crie_meu_numero(valor)
    assert(type(valor) == "number", "valor deve ser um número.")

    local resultado = {
        valor = valor
    }

    return resultado
end

local valido = crie_meu_numero(1)
local invalido = crie_meu_numero("Franco")
extends Node

class MeuNumero:
    var valor

    func _init(valor):
        assert(typeof(valor) == TYPE_INT or typeof(valor) == TYPE_REAL, "valor deve ser um número.")
        self.valor = valor

func _ready():
    var valido = MeuNumero.new(1)
    var invalido = MeuNumero.new("Franco")

Como feito em Subrotinas (Funções e Procedimentos), asserções também poder ser usadas para verificação de valores. Assim, poder-se-ia impedir a criação inicial de um registro com tipos e/ou valores iniciais inválidos.

Exemplos

Esta seção fornece exemplos adicionais para complementar os fornecidos ao longo do texto. Os exemplos são programas completos, estruturados usando registros e processados por combinações subrotinas. O intuito é mostrar como organizar programas e que já é possível começar a escrever sistemas mais complexos e complexos usando o conhecimento adquirido até este ponto.

Programa de Alto Nível

A combinação de registros, subrotinas e coleções permite criar programas complexos que também sejam elegantes e simples de ler. Esta seção retoma o exemplo de receitas e ingredientes para acrescentar funcionalidades. Novas funcionalidades são definidas por subrotinas.

class Ingrediente {
    constructor(nome = "", quantidade = 0.0, unidade_medida = "") {
        this.nome = nome
        this.quantidade = quantidade
        this.unidade_medida = unidade_medida
    }
}

class Receita {
    constructor(nome = "", modo_preparo = "", ingredientes = []) {
        this.nome = nome
        this.modo_preparo = modo_preparo
        this.ingredientes = ingredientes
    }
}

function escreva_ingrediente(ingrediente) {
    alert("- " + ingrediente.nome + ": " + ingrediente.quantidade + " " + ingrediente.unidade_medida)
}

function leia_ingrediente() {
    let ingrediente = new Ingrediente()
    ingrediente.nome = prompt("Nome do ingrediente:")
    ingrediente.quantidade = prompt("Quantidade de " + ingrediente.nome + ":")
    ingrediente.unidade_medida = prompt("Unidade de medida:")

    return ingrediente
}

function escreva_receita(receita) {
    alert(receita.nome)
    alert("Ingredientes:")
    for (let ingrediente of receita.ingredientes) {
        escreva_ingrediente(ingrediente)
    }

    alert("Modo de preparo:")
    alert(receita.modo_preparo)
}

function leia_receita() {
    let receita = new Receita()
    receita.nome = prompt("Nome da receita:")
    alert("Ingredientes")
    let adicionar_novo_ingrediente = true
    while (adicionar_novo_ingrediente) {
        let ingrediente = leia_ingrediente()
        receita.ingredientes.push(ingrediente)

        let resposta = prompt("Deseja adicionar um novo ingrediente?\nDigite Sim para continuar, qualquer outro valor para terminar")
        adicionar_novo_ingrediente = (resposta.toLowerCase() === "sim")
    }

    receita.modo_preparo = prompt("Modo de preparo para " + receita.nome + ":")

    return receita
}

function escreva_receitas(receitas) {
    alert("Livro de Receitas")
    let numero_receita = 1
    for (receita of receitas) {
        alert("Receita #" + numero_receita)
        escreva_receita(receita)
        alert("")
        ++numero_receita
    }
}

function escreva_menu() {
    let mensagem = "Opções\n\n"
    mensagem += "1. Mostrar todas as receitas cadastradas;\n"
    mensagem += "2. Cadastrar nova receita;\n"
    mensagem += "\n"
    mensagem += "Qualquer outro valor encerra o programa.\n"

    alert(mensagem)
}

function carregue_receitas() {
    let receitas = [
        new Receita("Pão",
                    "...",
                    [
                        new Ingrediente("Água", 3.0, "Xícaras"),
                        new Ingrediente("Farinha", 4.0, "Xícaras"),
                        new Ingrediente("Sal", 2.0, "Colheres de Sopa"),
                        new Ingrediente("Fermento", 2.0, "Colheres de Chá")
                    ]),
        new Receita("Pão Doce",
                    "...",
                    [
                        new Ingrediente("Água", 3.0, "Xícaras"),
                        new Ingrediente("Farinha", 4.0, "Xícaras"),
                        new Ingrediente("Açúcar", 2.0, "Xícaras"),
                        new Ingrediente("Sal", 2.0, "Colheres de Sopa"),
                        new Ingrediente("Fermento", 2.0, "Colheres de Chá")
                    ]),
    ]

    return receitas
}

function main() {
    let receitas = carregue_receitas()
    let fim = false
    while (!fim) {
        escreva_menu()
        let opcao_escolhida = prompt("Escolha uma opção:")
        switch (opcao_escolhida) {
            case "1": {
                escreva_receitas(receitas)
                break
            }

            case "2": {
                let nova_receita = leia_receita()
                receitas.push(nova_receita)
                break
            }

            default: {
                fim = true
            }
        }
    }
}

main()
class Ingrediente:
    def __init__(self, nome = "", quantidade = 0.0, unidade_medida = ""):
        self.nome = nome
        self.quantidade = quantidade
        self.unidade_medida = unidade_medida

class Receita:
    def __init__(self, nome = "", modo_preparo = "", ingredientes = None):
        self.nome = nome
        self.modo_preparo = modo_preparo
        self.ingredientes = ingredientes if (ingredientes != None) else []

def escreva_ingrediente(ingrediente):
    print("- " + ingrediente.nome + ": " + str(ingrediente.quantidade) + " " + ingrediente.unidade_medida)

def leia_ingrediente():
    ingrediente = Ingrediente()
    ingrediente.nome = input("Nome do ingrediente: ")
    ingrediente.quantidade = float(input("Quantidade de " + ingrediente.nome + ": "))
    ingrediente.unidade_medida = input("Unidade de medida: ")

    return ingrediente

def escreva_receita(receita):
    print(receita.nome)
    print("Ingredientes: ")
    for ingrediente in receita.ingredientes:
        escreva_ingrediente(ingrediente)

    print("Modo de preparo: ")
    print(receita.modo_preparo)

def leia_receita():
    receita = Receita()
    receita.nome = input("Nome da receita: ")
    print("Ingredientes")
    adicionar_novo_ingrediente = True
    while (adicionar_novo_ingrediente):
        ingrediente = leia_ingrediente()
        receita.ingredientes.append(ingrediente)

        resposta = input("Deseja adicionar um novo ingrediente?\nDigite Sim para continuar, qualquer outro valor para terminar ")
        adicionar_novo_ingrediente = (resposta.lower() == "sim")

    receita.modo_preparo = input("Modo de preparo para " + receita.nome + ": ")

    return receita

def escreva_receitas(receitas):
    print("Livro de Receitas")
    numero_receita = 1
    for receita in receitas:
        print("Receita #" + str(numero_receita))
        escreva_receita(receita)
        print("")
        numero_receita += 1

def escreva_menu():
    mensagem = "Opções\n\n"
    mensagem += "1. Mostrar todas as receitas cadastradas;\n"
    mensagem += "2. Cadastrar nova receita;\n"
    mensagem += "\n"
    mensagem += "Qualquer outro valor encerra o programa.\n"

    print(mensagem)

def carregue_receitas():
    receitas = [
        Receita("Pão",
                "...",
                [
                    Ingrediente("Água", 3.0, "Xícaras"),
                    Ingrediente("Farinha", 4.0, "Xícaras"),
                    Ingrediente("Sal", 2.0, "Colheres de Sopa"),
                    Ingrediente("Fermento", 2.0, "Colheres de Chá")
                ]),
        Receita("Pão Doce",
                "...",
                [
                    Ingrediente("Água", 3.0, "Xícaras"),
                    Ingrediente("Farinha", 4.0, "Xícaras"),
                    Ingrediente("Açúcar", 2.0, "Xícaras"),
                    Ingrediente("Sal", 2.0, "Colheres de Sopa"),
                    Ingrediente("Fermento", 2.0, "Colheres de Chá")
                ]),
    ]

    return receitas

def main():
    receitas = carregue_receitas()
    fim = False
    while (not fim):
        escreva_menu()
        opcao_escolhida = input("Escolha uma opção: ")
        if (opcao_escolhida == "1"):
            escreva_receitas(receitas)
        elif (opcao_escolhida == "2"):
            nova_receita = leia_receita()
            receitas.append(nova_receita)
        else:
            fim = True

if (__name__ == "__main__"):
    main()
function crie_ingrediente(nome, quantidade, unidade_medida)
    local resultado = {
        nome = nome or "",
        quantidade = quantidade or 0.0,
        unidade_medida = unidade_medida or ""
    }

    return resultado
end

function crie_receita(nome, modo_preparo, ingredientes)
    local resultado = {
        nome = nome or "",
        modo_preparo = quantidade or "",
        ingredientes = ingredientes or {}
    }

    return resultado
end

function escreva_ingrediente(ingrediente)
    print("- " .. ingrediente.nome .. ": " .. ingrediente.quantidade .. " " .. ingrediente.unidade_medida)
end

function leia_ingrediente()
    local ingrediente = crie_ingrediente()
    io.write("Nome do ingrediente: ")
    ingrediente.nome = io.read("*line")
    io.write("Quantidade de " .. ingrediente.nome .. ": ")
    ingrediente.quantidade = io.read("*number", "*line")
    io.write("Unidade de medida: ")
    ingrediente.unidade_medida = io.read("*line")

    return ingrediente
end

function escreva_receita(receita)
    print(receita.nome)
    print("Ingredientes: ")
    for _, ingrediente in ipairs(receita.ingredientes) do
        escreva_ingrediente(ingrediente)
    end

    print("Modo de preparo: ")
    print(receita.modo_preparo)
end

function leia_receita()
    local receita = crie_receita()
    io.write("Nome da receita: ")
    receita.nome = io.read("*line")
    print("Ingredientes")
    local adicionar_novo_ingrediente = true
    while (adicionar_novo_ingrediente) do
        local ingrediente = leia_ingrediente()
        table.insert(receita.ingredientes, ingrediente)

        io.write("Deseja adicionar um novo ingrediente?\nDigite Sim para continuar, qualquer outro valor para terminar ")
        local resposta = io.read("*line*")
        adicionar_novo_ingrediente = (string.lower(resposta) == "sim")
    end

    io.write("Modo de preparo para " .. receita.nome .. ": ")
    receita.modo_preparo = io.read("*line")

    return receita
end

function escreva_receitas(receitas)
    print("Livro de Receitas")
    local numero_receita = 1
    for _, receita in ipairs(receitas) do
        print("Receita #" .. numero_receita)
        escreva_receita(receita)
        print("")
        numero_receita = numero_receita + 1
    end
end

function escreva_menu()
    local mensagem = "Opções\n\n"
    mensagem = mensagem .. "1. Mostrar todas as receitas cadastradas;\n"
    mensagem = mensagem .. "2. Cadastrar nova receita;\n"
    mensagem = mensagem .. "\n"
    mensagem = mensagem .. "Qualquer outro valor encerra o programa.\n"

    print(mensagem)
end

function carregue_receitas()
    local receitas = {
        crie_receita("Pão",
                     "...",
                     {
                         crie_ingrediente("Água", 3.0, "Xícaras"),
                         crie_ingrediente("Farinha", 4.0, "Xícaras"),
                         crie_ingrediente("Sal", 2.0, "Colheres de Sopa"),
                         crie_ingrediente("Fermento", 2.0, "Colheres de Chá")
                     }),
        crie_receita("Pão Doce",
                     "...",
                     {
                         crie_ingrediente("Água", 3.0, "Xícaras"),
                         crie_ingrediente("Farinha", 4.0, "Xícaras"),
                         crie_ingrediente("Açúcar", 2.0, "Xícaras"),
                         crie_ingrediente("Sal", 2.0, "Colheres de Sopa"),
                         crie_ingrediente("Fermento", 2.0, "Colheres de Chá")
                     }),
    }

    return receitas
end

function main()
    local receitas = carregue_receitas()
    local fim = false
    while (not fim) do
        escreva_menu()
        io.write("Escolha uma opção: ")
        local opcao_escolhida = io.read("*line")
        if (opcao_escolhida == "1") then
            escreva_receitas(receitas)
        elseif (opcao_escolhida == "2") then
            local nova_receita = leia_receita()
            table.insert(receitas, nova_receita)
        else
            fim = true
        end
    end
end

main()
extends Node

func inteiro_aleatorio(minimo_inclusive, maximo_inclusive):
    var minimo = ceil(minimo_inclusive)
    var maximo = floor(maximo_inclusive)

    # randi(): [0.0, 1.0[
    return randi() % int(maximo + 1 - minimo) + minimo

func input(mensagem, valor_esperado):
    print(mensagem)

    if (valor_esperado == "sorteio menu"):
        randomize()
        return str(inteiro_aleatorio(1, 3))

    return valor_esperado

class Ingrediente:
    var nome
    var quantidade
    var unidade_medida

    func _init(nome = "", quantidade = 0.0, unidade_medida = ""):
        self.nome = nome
        self.quantidade = quantidade
        self.unidade_medida = unidade_medida

class Receita:
    var nome
    var modo_preparo
    var ingredientes

    func _init(nome = "", modo_preparo = "", ingredientes = []):
        self.nome = nome
        self.modo_preparo = modo_preparo
        self.ingredientes = ingredientes

func escreva_ingrediente(ingrediente):
    print("- " + ingrediente.nome + ": " + str(ingrediente.quantidade) + " " + ingrediente.unidade_medida)

func leia_ingrediente():
    var ingrediente = Ingrediente.new()
    ingrediente.nome = input("Nome do ingrediente: ", "farinha")
    ingrediente.quantidade = float(input("Quantidade de " + ingrediente.nome + ": ", "1.23"))
    ingrediente.unidade_medida = input("Unidade de medida: ", "kg")

    return ingrediente

func escreva_receita(receita):
    print(receita.nome)
    print("Ingredientes: ")
    for ingrediente in receita.ingredientes:
        escreva_ingrediente(ingrediente)

    print("Modo de preparo: ")
    print(receita.modo_preparo)

func leia_receita():
    var receita = Receita.new()
    receita.nome = input("Nome da receita: ", "Farinha")
    print("Ingredientes")
    var adicionar_novo_ingrediente = true
    while (adicionar_novo_ingrediente):
        var ingrediente = leia_ingrediente()
        receita.ingredientes.append(ingrediente)

        var resposta = input("Deseja adicionar um novo ingrediente?\nDigite Sim para continuar, qualquer outro valor para terminar ", "não")
        adicionar_novo_ingrediente = (resposta.to_lower() == "sim")

    receita.modo_preparo = input("Modo de preparo para " + receita.nome + ": ", "Não comestível!")

    return receita

func escreva_receitas(receitas):
    print("Livro de Receitas")
    var numero_receita = 1
    for receita in receitas:
        print("Receita #" + str(numero_receita))
        escreva_receita(receita)
        print("")
        numero_receita += 1

func escreva_menu():
    var mensagem = "Opções\n\n"
    mensagem += "1. Mostrar todas as receitas cadastradas;\n"
    mensagem += "2. Cadastrar nova receita;\n"
    mensagem += "\n"
    mensagem += "Qualquer outro valor encerra o programa.\n"

    print(mensagem)

func carregue_receitas():
    var receitas = [
        Receita.new("Pão",
                    "...",
                    [
                        Ingrediente.new("Água", 3.0, "Xícaras"),
                        Ingrediente.new("Farinha", 4.0, "Xícaras"),
                        Ingrediente.new("Sal", 2.0, "Colheres de Sopa"),
                        Ingrediente.new("Fermento", 2.0, "Colheres de Chá")
                    ]),
        Receita.new("Pão Doce",
                    "...",
                    [
                        Ingrediente.new("Água", 3.0, "Xícaras"),
                        Ingrediente.new("Farinha", 4.0, "Xícaras"),
                        Ingrediente.new("Açúcar", 2.0, "Xícaras"),
                        Ingrediente.new("Sal", 2.0, "Colheres de Sopa"),
                        Ingrediente.new("Fermento", 2.0, "Colheres de Chá")
                    ]),
    ]

    return receitas

func _ready():
    var receitas = carregue_receitas()
    var fim = false
    while (not fim):
        escreva_menu()
        var opcao_escolhida = input("Escolha uma opção: ", "sorteio menu")
        if (opcao_escolhida == "1"):
            escreva_receitas(receitas)
        elif (opcao_escolhida == "2"):
            var nova_receita = leia_receita()
            receitas.append(nova_receita)
        else:
            fim = true

Na versão para JavaScript, pode-se trocar alert() por console.log() para escrever o resultado no console do navegador (ao invés de um painel de alerta). Uma melhoria desejável para o uso de alert() seria retornar cadeias de caracteres para apresentar receitas em um único diálogo (ao invés de um diálogo por frase). Para isso, poder-se-ia criar uma função ingrediente_para_string() que retornasse o texto (ao invés de escrevê-lo como feito em escreva_ingrediente()). A lista de ingredientes como cadeias de caracteres poderia ser concatenada em uma única mensagem, para a geração do alerta com os outros dados da receita. O procedimento escreva_ingrediente() também poderia ser refatorado para usar a nova função.

Como GDScript não possui subrotinas para entrada em console (terminal), a implementação define uma função input() com um segundo parâmetro como valor esperado para o retorno. Para o uso do menu principal, a função retorna um valor pseudoaleatório entre 1 e 3, simulando a escolha de uma opção. Assim, o resultado poderá ser diferente a cada uso do programa. A técnica pode servir como ferramenta útil para automação de testes, como forma de simular entradas de usuárias ou usuários finais em um programa. Para testes, ao invés de valores aleatórios, poder-se-ia definir um vetor com uma seqüência de entradas (e retornar o valor da próxima posição a cada solicitação). Além disso, neste momento já é possível começar a implementar interfaces gráficas usando Godot Engine. Elas serão exploradas em tópicos futuros, possivelmente após uma breve introdução sobre OOP. Caso você já queira tentar, você poderia seguir os passos definidos na preparação do ambiente de desenvolvimento para GDScript (Godot) para começar com formulários mais simples que o deste exemplo.

Cessando a digressão, o programa resultante é modular, organizado e simples de ler. Os nomes de subrotinas descrevem o que elas fazem, simplificando a leitura do código do programa. Por exemplo, a leitura de main() (criada como programa principal) permitem entender rapidamente o que o programa faz:

  • Inicialização de receitas;
  • Escrita de menu de opções;
  • Leitura (entrada) de opção:
    1. Escreve receitas salvas;
    2. Adiciona nova receita.
    3. (Ou qualquer outro valor) Encerra o programa.

Também é fácil modificar e estender o programa, pois basta adicionar ou remover funcionalidades das respectivas subrotinas (ou criar novas opções para o menu). Por exemplo, para adicionar validação de dados para a leitura de um ingrediente, poder-se-ia modificar a função leia_ingrediente(). Após a introdução de arquivos, também seria possível salvar e carregar receitas de arquivos para persistir dados entre diversas sessões de uso do programa. Assim, não seria mais necessário predefinir receitas no código-fonte do programa.

Conforme adquire-se experiência em programação, deve-se começar a pensar em arquiteturas de software para organização e estruturação de programas. Boas arquiteturas facilitam a implementação e a manutenção de sistemas.

Por exemplo, é uma boa prática de programação separar funcionalidades de entrada e saída de dados de funcionalidades de modelagem de dados e de processamento de dados. Isso pode ser feito, dentre outros, usando um modelo de três camadas, uma arquitetura multicamada, ou o padrão de software (software pattern) Model-View-Controller (MVC). A próxima seção apresenta um exemplo para implementar uma simulação. Embora o conceito de camadas seja normalmente associado a OOP, ele pode ser explorado de forma procedural (e funcional).

Jogo da Vida (Conway's Game Of Life)

Para ilustrar a separação de lógica e apresentação de um programa, o próximo exemplo apresenta a implementação de um algoritmo chamada Jogo da Vida, mais conhecido pelo nome em Inglês definido pelo criador: Conway's Game of Life. O Jogo da Vida é um exemplo simples de autômato celular. Ele será o primeiro exemplo deste material com uma animação ao invés de saída estática (consequentemente, infelizmente o programa será inacessível para pessoas não videntes).

A citação a seguir contém as regras para o Jogo da Vida definidas no artigo da Wikipedia:

  1. Qualquer célula viva com menos de dois vizinhos vivos morre de solidão.
  2. Qualquer célula viva com mais de três vizinhos vivos morre de superpopulação.
  3. Qualquer célula morta com exatamente três vizinhos vivos se torna uma célula viva.
  4. Qualquer célula viva com dois ou três vizinhos vivos continua no mesmo estado para a próxima geração.

Para implementar o jogo, define-se uma matriz como um tabuleiro (também chamado de grelha ou grid em Inglês) e adiciona-se valores como células. Para a implementação das regras, deve-se checar as células (da tabela) vizinhas a cada posição da matriz. A comparação de valores é feita usando os índices da matriz. Por exemplo, m[linha][coluna], m[linha][coluna + 1] corresponde à célula na próxima coluna.

Cada posição tem até 8 vizinhos (caso do 5 na tabela a seguir), embora seja possível existir menos. Por exemplo, nas extremidades da tabela, a célula pode ter apenas 3 vizinhos (caso de 1, 3, 7 e 9 no exemplo a seguir).

| 1 | 2 | 3 |
| 4 | 5 | 6 |
| 7 | 8 | 9 |

Uma forma de facilitar a resolução do problema é inserir linhas e colunas adicionais antes da primeira e a após última.

|   |   |   |   |   |
|   | 1 | 2 | 3 |   |
|   | 4 | 5 | 6 |   |
|   | 7 | 8 | 9 |   |
|   |   |   |   |   |

Com a alteração, todas as células com dados terão 8 vizinhos. Isso facilita a implementação, porque é possível verificar todas as células adjacentes sem preocupações com casos particulares e exceções. Ou seja, é possível comparar todos os vizinhos da mesma forma, pois todos os valores válidos comportar-se-ão como 5 na tabela anterior. Pode ser mais fácil entender o motivo considerando-se deslocamentos do valor central:

| (-1, -1) | (-1, 0) | (-1, 1) |
|  (0, -1) |  (0, 0) |  (0, 1) |
|  (1, -1) |  (1, 0) |  (1, 1) |

O elemento central (o par linha 0, coluna 0, ou seja, (0, 0)) é ignorado, pois uma célula não é vizinha dela mesma. Os demais deslocamentos podem ser implementados como índices na matriz de valores.

tabuleiro[linha - 1][coluna - 1]
tabuleiro[linha - 1][coluna]
tabuleiro[linha - 1][coluna + 1]
tabuleiro[linha][coluna - 1]
tabuleiro[linha][coluna + 1]
tabuleiro[linha + 1][coluna - 1]
tabuleiro[linha + 1][coluna]
tabuleiro[linha + 1][coluna + 1]

Pode-se aplicar diretamente os valores anteriores na solução ou escrever dois laços aninhado para obter os índices.

for linha_vizinha in range(-1, 2):
    for coluna_vizinha in range(-1, 2):
        if (not ((linha_vizinha == 0) and (coluna_vizinha == 0))):
            // ...

Ambas as variações são válidas. A primeira é mais simples; a segunda é mais genérica e permitiria modificar a solução mais facilmente caso se desejasse verificar por células mais distantes. Com as informações anteriores, basta, agora, implementar as regras definidas.

const LINHAS_TABULEIRO = 20
const COLUNAS_TABULEIRO = 20

const CELULA_MORTA = 0
const CELULA_VIVA = 1

function inteiro_aleatorio(minimo_inclusive, maximo_inclusive) {
    let minimo = Math.ceil(minimo_inclusive)
    let maximo = Math.floor(maximo_inclusive)

    return Math.floor(minimo + Math.random() * (maximo + 1 - minimo))
}

class Tabuleiro {
    constructor(linhas = LINHAS_TABULEIRO, colunas = COLUNAS_TABULEIRO) {
        this.linhas = linhas
        this.colunas = colunas
        this.valores = []
        linhas += 2
        colunas += 2
        for (let linha = 0; linha < linhas; ++linha) {
            let nova_coluna = []
            this.valores.push(nova_coluna)
            for (let coluna = 0; coluna < colunas; ++coluna) {
                nova_coluna.push(CELULA_MORTA)
            }
        }
    }
}

function inicialize_tabuleiro(tabuleiro) {
    let linhas = tabuleiro.linhas + 1
    let colunas = tabuleiro.colunas + 1
    for (let linha = 1; linha < linhas; ++linha) {
        for (let coluna = 1; coluna < colunas; ++coluna) {
            if (inteiro_aleatorio(0, 100) < 30) {
                tabuleiro.valores[linha][coluna] = CELULA_VIVA
            } else {
                tabuleiro.valores[linha][coluna] = CELULA_MORTA
            }
        }
    }
}

function atualize_tabuleiro(tabuleiro) {
    let linhas = tabuleiro.linhas + 1
    let colunas = tabuleiro.colunas + 1
    // Referência.
    let valores = tabuleiro.valores

    // Contagem de vizinhos vivos.
    let tabuleiro_contagem = new Tabuleiro(tabuleiro.linhas, tabuleiro.colunas)
    let contagem_valores = tabuleiro_contagem.valores
    for (let linha = 1; linha < linhas; ++linha) {
        for (let coluna = 1; coluna < colunas; ++coluna) {
            let vizinhos_vivos = 0
            for (let linha_vizinha = -1; linha_vizinha < 2; ++linha_vizinha) {
                for (let coluna_vizinha = -1; coluna_vizinha < 2; ++coluna_vizinha) {
                    if (!((linha_vizinha === 0) && (coluna_vizinha === 0))) {
                        if (valores[linha + linha_vizinha][coluna + coluna_vizinha] === CELULA_VIVA) {
                            ++vizinhos_vivos
                        }
                    }
                }
            }

            contagem_valores[linha][coluna] = vizinhos_vivos
        }
    }

    // Atualização de estado baseada na contagem de vizinhos.
    for (let linha = 1; linha < linhas; ++linha) {
        for (let coluna = 1; coluna < colunas; ++coluna) {
            if (valores[linha][coluna] === CELULA_VIVA) {
                if (contagem_valores[linha][coluna] < 2) {
                    // Qualquer célula viva com menos de dois vizinhos vivos
                    // morre de solidão.
                    valores[linha][coluna] = CELULA_MORTA
                } else if (contagem_valores[linha][coluna] > 3) {
                    // Qualquer célula viva com mais de três vizinhos vivos
                    // morre de superpopulação.
                    valores[linha][coluna] = CELULA_MORTA
                } /* else {
                    // Redundante neste caso, já a mudança é in-place.
                    // Qualquer célula viva com dois ou três vizinhos vivos
                    // continua no mesmo estado para a próxima geração.
                    valores[linha][coluna] = valores[linha][coluna]
                } */
            } else {
                if (contagem_valores[linha][coluna] === 3) {
                    // Qualquer célula morta com exatamente três vizinhos vivos
                    // se torna uma célula viva.
                    valores[linha][coluna] = CELULA_VIVA
                }
            }
        }
    }
}

function desenhe_tabuleiro(tabuleiro) {
    let linhas = tabuleiro.linhas + 1
    let colunas = tabuleiro.colunas + 1
    let mensagem = ""
    for (let linha = 1; linha < linhas; ++linha) {
        for (let coluna = 1; coluna < colunas; ++coluna) {
            if (tabuleiro.valores[linha][coluna] === CELULA_VIVA) {
                mensagem += "⏹" // Ou "X" ou "*" (qualquer caractere).
            } else {
                mensagem += " "
            }
        }

        mensagem += "\n"
    }

    clear()
    console.log(mensagem)
    // alert(mensagem)
}

async function main() {
    let tabuleiro = new Tabuleiro()
    inicialize_tabuleiro(tabuleiro)
    let numero_iteracoes = 100
    for (let iteracao = 0; iteracao < numero_iteracoes; ++iteracao) {
        atualize_tabuleiro(tabuleiro)
        desenhe_tabuleiro(tabuleiro)
        // sleep(): Aguarda 300ms antes da próxima atualização.
        await new Promise(resolve => setTimeout(resolve, 300))
    }
}

main()
import random
import time
from typing import Final

LINHAS_TABULEIRO: Final = 20
COLUNAS_TABULEIRO: Final = 20

CELULA_MORTA: Final = 0
CELULA_VIVA: Final = 1

def limpe_console(numero_linhas = LINHAS_TABULEIRO):
    for contador in range(numero_linhas):
        print("\n")

class Tabuleiro:
    def __init__(self, linhas = LINHAS_TABULEIRO, colunas = COLUNAS_TABULEIRO):
        self.linhas = linhas
        self.colunas = colunas
        self.valores = []
        linhas += 2
        colunas += 2
        for linha in range(linhas):
            nova_coluna = []
            self.valores.append(nova_coluna)
            for coluna in range(colunas):
                nova_coluna.append(CELULA_MORTA)

def inicialize_tabuleiro(tabuleiro):
    linhas = tabuleiro.linhas + 1
    colunas = tabuleiro.colunas + 1
    for linha in range(1, linhas):
        for coluna in range(1, colunas):
            if (random.randint(0, 100) < 30):
                tabuleiro.valores[linha][coluna] = CELULA_VIVA
            else:
                tabuleiro.valores[linha][coluna] = CELULA_MORTA

def atualize_tabuleiro(tabuleiro):
    linhas = tabuleiro.linhas + 1
    colunas = tabuleiro.colunas + 1
    # Referência.
    valores = tabuleiro.valores

    # Contagem de vizinhos vivos.
    tabuleiro_contagem = Tabuleiro(tabuleiro.linhas, tabuleiro.colunas)
    contagem_valores = tabuleiro_contagem.valores
    for linha in range(1, linhas):
        for coluna in range(1, colunas):
            vizinhos_vivos = 0
            for linha_vizinha in range(-1, 2):
                for coluna_vizinha in range(-1, 2):
                    if (not ((linha_vizinha == 0) and (coluna_vizinha == 0))):
                        if (valores[linha + linha_vizinha][coluna + coluna_vizinha] == CELULA_VIVA):
                            vizinhos_vivos += 1

            contagem_valores[linha][coluna] = vizinhos_vivos

    # Atualização de estado baseada na contagem de vizinhos.
    for linha in range(1, linhas):
        for coluna in range(1, colunas):
            if (valores[linha][coluna] == CELULA_VIVA):
                if (contagem_valores[linha][coluna] < 2):
                    # Qualquer célula viva com menos de dois vizinhos vivos
                    # morre de solidão.
                    valores[linha][coluna] = CELULA_MORTA
                elif (contagem_valores[linha][coluna] > 3):
                    # Qualquer célula viva com mais de três vizinhos vivos
                    # morre de superpopulação.
                    valores[linha][coluna] = CELULA_MORTA
                # else:
                    # Redundante neste caso, já a mudança é in-place.
                    # Qualquer célula viva com dois ou três vizinhos vivos
                    # continua no mesmo estado para a próxima geração.
                    # valores[linha][coluna] = valores[linha][coluna]
            else:
                if (contagem_valores[linha][coluna] == 3):
                    # Qualquer célula morta com exatamente três vizinhos vivos
                    # se torna uma célula viva.
                    valores[linha][coluna] = CELULA_VIVA

def desenhe_tabuleiro(tabuleiro):
    linhas = tabuleiro.linhas + 1
    colunas = tabuleiro.colunas + 1
    mensagem = ""
    for linha in range(1, linhas):
        for coluna in range(1, colunas):
            if (tabuleiro.valores[linha][coluna] == CELULA_VIVA):
                mensagem += "⏹" # Ou "X" ou "*" (qualquer caractere).
            else:
                mensagem += " "

        mensagem += "\n"

    limpe_console()
    print(mensagem)

def main():
    random.seed()
    tabuleiro = Tabuleiro()
    inicialize_tabuleiro(tabuleiro)
    numero_iteracoes = 100
    for iteracao in range(numero_iteracoes):
        atualize_tabuleiro(tabuleiro)
        desenhe_tabuleiro(tabuleiro)
        # sleep(): Aguarda 300ms antes da próxima atualização.
        time.sleep(0.3)

if (__name__ == "__main__"):
    main()
local LINHAS_TABULEIRO = 20
local COLUNAS_TABULEIRO = 20

local CELULA_MORTA = 0
local CELULA_VIVA = 1

-- Implementação ineficiente usando espera ocupada.
function sleep(tempo_segundos)
    local fim = tonumber(os.clock() + tempo_segundos);
    while (os.clock() < fim) do
        -- Espera o tempo passar, desperdiçando ciclos do processador.
    end
end

function limpe_console(numero_linhas)
    numero_linhas = numero_linhas or LINHAS_TABULEIRO
    for contador = 1, numero_linhas do
        print("\n")
    end
end

function crie_tabuleiro(linhas, colunas)
    linhas = linhas or LINHAS_TABULEIRO
    colunas = colunas or COLUNAS_TABULEIRO
    local resultado = {
        linhas = linhas,
        colunas = colunas,
        valores = {}
    }

    linhas = linhas + 2
    colunas = colunas + 2
    for linha = 2, linhas do
        local nova_coluna = {}
        table.insert(resultado.valores, nova_coluna)
        for coluna = 2, colunas do
            table.insert(nova_coluna, CELULA_MORTA)
        end
    end

    return resultado
end

function inicialize_tabuleiro(tabuleiro)
    local linhas = tabuleiro.linhas
    local colunas = tabuleiro.colunas
    for linha = 2, linhas do
        for coluna = 2, colunas do
            if (math.random(0, 100) < 30) then
                tabuleiro.valores[linha][coluna] = CELULA_VIVA
            else
                tabuleiro.valores[linha][coluna] = CELULA_MORTA
            end
        end
    end
end

function atualize_tabuleiro(tabuleiro)
    local linhas = tabuleiro.linhas
    local colunas = tabuleiro.colunas
    -- Referência.
    local valores = tabuleiro.valores

    -- Contagem de vizinhos vivos.
    local tabuleiro_contagem = crie_tabuleiro(tabuleiro.linhas, tabuleiro.colunas)
    local contagem_valores = tabuleiro_contagem.valores
    for linha = 2, linhas do
        for coluna = 2, colunas do
            local vizinhos_vivos = 0
            for linha_vizinha = -1, 1 do
                for coluna_vizinha = -1, 1 do
                    if (not ((linha_vizinha == 0) and (coluna_vizinha == 0))) then
                        if (valores[linha + linha_vizinha][coluna + coluna_vizinha] == CELULA_VIVA) then
                            vizinhos_vivos = vizinhos_vivos + 1
                        end
                    end
                end
            end

            contagem_valores[linha][coluna] = vizinhos_vivos
        end
    end

    -- Atualização de estado baseada na contagem de vizinhos.
    for linha = 2, linhas do
        for coluna = 2, colunas do
            if (valores[linha][coluna] == CELULA_VIVA) then
                if (contagem_valores[linha][coluna] < 2) then
                    -- Qualquer célula viva com menos de dois vizinhos vivos
                    -- morre de solidão.
                    valores[linha][coluna] = CELULA_MORTA
                elseif (contagem_valores[linha][coluna] > 3) then
                    -- Qualquer célula viva com mais de três vizinhos vivos
                    -- morre de superpopulação.
                    valores[linha][coluna] = CELULA_MORTA
                -- else:
                    -- Redundante neste caso, já a mudança é in-place.
                    -- Qualquer célula viva com dois ou três vizinhos vivos
                    -- continua no mesmo estado para a próxima geração.
                    -- valores[linha][coluna] = valores[linha][coluna]
                end
            else
                if (contagem_valores[linha][coluna] == 3) then
                    -- Qualquer célula morta com exatamente três vizinhos vivos
                    -- se torna uma célula viva.
                    valores[linha][coluna] = CELULA_VIVA
                end
            end
        end
    end
end

function desenhe_tabuleiro(tabuleiro)
    local linhas = tabuleiro.linhas
    local colunas = tabuleiro.colunas
    local mensagem = ""
    for linha = 2, linhas do
        for coluna = 2, colunas do
            if (tabuleiro.valores[linha][coluna] == CELULA_VIVA) then
                mensagem = mensagem .. "⏹" -- Ou "X" ou "*" (qualquer caractere).
            else
                mensagem = mensagem .. " "
            end
        end

        mensagem = mensagem .. "\n"
    end

    limpe_console()
    print(mensagem)
end

function main()
    math.randomseed(os.time())
    local tabuleiro = crie_tabuleiro()
    inicialize_tabuleiro(tabuleiro)
    local numero_iteracoes = 100
    for iteracao = 1, numero_iteracoes do
        atualize_tabuleiro(tabuleiro)
        desenhe_tabuleiro(tabuleiro)
        -- sleep(): Aguarda 300ms antes da próxima atualização.
        sleep(0.3)
    end
end

main()
extends Node

const LINHAS_TABULEIRO = 20
const COLUNAS_TABULEIRO = 20

const CELULA_MORTA = 0
const CELULA_VIVA = 1

func inteiro_aleatorio(minimo_inclusive, maximo_inclusive):
    var minimo = ceil(minimo_inclusive)
    var maximo = floor(maximo_inclusive)

    # randi(): [0.0, 1.0[
    return randi() % int(maximo + 1 - minimo) + minimo

func limpe_console(numero_linhas = LINHAS_TABULEIRO):
    for contador in range(numero_linhas):
        print("\n")

class Tabuleiro:
    var linhas
    var colunas
    var valores

    func _init(linhas = LINHAS_TABULEIRO, colunas = COLUNAS_TABULEIRO):
        self.linhas = linhas
        self.colunas = colunas
        self.valores = []
        linhas += 2
        colunas += 2
        for linha in range(linhas):
            var nova_coluna = []
            self.valores.append(nova_coluna)
            for coluna in range(colunas):
                nova_coluna.append(CELULA_MORTA)

func inicialize_tabuleiro(tabuleiro):
    var linhas = tabuleiro.linhas + 1
    var colunas = tabuleiro.colunas + 1
    for linha in range(1, linhas):
        for coluna in range(1, colunas):
            if (inteiro_aleatorio(0, 100) < 30):
                tabuleiro.valores[linha][coluna] = CELULA_VIVA
            else:
                tabuleiro.valores[linha][coluna] = CELULA_MORTA

func atualize_tabuleiro(tabuleiro):
    var linhas = tabuleiro.linhas + 1
    var colunas = tabuleiro.colunas + 1
    # Referência.
    var valores = tabuleiro.valores

    # Contagem de vizinhos vivos.
    var tabuleiro_contagem = Tabuleiro.new(tabuleiro.linhas, tabuleiro.colunas)
    var contagem_valores = tabuleiro_contagem.valores
    for linha in range(1, linhas):
        for coluna in range(1, colunas):
            var vizinhos_vivos = 0
            for linha_vizinha in range(-1, 2):
                for coluna_vizinha in range(-1, 2):
                    if (not ((linha_vizinha == 0) and (coluna_vizinha == 0))):
                        if (valores[linha + linha_vizinha][coluna + coluna_vizinha] == CELULA_VIVA):
                            vizinhos_vivos += 1

            contagem_valores[linha][coluna] = vizinhos_vivos

    # Atualização de estado baseada na contagem de vizinhos.
    for linha in range(1, linhas):
        for coluna in range(1, colunas):
            if (valores[linha][coluna] == CELULA_VIVA):
                if (contagem_valores[linha][coluna] < 2):
                    # Qualquer célula viva com menos de dois vizinhos vivos
                    # morre de solidão.
                    valores[linha][coluna] = CELULA_MORTA
                elif (contagem_valores[linha][coluna] > 3):
                    # Qualquer célula viva com mais de três vizinhos vivos
                    # morre de superpopulação.
                    valores[linha][coluna] = CELULA_MORTA
                # else:
                    # Redundante neste caso, já a mudança é in-place.
                    # Qualquer célula viva com dois ou três vizinhos vivos
                    # continua no mesmo estado para a próxima geração.
                    # valores[linha][coluna] = valores[linha][coluna]
            else:
                if (contagem_valores[linha][coluna] == 3):
                    # Qualquer célula morta com exatamente três vizinhos vivos
                    # se torna uma célula viva.
                    valores[linha][coluna] = CELULA_VIVA

func desenhe_tabuleiro(tabuleiro):
    var linhas = tabuleiro.linhas + 1
    var colunas = tabuleiro.colunas + 1
    var mensagem = ""
    for linha in range(1, linhas):
        for coluna in range(1, colunas):
            if (tabuleiro.valores[linha][coluna] == CELULA_VIVA):
                mensagem += "X" # Ou "X" ou "*" (qualquer caractere).
            else:
                mensagem += " "

        mensagem += "\n"

    limpe_console()
    print(mensagem)

func _ready():
    randomize()
    var tabuleiro = Tabuleiro.new()
    inicialize_tabuleiro(tabuleiro)
    var numero_iteracoes = 100
    for iteracao in range(numero_iteracoes):
        atualize_tabuleiro(tabuleiro)
        desenhe_tabuleiro(tabuleiro)
        # sleep(): Aguarda 300ms antes da próxima atualização.
        yield(get_tree().create_timer(0.3), "timeout")

Para o exemplo, primeiro convém notar a estruturação da solução. Pode-se observar que toda atualização de lógica do programa ocorre em atualize_tabuleiro(). Da mesma forma, a apresentação do tabuleiro ocorre em desenhe_tabuleiro(). O programa não lê entradas de usuários (ou usuárias) finais; caso o fizesse, poder-se-ia definir uma nova subrotina chamada leia_entrada(). Os valores lidos seriam, então, passados como parâmetros para atualize_tabuleiro() (ou seja, a atualização da lógica do programa não faria a leitura de dados diretamente).

Além disso, o exemplo introduz alguns recursos novos, usadas para a animação quadro a quadro (frame a frame).

  1. Uma subrotina para limpar o console (terminal). JavaScript fornece o console.clear() (documentação) para a limpeza. Em Python, Lua e GDScript, o procedimento limpe_console() escreve linhas vazias para simular uma alternativa. Pode-se interessante escrever ainda mais linhas para um resultado melhor;
  2. Uma subrotina para esperar (pausar) o programa por um determinado tempo, normalmente chamada de sleep() (algo como durma). JavaScript utiliza uma Promise (documentação) e setTimeout() (documentação). Em JavaScript, também é importante observar que main() deve ser declarada como função async para uso das subrotinas anteriores. Python fornece time.sleep() (documentação). GDScript provê get_tree().create_timer() (documentação). Lua não possui nenhuma subrotina pré-definida; a implementação de sleep() utiliza uma técnica chamada espera ocupada. A técnica repete um bloco de código até que uma condição torne-se falsa, para desperdiçar tempo. No caso, isso é feito para esperar um certo tempo até encerrar a repetição. Um implementação mais simples poderia ser um bloco vazio repetido algumas milhares de vezes.

Nas implementações, o incremento de linhas e colunas permitem ignorar valores vazios para desenhar ou atualizar tabuleiro. Ao invés de somar linhas = tabuleiro.linhas + 2 e usar uma condição linhas - 1, pode-se simplificar o código fazendo-se tabuleiro.linhas + 1 diretamente. Em Lua, como a indexação inicia em 1, pode-se usar o valor original diretamente.

Dependendo da seqüência inicial gerada, o Jogo da Vida pode gerar padrões que se repetem infinitamente. A página inglesa da Wikipedia apresenta alguns padrões. Caso você execute o programa algumas vezes, talvez você obtenha padrões assim (dependendo da sorte). Alternativamente, você pode criar um tabuleiro inicial com uma configuração adequada para o padrão que quiser.

Por exemplo, em um tabuleiro 48x48 (50x50 com os zeros inseridos nas extremidades):

Mostrar/Ocultar Tabuleiro

Para melhor visualização, é recomendável usar um caractere como X ao invés do quadrado (para garantir espaçamento igual entre caracteres).

O exemplo provê as configurações para JavaScript, mas os princípios (e valores) são os mesmos para outras linguagens. Em Lua, deve-se substituir colchetes por chaves.

const LINHAS_TABULEIRO = 47
const COLUNAS_TABULEIRO = 47

const CELULA_MORTA = 0
const CELULA_VIVA = 1

// ...
function inicialize_tabuleiro(tabuleiro) {
    tabuleiro.valores = [
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ]
}

Embora os exemplos adicionem um limite máximo de repetições, poder-se-ia usar um laço infinito para a simulação continuasse. Além disso, para melhor visualização, é interessante escolher um número de linhas que permita limpar totalmente a tela a cada iteração, para que o próximo desenho ocorra sempre na mesma região de tela.

O Jogo da Vida é interessante porque define uma simulação simples, porém completa. A implementação apresentada ilustra como sistemas como jogos digitais, animações, vídeos ou simulações funcionam: processa-se e desenha-se conteúdo repetidamente, criando-se a ilusão de animação. Um exemplo usando material cotidiano seriam desenhos animados em papel. Cada página altera um pouco o desenho; ao se folhear rapidamente a seqüência de páginas, gera-se a impressão de animação. Uma coleção de imagens assim chama-se folioscópio (ou flip book, em inglês).

Simulações digitais funcionam similarmente. Elas repetem um código continuamente. Alterações de estado entre uma repetição e outra podem ser desenhadas. O desenho rápido gera a ilusão de animação.

Como computadores modernos são máquinas rápidas, o uso de uma subrotina como sleep() permite esperar um tempo arbitrário entre duas atualizações. Caso contrário, exceto caso o número de repetições seja muito grande, o programa pode terminar quase instantaneamente. A última imagem gerada será a exibida ao final do programa, resultando em um desenho estático (alternativamente, dependendo da solução, pode-se rolar a saída do terminal para verificar as saídas anteriores).

Por sinal, praticamente toda aplicação com interface gráfica opera de forma similar ao exemplo. Usa-se uma cor sólida de fundo para limpar a tela; desenha-se o novo conteúdo. Então, repete-se o processo a cada mudança de dados (ou intervalo de tempo). Este é o princípio usado para exibir esta página em seu monitor, qualquer programa em uso, jogos digitais e outros conteúdos multimídia.

Caso você esteja usando um leitor de tela e ouvindo o conteúdo desta página (ou ouvindo música), o funcionamento de som digital também é similar. Toca-se um ruído, avança-se os dados, toca-se o próximo ruído. O som é criado ao longo do tempo, quadro a quadro. Mais exatamente, amostra (sample) a amostra.

Novos Itens para Seu Inventário

Ferramentas:

  • Função como simulador de entradas de usuário final em um programa;
  • Folioscópio (flip book).

Habilidades:

  • Criação de registros;
  • Programação em alto nível.

Conceitos:

  • Registros (structs ou records);
  • Decomposição de dados;
  • Abstração de dados;
  • Atributos ou campos;
  • Estado;
  • Plain Old Data (POD) or Passive Data Structures (PDS);
  • Classes;
  • Métodos;
  • Instância;
  • Data hiding;
  • Encapsulamento;
  • Interfaces;
  • Fluxo de dados;
  • Construtor;
  • Parâmetros nomeados;
  • Vetor de Registros (Array of Structures);
  • Granularidade;
  • Tipos de dados recursivos;
  • Consistência;
  • Detalhe de implementação;
  • Espera ocupada.

Recursos de programação:

  • Registros;
  • Definição e uso de parâmetros nomeados.

Pratique

Excetuando-se o uso de tipos de dados recursivos (com referências para o próprio tipo de registro), registros agregam maior comodidade para programação, mas não permitem resolver muitos problemas diferentes do que já era possível. Uma boa forma de praticar o uso é refatorar alguns de seus programas antigos para usar registros. Em particular, adotar um estilo de programação com abstrações de dados e funcionais permitirá programar em nível mais alto; as soluções resultantes poderão ser mais legíveis e elegantes. Revistar seus programas antigos também permitirá constatar sua evolução como programadora ou programador. Você perceberá situações em que poderia ter optado por uma solução mais simples ou uma técnica mais avançada (que você não conhecia até então).

  1. Crie um registro chamado Animal. Adicione dados característicos de animais (por exemplo, espécie e nome científico) e monte uma pequena base de dados com informações sobre seus animais favoritos. Caso prefira plantas, você pode criar um registro Vegetal.

  2. Crie um registro chamado Produto e crie um pequeno sistema de controle de estoque. O sistema deve armazenar nomes, quantidades, números de lotes e preços de produtos.

  3. Crie um registro chamado Livro e um registro chamado Autor. Armazene seus livros favoritos. Cada livro deve possuir título, um vetor de autores e um resumo (ou comentário). Implemente subrotinas para adicionar, listar e procurar por livros. A busca pode ser feita por título ou por autores.

  4. Crie uma subrotina para verificar se dois registros são iguais. A subrotina deve retornar um valor lógico.

  5. Como ordenar um vetor de registros? Dica: pode-se criar uma subrotina com um critério para informar se um registro é menor ou maior que outro. A ordenação é feita usando um ou mais atributos do registro. Atributos comparados primeiro terão maior prioridade na ordenação.

  6. Crie um cardápio para um restaurante usando registros.

  7. Quais vantagens você citaria para o uso de registros em um programa? Quais desvantagens?

  8. É possível criar um registro vazio, isto é, sem nenhum atributo? Caso seja possível, você consegue imaginar alguma utilidade para ele? A resposta pode depender da linguagem de programação escolhia.

  9. É possível adicionar todas as variáveis de um programa (mesmo que bastante simples) a um registro?

  10. Implemente um jogo de batalha naval. Use registros para armazenar as informações sobre o tabuleiro e para cada cartela de jogadores.

Próximos Passos

Com a introdução de registros, agora você tem quase todos os recursos básicos providos por linguagens de programação para a resolução de problemas usando computadores. Antes, você era capaz de processar dados de tipos primitivos e coleções de tipos primitivos. Agora, você tornou-se capaz de criar seus próprios tipos de dados. Inclusive, você pode usar registros para criar novos tipos de coleções.

Registros permitem abstrair dados para pensar em problemas e soluções em nível mais alto. Pode-se inclusive agrupar todos os dados de um programa em um único registro, que pode ser pensado como a memória primária do programa. Na prática, contudo, é preferível armazenar apenas variáveis de interesse para o longo prazo do programa. Por exemplo, variáveis locais criadas como contadores em estruturas de repetição ou valores temporários em partes do código não precisam ser armazenadas por todo o programa. Embora seja possível, normalmente não há muitas boas razões para ocupar a memória com variáveis efêmeras (embora existem situações em que isso é válido, como para quando for necessário garantir que o orçamento de memória nunca ultrapasse um teto).

Além disso, após esta introdução sobre registros, a transição para Programação Orientada a Objetos (POO ou OOP, da sigla inglesa) tenderá a ser mais suave. O básico de OOP não é muito diferente do uso de subrotinas com registros, embora forneça recursos adicionais, como modificadores e especificadores de acesso. OOP também fornece recursos mais avançados, como polimorfismo, herança e delegação, que permitem definir hierarquias complexas de classes e (re-)definir métodos de acordo com contextos e particularidades de classes definidas na hierarquia. Linguagens de programação orientadas a objetos são populares, tanto na academia, quanto na indústria e mercado profissional. Além de Python e JavaScript, Java, C++, C#, PHP e Ruby são exemplos linguagens com alta demanda por profissionais.

Antes, contudo, existe uma limitação de todos os programas que você criou até este momento. Todos eles perdem os dados manipulados ao final da execução. Todo programa começa de um mesmo estado inicial (a única exceção são programas usando números pseudoaleatórios com sementes baseadas em tempo). Os dados de execuções prévias são perdidos quando o programa termina.

Com arquivos, você poderá usar memória secundária para armazenar dados para uso em múltiplas execuções de programa. Salvar e carregar dados tornar-se-ão termos em seu vocabulário de programação.

  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