Skip to content
Image with logo, providing a link to the home page
  • 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.

Learn Programming: Records (Structs)

Example of using records in four programming languages: Python, Lua, GDScript and JavaScript.

Image credits: Image created by the author using the program Spectacle.

Requirements

In the introduction to development environments, I have mentioned Python, Lua and JavaScript as good choices of programming languages for beginners. Later, I have commented about GDScript as an option for people who want to program digital games or simulations. For the introductory programming activities, you will need, at least, a development environment configured for one of the previous languages.

If you wish to try programming without configuring an environment, you can use of the online editors that I have created:

However, they do not provide all features offered by interpreters for the languages. Thus, sooner or later, you will need to set up a development environment. If you need to configure one, you can refer to the following resources.

Thus, if you have an Integrated Development Environment (IDE), or a combination of text editor and an interpreter, you are ready to start. The following example assumes that you know how to run code in your chosen language, as presented in the configuration pages.

If you want to use another language, the introduction provides links for configure development environments for the C, C++, Java, LISP, Prolog, and SQL (with SQLite) languages. In many languages, it suffices to follow the models from the experimentation section to modify syntax, commands and functions from the code blocks. C and C++ are exceptions, for they require pointers access the memory.

Data Abstraction and Decomposition

A collection (such as an array or dictionary) allows storing values in a single variable. Some programming languages (especially high-level languages) allow storing values of different types in a same collection variable (for instance, an array). Other languages (especially lower level languages, such as C and C++) permits storing values of a single data type in a variable of a collection.

Something similar happens with functions (or subroutines). Some languages (such as Lua and Python) allow returning multiple values; others (such as JavaScript and GDScript) impose that functions can return a single value.

In the case of subroutines, collections provide a way of returning multiple values. In the case of data structures, techniques such as parallel arrays server as a way to distribute many values for a same entity.

Nevertheless, there is an alternative way to avoid the problem, which applies to subroutines and collections alike. Records (structures or structs) allow grouping multiple data to compose a new composite data type. A variable of the type of a record combines many other variables into a single one.

As subroutines allows performing functional decomposition, records provides a way to practice data decomposition in a program (it can also be thought as a data composition, depending on the desired direction). This allows thinking about the data of a program at a higher level, expanding the concept of data abstraction. For instance, instead of thinking about the data of a person as name, age and gender, one can define a Person data type that is composed by name, age and gender. Every variable created using the type Person will have the pertinent data to define a person in the considered problem or domain.

Records (or Structs) and Classes

Many programming languages provide features that allow a programmer to create her/his own data types. The features, though, can vary.

Imperative languages (procedural or imperative paradigms) tend to allow the definition of records or structures (structs). In general, records allow combining data from preexisting types into a new type. The data of a record are commonly called attributes or fields. The values of the attributes defined the state of the record. This is the simplest and most basic approach of creating composite types.

In languages that allow storing references to subroutines in variables, a record can contain data and subroutines. In languages that do not allow, a record can contain only data. To different both approaches, the term Plain Old Data (POD) or Passive Data Structure (PDS) can be used to refer to records that only contains data. The expression plain old can be understood as "good and old"; that is, the good and old data -- simple, reliable and without extravagances. In other words, without surprises: pure data that is not mixed with code.

Object-oriented languages (Object-Oriented Programming (OOP) paradigm) allow creating classes. A class allows combining attributes with subroutines to process them, called methods. A class tends to be the contrary of POD, because they allow mixing data and code in a single structure, called object. An object is an instance of a class.

Classes can be much more powerful, sophisticated and versatile than PODs (hereafter called records). In classes, techniques such as data hiding (which encompasses the concept of encapsulation), inheritance, polymorphism and delegation can define complex processing. For instance, it is possible to permit or restrict the access to data depending on how the class is defined; in potential, it is possible to restrict data changes to selected methods. Thus, in classes, the data can be mere details. The manipulation of an object can be performed by exclusive means of methods. In other words, it is possible to abstract the data and operate the values using interfaces. In good OOP implementations, a very same interface can be kept, and the inner data definition can be modified. The program will to keep as it nothing had been changed.

In records, that are only data as attributes. Data are variables that can be modified anywhere. One can create subroutines to manipulate the data, though nothing restricts direct manipulation. Attributes in records, are, thus, modifiable anywhere that the variable is accessible. The attributes are only reunited in a record for convenience of use and access. It is a grouping or composition, simple and without surprises.

In pseudocode, a record can be as simple as the following representation:

record name_of_the_record
begin
    attribute1: type1
    attribute2: type2
    // ...
    attributeN: typeN
end

variable record_variable: name_of_the_record
record_variable.attribute1 = ...
record_variable.attribute2 = ...
// ...
record_variable.attributeN = ...

The type of an attribute can be any previously defined type. This means that it can be of a primitive type, a composite type (as the ones studied previously), and, in many program languages, of the type of a previously declared record.

In general, records can be understood as a data type composed of other data types. Programming languages will provide an operator (typically a dot such as ., in which the name of the variable precedes the dot, and the name of the attribute succeeds the dot) to access each defined attribute.

For the example of a type Person, the pseudocode would become:

record Person
begin
    name: string
    age: integer
    gender: string
end

variable franco: Person
franco.name = "Franco"
franco.age = 1234
franco.gender = "Male"

variable you: Person
you.name = "???"
you.age = 4321
you.gender = "???"

Person could have attributes for pronouns, for a more inclusive solution.

In this topic, there is a convention that every name of a type defined with a record will be started by an uppercase letter. This is a usual convention in many programming languages and allows quickly distinguishing the name of a type from an identifier (such as the name of a variable or subroutine).

OOP and Base Use of Objects

Records are simpler; simplicity is convenient for the initial learning. Therefore, even in languages that allow object-oriented programming (such as Python, JavaScript and GDScript), this topic will use classes as if there were records. The choice requires maturity for use and might not be exactly recommended for beginners. In fact, people used to OOP can disagree with the approach or feel uneasy about the (mis)use of objects as records; such reactions are fair and acceptable. However, they would also restrict the potential for learning.

As habitual, my opinion is that programming languages are tools. Programming paradigms are tools as well. Paradigms are like lens that help to understand, model and solve problems. There are not solutions, though they can influence how one will build a solution. The lenses enhance certain details in detriment of others. Computation always has trade-offs.

Object orientation is not a paradigm that is superior to every other one. As any other paradigms, OOP have benefits and limitations. The understanding of records as data can help, in the future, to use classes and objects with greater simplicity and elegance, instead of excessively exploring OOP idioms and patterns when they are not really suitable or necessary.

Thus, the adopted approach in this topic is not a good example of OOP, especially in the initial examples. The choice is intentional, because OOP can be detailed in the future with suitable depth. Instead of object orientation, the paradigms explored in this topic are imperative and procedural.

At this time, the focus are the data stored in composite types. In any programming paradigm, it is fundamental to understand the data flow of a program. The comprehension of a problem and data modeling are part of every solution. Planning and conceiving the data flow of a program is important; for good software architectures, an understanding of the flow contributes to the creation of good abstractions. On the other hand, starting to create abstractions before understanding the problem can lead to architectural problems at the medium or long term. So can creating classes and data types indiscriminately, especially if they are forced for parts of situations on which they are unsuitable or unnecessary.

Data does not require OOP; to process data, one can use all resources previously explored. The chosen approach will allow explaining how to create a simple model of object orientation, with some basic features. To do this, it suffices to use already discussed concepts. From simplicity to complexity, in incremental steps.

Textual Programming Languages: JavaScript, Python, Lua and GDScript

JavaScript, Python and GDScript allow object-oriented programming. Lua does not have a concept for registers nor classes, although it is possible implementing many OOP resources using tables.

class Person {
    constructor() {
        this.name = ""
        this.age = 0
        this.gender = ""
    }
}

// It is also possible to do:
// class Person {
//    name = ""
//    age = 0
//    gender = ""
//}

let franco = new Person()
franco.name = "Franco"
franco.age = 1234
franco.gender = "Male"

console.log(franco)
console.log(franco.name)
console.log(franco.age)
console.log(franco.gender)
// The name could be Person() to become closer to other programming languages.
function new_person() {
    // JavaScript Object.
    return {
        "name": "",
        "age": 0,
        "gender": ""
    }
    // Or:
    // return {
    //     name: "",
    //     age: 0,
    //     gender: ""
    // }
}

let franco = new_person()
franco.name = "Franco"
franco.age = 1234
franco.gender = "Male"

console.log(franco)
console.log(franco.name)
console.log(franco.age)
console.log(franco.gender)
class Person:
    def __init__(self):
        self.name = ""
        self.age = 0
        self.gender = ""

franco = Person()
franco.name = "Franco"
franco.age = 1234
franco.gender = "Male"

print(franco)
print(franco.name)
print(franco.age)
print(franco.gender)
-- The name could be Person() to become closer to other programming languages.
function new_person()
    return {
        name = "",
        age = 0,
        gender = ""
    }
end

local franco = new_person()
franco.name = "Franco"
franco.age = 1234
franco.gender = "Male"

print(franco)
print(franco.name)
print(franco.age)
print(franco.gender)
extends Node

# Inner class.
class Person:
    # If an initial value is omitted, it will be null.
    var name = ""
    var age = 0
    var gender = ""

    # An _init() method is optional (as a contructor).

func _ready():
    var franco = Person.new()
    franco.name = "Franco"
    franco.age = 1234
    franco.gender = "Male"

    print(franco)
    print(franco.name)
    print(franco.age)
    print(franco.gender)
extends Node

# File as class.
# The name of the file is the name of the class.
# It is also possible to define a custom name for the class using:
# class_name Person

# If an initial value is omitted, it will be null.
var name
var age
var gender

func _init():
    name = ""
    age = 0
    gender = ""

func _ready():
    name = "Franco"
    age = 1234
    gender = "Male"

    var franco = self
    printt(self, franco)
    printt(name, self.name, franco.name)
    printt(age, self.age, franco.age)
    printt(gender, self.gender, franco.gender)

Documentation for creating records or classes:

JavaScript, Python and GDScript allow defining classes. In these languages, there is a new reserved word to reference the instance of currently manipulated object itself. It is called this in JavaScript, and self in Python and GDScript (Lua also proposes self as a convention for the variable itself, used when a subroutine is called using the operator :; more details when appropriate). In languages such as GDScript, in cases that are no ambiguities to determine the variable (for instance, there is not a parameter with the same name of an attribute), it is possible to omit self. Otherwise, the use is obligatory to reference the class attribute instead of the other variable.

In JavaScript, the introduction of classes is somewhat recent (ECMAScript 2015). Before 2015, objects in JavaScript were created as JavaScript Objects, previously presented in Collections as one of the alternatives to use of dictionaries. Currently, one can opt between using native classes (defined using class) or JavaScript Objects.

In GDScript, every source code file defines a class. In other words, every program written hitherto in the language defined classes. Variables declared outside a subroutine are class attributes (instead of global variables). All subroutines declared in a file in the language are methods. It is also possible to declare inner classes, which can be used as records. An inner class is a nested class, that is, a class defined inside another class.

In the code snippets that define classes, a special method called constructor performs the allocation and initialization of an object (a variable) of the type of the class. The constructor is called automatically every time a class object is built, to perform the initialization with the defined values. This makes POO objects always start with a valid and known initial state (instead of random, as it can happen with variables of primitive types). In JavaScript, the constructor is called constructor() and it is optional. In Python, it is called __init__(), using two underscores before and after the word, and it is required. Attrinbutes in Python should be declared in the definition of __init()__ (otherwise, they will be shared among all instances of the class, which is known as static attribute). In GDScript, it is called _init() and it is optional.

Furthermore, in code with classes, the language can require using a reserved word to create an object. In JavaScript, it is new(). In Python, one should use parentheses after the name of the class. In GDScript, one should use .new() after the name of the (internal) class.

In Lua, records can be created as tables. The simplest way is declaring a table with some keys of the string type. For convenience, one can define a function to create an empty record. This is the purpose of new_person() (both in Lua and for JavaScript Object). Although optional, such construction simplifies the creation of several records with the same attributes. Instead of duplicating code, one can call the subroutine. This is practical, because it makes it easier to change all variables of a type when the definition of the record is changed (although references to old variables will still require manual fixes).

In Python, Lua and GDScript, an attempt to write a variable of a record type with print() will write the address of the reference. In JavaScript, console.log() will write the values stored in the object.

Passage By Reference or Passage By Value?

The previous paragraphs are almost sufficient to use records (or classes are records) in programming activities. An important additional detail is to know that some programming languages pass records or object to subroutines by value (for instance, C and C++), while others pass by reference (case of JavaScript, Python, Lua and GDScript). As JavaScript, Python, Lua and GDScript pass records or objects by reference, it must be noted that value modifications in parameters of subroutines will affect the original variable passed as the parameter, and these changes will be persistent after the end of the call.

class Person {
    constructor() {
        this.name = ""
        this.age = 0
        this.gender = ""
    }
}

function initialize_person(person) {
    person.name = "Franco"
    person.age = 1234
    person.gender = "Male"
}

let franco = new Person()
console.log(franco)
console.log(franco.name)
console.log(franco.age)
console.log(franco.gender)

initialize_person(franco)
console.log(franco)
console.log(franco.name)
console.log(franco.age)
console.log(franco.gender)
function new_person() {
    // JavaScript Object.
    return {
        "name": "",
        "age": 0,
        "gender": ""
    }
}

function initialize_person(person) {
    person.name = "Franco"
    person.age = 1234
    person.gender = "Male"
}

let franco = new_person()
console.log(franco)
console.log(franco.name)
console.log(franco.age)
console.log(franco.gender)

initialize_person(franco)
console.log(franco)
console.log(franco.name)
console.log(franco.age)
console.log(franco.gender)
class Person:
    def __init__(self):
        self.name = ""
        self.age = 0
        self.gender = ""

def initialize_person(person):
    person.name = "Franco"
    person.age = 1234
    person.gender = "Male"

franco = Person()
print(franco)
print(franco.name)
print(franco.age)
print(franco.gender)

initialize_person(franco)
print(franco)
print(franco.name)
print(franco.age)
print(franco.gender)
function new_person()
    return {
        name = "",
        age = 0,
        gender = ""
    }
end

function initialize_person(person)
    person.name = "Franco"
    person.age = 1234
    person.gender = "Male"
end

local franco = new_person()
print(franco)
print(franco.name)
print(franco.age)
print(franco.gender)

initialize_person(franco)
print(franco)
print(franco.name)
print(franco.age)
print(franco.gender)
extends Node

class Person:
    var name
    var age
    var gender

func initialize_person(person):
    person.name = "Franco"
    person.age = 1234
    person.gender = "Male"

func _ready():
    var franco = Person.new()
    print(franco)
    print(franco.name)
    print(franco.age)
    print(franco.gender)

    initialize_person(franco)
    print(franco)
    print(franco.name)
    print(franco.age)
    print(franco.gender)
extends Node

var name
var age
var gender

func _init():
    name = ""
    age = 0
    gender = ""

func initialize_person():
    name = "Franco"
    age = 1234
    gender = "Male"

func initialize_person_com_parametro(person):
    person.name = "Franco Garcia"
    person.age = 4321
    person.gender = "MASCULINO"

func _ready():
    print(self)
    printt(name, self.name)
    printt(age, self.age)
    printt(gender, self.gender)

    # The call will affect the object itself.
    # In this case, the modification of values is a side effect of the call.
    initialize_person()
    print(self)
    printt(name, self.name)
    printt(age, self.age)
    printt(gender, self.gender)

    # The previous code is equivalent to the use of self as a parameter.
    initialize_person_com_parametro(self)
    print(self)
    printt(name, self.name)
    printt(age, self.age)
    printt(gender, self.gender)

The term intialize is commonly abbreviated as init (or setup) in programming. A second detail worth noticing refers to comparisons in programming languages that treat objects are references.

Comparisons: Equality and Difference

As variables that store values in JavaScript, Python, Lua and GDScript store references, it is important taking care with equality and difference comparisons. They should be performed attribute by attribute, instead of using the operator available in the language. Case an attribute is a reference itself, it should also be compared suitably.

class Person {
    constructor() {
        this.name = ""
        this.age = 0
        this.gender = ""
    }
}

function equal_people(person1, person2) {
    return ((person1.name === person2.name) &&
            (person1.age === person2.age) &&
            (person1.gender === person2.gender))
}

function different_people(person1, person2) {
    return (!equal_people(person1, person2))
}

let person1 = new Person()
let person2 = new Person()
console.log(person1 === person2, equal_people(person1, person2))
console.log(person1 !== person2, different_people(person1, person2))
function new_person() {
    // JavaScript Object.
    return {
        "name": "",
        "age": 0,
        "gender": ""
    }
}

function equal_people(person1, person2) {
    return ((person1.name === person2.name) &&
            (person1.age === person2.age) &&
            (person1.gender === person2.gender))
}

function different_people(person1, person2) {
    return (!equal_people(person1, person2))
}

let person1 = new_person()
let person2 = new_person()
console.log(person1 === person2, equal_people(person1, person2))
console.log(person1 !== person2, different_people(person1, person2))
class Person:
    def __init__(self):
        self.name = ""
        self.age = 0
        self.gender = ""

def equal_people(person1, person2):
    return ((person1.name == person2.name) and
            (person1.age == person2.age) and
            (person1.gender == person2.gender))

def different_people(person1, person2):
    return (not equal_people(person1, person2))

person1 = Person()
person2 = Person()
print(person1 == person2, equal_people(person1, person2))
print(person1 != person2, different_people(person1, person2))
function new_person()
    return {
        name = "",
        age = 0,
        gender = ""
    }
end

function equal_people(person1, person2)
    return ((person1.name == person2.name) and
            (person1.age == person2.age) and
            (person1.gender == person2.gender))
end

function different_people(person1, person2)
    return (not equal_people(person1, person2))
end

local person1 = new_person()
local person2 = new_person()
print(person1 == person2, equal_people(person1, person2))
print(person1 ~= person2, different_people(person1, person2))
extends Node

class Person:
    var name
    var age
    var gender

func equal_people(person1, person2):
    return ((person1.name == person2.name) and
            (person1.age == person2.age) and
            (person1.gender == person2.gender))

func different_people(person1, person2):
    return (not equal_people(person1, person2))

func _ready():
    var person1 = Person.new()
    var person2 = Person.new()
    printt(person1 == person2, equal_people(person1, person2))
    printt(person1 != person2, different_people(person1, person2))
extends Node

class_name Person

var name
var age
var gender

func _init():
    name = ""
    age = 0
    gender = ""

func equal_people(person):
    return ((self.name == person.name) and
            (self.age == person.age) and
            (self.gender == person.gender))

func different_people(person):
    return (not equal_people(person))

func _ready():
    var person2 = get_script().new()
    printt(self == person2, equal_people(person2))
    printt(self != person2, different_people(person2))

    # It is not possible to use class_name for this case.
    # var person3 = Person.new()
    # printt(self == person3, equal_people(person3))
    # printt(self != person3, different_people(person3))

The potentially most confusing version is the one in GDScript using the own file as a class. The method get_script() documentation allows referencing the source code file itself, used to instance a new variable.

In all previous cases, the comparison using operators returns the incorrect value, because it compares addresses. Both variables should be equal, because both were initialized with the same values (empty strings for name and gender, and 0 for age). The comparison using subroutines returns the correct value. As the operators for equality and difference are the reverse of each other, one can define one of them. Next, she/he can define the second as the negation of the first.

To sort values in arrays of records, it can also be interesting to define subroutines that inform whether a value is less than another (for increasing order), or larger than the other (for decreasing order). For instance, one sorting criterion for a variable of the type Person could be alphabetic order for the name. Another criteria could be age or gender. It is also possible to define priorities to the sorting (for instance, first by name, then age, then gender). To do this, it suffices to perform the next comparison with the immediately less important criterion if the previously compared values are equal.

More Advanced Resources

This section presents some more advanced features to use with records and/or classes. They are more complex, though they provide conveniences and practicality to programming activities. Furthermore, some features will be not be available in some programming language. If the subsections seem too complex, you can skip to the section Techniques Using Records.

Operator Overloading

Languages that provide resources for operator overloading make it possible to redefine the behavior of operators for records, potentially making the code easier to read. Operator overloading has been previously mentioned (for instance, in Relational Operations and Comparisons).

From the languages that are being considered from examples, Python and Lua allow overloading arithmetic, logic and relational operators. The next example overloads the equality operator and the difference operator for the record Person.

class Person:
    def __init__(self):
        self.name = ""
        self.age = 0
        self.gender = ""

    # It is important noticing the indentation.
    # The subroutine (method) is defined inside the class, in the same level of
    # __init__().
    def __eq__(self, person):
        if (type(self) != type(person)):
            return false

        return ((self.name == person.name) and
                (self.age == person.age) and
                (self.gender == person.gender))

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

person1 = Person()
person2 = Person()
print(person1 == person2)
print(person1 != person2)
function equal_people(person1, person2)
    if (type(person1) ~= type(person2)) then
        return nil
    end

    return ((person1.name == person2.name) and
            (person1.age == person2.age) and
            (person1.gender == person2.gender))
end

function new_person()
    local data = {
        name = "",
        age = 0,
        gender = ""
    }
    local metatable = {
        __eq = equal_people
    }

    local person = setmetatable(data, metatable)

    return person
end

local person1 = new_person()
local person2 = new_person()
print(person1 == person2)
print(person1 ~= person2)

Python allows overloading all operators described in the documentation. In the case of the operators for equality and difference, if one defines only the equality operator, Python uses its negation to create the operator of difference automatically (or vice-versa).

Lua uses metatable for operator overloading. The concept has been previously mentioned (for instance, in Collections). Now it is a good time to explain it. Lua uses metatable as a way to allow the redefinition of the language's operators (and some operations). To do this, one should define a table with keys and functions for the desired operations, and associate them to another table (the one that will receive the operations) using setmetatable() (documentation).

Available operators and operations vary according the language's version. More up-to-date versions have more operators. For instance, documentation for version 5.1 and documentation for version 5.4. In the case of the operator for equality and difference, Lua requires only the implementation of the equality operator. The difference operator is created automatically as the negation of the equality one. Furthermore, versions older than 5.3 may require a same reference to the function. For instance, the next code snippet work correctly in Lua's current version (5.4), though it does not work until the version 5.2. Up to the version 5.2, as the defined anonymous would have different addresses, the overloaded operator would not be called.

-- Requires Lua 5.3 or more recent.
function new_person()
    local data = {
        name = "",
        age = 0,
        gender = ""
    }
    local metatable = {
        __eq = function(person1, person2)
            if (type(person1) ~= type(person2)) then
                return nil
            end

            return ((person1.name == person2.name) and
                    (person1.age == person2.age) and
                    (person1.gender == person2.gender))
        end
    }

    local person = setmetatable(data, metatable)
    -- The addresses of __eq will be different.
    -- print(metatable, getmetatable(person), getmetatable(person).__eq)

    return person
end

local person1 = new_person()
local person2 = new_person()
print(person1 == person2)
print(person1 ~= person2)

The commented line in both Lua examples allow to find the address of the created __eq() function.

A possible alternative is declaring the metatable globally or local to the file and use it for all created instances (as shared memory). As all variables created in new_person() will have the same metatable called person_metatable, the defined addresses will be the same. Therefore, the following code snippet is valid in versions older than Lua 5.3 (and also on newer versions). A second advantage of the approach is that is saves memory.

local person_metatable = {
    __eq = function(person1, person2)
        if (type(person1) ~= type(person2)) then
            return nil
        end

        return ((person1.name == person2.name) and
                (person1.age == person2.age) and
                (person1.gender == person2.gender))
    end
}

function new_person()
    local data = {
        name = "",
        age = 0,
        gender = ""
    }

    local person = setmetatable(data, person_metatable)

    return person
end

local person1 = new_person()
local person2 = new_person()
print(person1 == person2)
print(person1 ~= person2)

Although some programming languages do not provide features for operator overloading, they can be useful when available. In particular, they can make reading code simpler. For instance, instead of adding two matrices with sum_matrices(x, y), the operator + could be overloaded to make it possible writing x + y as the sum of two matrices. The advantage becomes clearer when one considers x + y + z, that would be written as sum_matrices(sum_matrices(x, y), z) without operator overloading. The first version is more immediate (although it can hide the computational cost if one does not remember that is a custom operation instead of a conventional sum). For instance, overloading the operator / to perform the sum of matrices would be confusing (although possible).

Polymorphism

OOP defines a concept called polymorphism, which also has been previously mentioned (for instance, in Sub Subroutines (Functions and Procedures). With classes and inheritance in OOP, one can make children classes (or derived classes) able to redefine methods of their respective parent classes (or superclasses).

As the current goal is not to introduce OOP in detail, the following code snippets provide fast examples of how to use polymorphism with composite types to redefine the operator to convert data of a record (class) into strings.

class Person {
    constructor() {
        this.name = ""
        this.age = 0
        this.gender = ""
    }

    toString() {
        let result = this.name + " (" + this.age + " years old, gender " + this.gender + ")"

        return result
    }
}

let franco = new Person()
franco.name = "Franco"
franco.age = 1234
franco.gender = "Male"

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

let text = "Hello, " + franco + "!"
console.log(text)
function Person() {
    this.name = ""
    this.age = 0
    this.gender = ""
}

Person.prototype.toString = function() {
    let result = this.name + " (" + this.age + " years old, gender " + this.gender + ")"

    return result
}

let franco = new Person()
franco.name = "Franco"
franco.age = 1234
franco.gender = "Male"

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

let text = "Hello, " + franco + "!"
console.log(text)
class Person:
    def __init__(self):
        self.name = ""
        self.age = 0
        self.gender = ""

    def __str__(self):
        result = self.name + " (" + str(self.age) + " years old, gender " + self.gender + ")"

        return result

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

        return ((self.name == person.name) and
                (self.age == person.age) and
                (self.gender == person.gender))

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

franco = Person()
franco.name = "Franco"
franco.age = 1234
franco.gender = "Male"

print(franco)

text = "Hello, " + str(franco) + "!"
print(text)
function equal_people(person1, person2)
    if (type(person1) ~= type(person2)) then
        return nil
    end

    return ((person1.name == person2.name) and
            (person1.age == person2.age) and
            (person1.gender == person2.gender))
end

function person_to_string(person)
    local result = person.name .. " (" .. person.age .. " years old, gender " .. person.gender .. ")"

    return result
end

function concatenate(x, y)
    local result = tostring(x) .. tostring(y)
    return result
end

local person_metatable = {
    __eq = equal_people,
    __tostring = person_to_string,
    __concat = concatenate
}

function new_person()
    local data = {
        name = "",
        age = 0,
        gender = ""
    }

    local person = setmetatable(data, person_metatable)

    return person
end

local franco = new_person()
franco.name = "Franco"
franco.age = 1234
franco.gender = "Male"

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

text = "Hello, " .. franco .. "!"
print(text)
extends Node

class Person:
    var name
    var age
    var gender

    func _to_string():
        var result = self.name + " (" + str(self.age) + " years old, gender " + self.gender + ")"

        return result

func _ready():
    var franco = Person.new()
    franco.name = "Franco"
    franco.age = 1234
    franco.gender = "Male"

    print(franco)

    var text = "Hello, " + str(franco) + "!"
    print(text)
extends Node

var name
var age
var gender

func _init():
    name = ""
    age = 0
    gender = ""

func _ready():
    name = "Franco"
    age = 1234
    gender = "Male"

    var franco = self
    printt(self, franco)

    var text = "Hello, " + str(franco) + "!"
    print(text)

    text = "Hello, " + str(self) + "!"
    print(text)

func _to_string():
    var result = self.name + " (" + str(self.age) + " years old, gender " + self.gender + ")"

    return result

In JavaScript, the method toString() (documentation) must be implemented to enable automatic conversion of classes to strings. In Python, one must implement __str__() (documentation). In Lua, one should define __tostring and __concat for the metatable (__tostring: documentation; __concat: documentation for version 5.1 and documentation for version 5.4). In GDScript, one must implement _to_string() (documentation).

The use of polymorphism to implement a subroutine for conversion of a record (or object) to string makes implicit (or explicit) conversions to string easier. A second advantage is that it normally also allow using the standard common or subroutine to print data, such as print(), to write the contents objects.

Constructors

A constructor is a subroutine that allocates memory and initialize initial values for an OOP object. For records, a register can be a simple function. The function new_person(), for instance, initialize the attributes of a records with values considered zero (zero for numbers, False for logic values, empty string for text). With some parameters, it could initialize the newly created variable with values provided by the call.

class Person {
    constructor(name = "", age = 0, gender = "") {
        this.name = name
        this.age = age
        this.gender = gender
    }

    toString() {
        let result = this.name + " (" + this.age + " years old, gender " + this.gender + ")"

        return result
    }
}

let franco = new Person("Franco", 1234, "Male")
console.log(franco)
console.log("" + franco)

let text = "Hello, " + franco + "!"
console.log(text)
function Person(name = "", age = 0, gender = "") {
    this.name = name
    this.age = age
    this.gender = gender
}

Person.prototype.toString = function() {
    let result = this.name + " (" + this.age + " years old, gender " + this.gender + ")"

    return result
}

let franco = new Person("Franco", 1234, "Male")
console.log(franco)
console.log("" + franco)

let text = "Hello, " + franco + "!"
console.log(text)
class Person:
    def __init__(self, name = "", age = 0, gender = ""):
        self.name = name
        self.age = age
        self.gender = gender

    def __str__(self):
        result = self.name + " (" + str(self.age) + " years old, gender " + self.gender + ")"

        return result

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

        return ((self.name == person.name) and
                (self.age == person.age) and
                (self.gender == person.gender))

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

franco = Person("Franco", 1234, "Male")
print(franco)

text = "Hello, " + str(franco) + "!"
print(text)
function equal_people(person1, person2)
    if (type(person1) ~= type(person2)) then
        return nil
    end

    return ((person1.name == person2.name) and
            (person1.age == person2.age) and
            (person1.gender == person2.gender))
end

function person_to_string(person)
    local result = person.name .. " (" .. person.age .. " years old, gender " .. person.gender .. ")"

    return result
end

function concatenate(x, y)
    local result = tostring(x) .. tostring(y)
    return result
end

local person_metatable = {
    __eq = equal_people,
    __tostring = person_to_string,
    __concat = concatenate
}

function new_person(name, age, gender)
    local data = {
        name = name or "",
        age = age or 0,
        gender = gender or ""
    }

    local person = setmetatable(data, person_metatable)

    return person
end

local franco = new_person("Franco", 1234, "Male")
print(franco)
print(tostring(franco))
print("" .. franco)

text = "Hello, " .. franco .. "!"
print(text)
extends Node

class Person:
    var name
    var age
    var gender

    func _init(name = "", age = 0, gender = ""):
        self.name = name
        self.age = age
        self.gender = gender

    func _to_string():
        var result = self.name + " (" + str(self.age) + " years old, gender " + self.gender + ")"

        return result

func _ready():
    var franco = Person.new("Franco", 1234, "Male")
    print(franco)

    var text = "Hello, " + str(franco) + "!"
    print(text)
extends Node

var name
var age
var gender

func _init(name = "", age = 0, gender = ""):
    self.name = name
    self.age = age
    self.gender = gender

func _ready():
    name = "Franco"
    age = 1234
    gender = "Male"

    var franco = self
    printt(self, franco)

    var text = "Hello, " + str(franco) + "!"
    print(text)

    text = "Hello, " + str(self) + "!"
    print(text)

func _to_string():
    var result = self.name + " (" + str(self.age) + " years old, gender " + self.gender + ")"

    return result

In many programming languages, the name of the parameter in the constructor must not be necessarily equal to the name of the attribute. They can be different; furthermore, the parameters from the signature can be different from the declared attributes. However, it is very common to the same name for parameter and variable, when it represents a direct assignment. Indeed, when all parameters are directly assigned to attributes without any processing (such as validation), one should consider whether the use of a simple record could be more appropriate than using a class.

Regardless, the use of a same name results in ambiguities, because there are two different variables in a same scope with identical names. In the previous examples, the use of this or self remove ambiguities in such cases. The use self or this explicitly reference the value stored in the class on in the record, instead of the local variable or parameter (which is references without the use of the reserved word).

Furthermore, the constructor can have additional code for data validation (instead of simple value assignment), if needed. Additionally, in programming languages that allow defining default values for parameters, they can be used to create optional fields in constructors.

For instance, in JavaScript:

  • new Person() creates a variable with name "", age 0 and gender "".
  • new Person("Franco") creates a variable with name "Franco", age 0 and gender "".
  • new Person("Franco", 1234) creates a variable with name "Franco", age 1234 and gender "".
  • new Person("Franco", 1234, "Male") creates a variable with name "Franco", age 1234 and gender "Male".

It should be noted that is not possible to omit intermediate parameters. For instance, new Person("Franco", "Male") would assign "Male" to age. As JavaScript does not perform type verification, the assignment would be valid. A way to avoid the problem could be using assertions, which will be commented in a subsection.

Warning. In Python, the initialization of a default parameter cannot be a reference type; every reference type must be initialized for every object. Otherwise, the data may be shared. Thus, unless the value is of a primitive type, it is better to use None as the default value, then set the desired value (for instance, an array, dictionary or object) in the constructor.

Subroutines as Constructors

It can be worth noticing that, if one renamed new_person() to Person(), the result in Lua would become very similar to constructors in programming languages that supports classes.

-- Remainder of the implementation...

function Person(name, age, gender)
    local data = {
        name = name or "",
        age = age or 0,
        gender = gender or ""
    }

    local person = setmetatable(data, person_metatable)

    return person
end

local franco = Person("Franco", 1234, "Male")

The result is a subroutine to create a record similarly to how it is done in Python and JavaScript. Another possibility could be incorporating a subroutine for building variables (for instance, new()) in a table called Person.

-- Remainder of the implementation...

local Person = {
    new = function(name, age, gender)
      local data = {
          name = name or "",
          age = age or 0,
          gender = gender or "",
      }

      local person = setmetatable(data, person_metatable)

      return person
    end
}

local franco = Person.new("Franco", 1234, "Male")

In the second case, the result is similar to how GDScript instances objects: Person.new().

Thus, the knowledge of programming fundamentals and techniques allow using existing features in a language as ways to incorporate some existing features from other languages. In a certain way, it is similar to creating your own custom language (or custom dialect) using the original language as the source material. Although the result might not be idiomatic, it is convenient and powerful incorporating resources that make you more efficient at using the language.

As usual, programming languages are tools. In potential, they are tools that can create new tools. Meta-tools, one could say. You can adapt them according to own your needs, to make them better for you.

Named Parameters

Although it is not possible in the traditional way (using positional parameters), there are ways to omit intermediate parameters. The first is more restrict, although the better option. Some programming languages (such as Python) provide a feature called named parameter. For instance, in Python:

  • Person() creates a variable with name "", age 0 and gender "".
  • Person("Franco") creates a variable with name "Franco", age 0 and gender "".
  • Person("Franco", 1234) creates a variable with name "Franco", age 1234 and gender "".
  • Person("Franco", 1234, "Male") creates a variable with name "Franco", age 1234 and gender "Male".
  • Person(name = "Franco") creates a variable with name "Franco", age 0 and gender "".
  • Person(name = "Franco", age = 1234) creates a variable with name "Franco", age 1234 and gender "".
  • Person(name = "Franco", gender = "Male") creates a variable with name "Franco", age 0 and gender "Male".

With named parameters, the order and number of parameters does not matter.

class Person:
    def __init__(self, name = "", age = 0, gender = ""):
        self.name = name
        self.age = age
        self.gender = gender

    def __str__(self):
        result = self.name + " (" + str(self.age) + " years old, gender " + self.gender + ")"

        return result

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

        return ((self.name == person.name) and
                (self.age == person.age) and
                (self.gender == person.gender))

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

print(Person())
print(Person("Franco"))
print(Person("Franco", 1234))
print(Person("Franco", 1234, "Male"))
print(Person(name = "Franco"))
# Examples with out of order parameters.
print(Person(age = 1234, name = "Franco"))
print(Person(gender = "Male", age = 1234))
print(Person(gender = "Male", name = "Franco", age = 1234))

All calls to the Person() constructor in the previous example are valid and initialize a new object with the chosen values for each named parameter.

In languages without named parameters, it is possible to use a dictionary as a parameter to simulate the technique. As the order of keys in a dictionary is irrelevant, the passage can be performed in any order. Similarly, if ones does not define a key, a default value can be used to initialize the ignored value.

In particular, this technique is common in JavaScript and has a feature which makes it easier to use, called destructuring assignment (documentation). Destructuring assignment allows writing:

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

Instead of:

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

Thus, the version in JavaScript will be more legible than the one defined for Lua and GDScript. Lua and GDScript requires the usual forms of accessing values in dictionaries. As Python provides the feature of named parameters, the implementation was omitted for the language.

class Person {
    constructor({name, age, gender} = {name: "", age: 0, gender: ""}) {
        // If name is undefined or null, initialized with the default value.
        this.name = (name) ? name : ""
        this.age = (age) ? age : 0
        this.gender = (gender) ? gender : ""
    }

    toString() {
        let result = this.name + " (" + this.age + " years old, gender " + this.gender + ")"

        return result
    }
}

let franco = new Person({
    name: "Franco",
    age: 1234,
    gender: "Male"
})
console.log(franco)
console.log("" + franco)

let text = "Hello, " + franco + "!"
console.log(text)
function Person({name, age, gender} = {name: "", age: 0, gender: ""}) {
    // If name is undefined or null, initialized with the default value.
    this.name = (name) ? name : ""
    this.age = (age) ? age : 0
    this.gender = (gender) ? gender : ""
}

Person.prototype.toString = function() {
    let result = this.name + " (" + this.age + " years old, gender " + this.gender + ")"

    return result
}

let franco = new Person({
    name: "Franco",
    age: 1234,
    gender: "Male"
})
console.log(franco)
console.log("" + franco)

let text = "Hello, " + franco + "!"
console.log(text)
function equal_people(person1, person2)
    if (type(person1) ~= type(person2)) then
        return nil
    end

    return ((person1.name == person2.name) and
            (person1.age == person2.age) and
            (person1.gender == person2.gender))
end

function person_to_string(person)
    local result = person.name .. " (" .. person.age .. " years old, gender " .. person.gender .. ")"

    return result
end

function concatenate(x, y)
    local result = tostring(x) .. tostring(y)
    return result
end

local person_metatable = {
    __eq = equal_people,
    __tostring = person_to_string,
    __concat = concatenate
}

function new_person(data)
    data = data or {}
    local data_person = {
        name = data.name or "",
        age = data.age or 0,
        gender = data.gender or ""
    }

    local person = setmetatable(data_person, person_metatable)

    return person
end

local franco = new_person({
    name = "Franco",
    age = 1234,
    gender = "Male"
})
print(franco)
print(tostring(franco))
print("" .. franco)

text = "Hello, " .. franco .. "!"
print(text)
extends Node

class Person:
    var name
    var age
    var gender

    func _init(data = {}):
        self.name = data.get("name", "")
        self.age = data.get("age", 0)
        self.gender = data.get("gender", "")

    func _to_string():
        var result = self.name + " (" + str(self.age) + " years old, gender " + self.gender + ")"

        return result

func _ready():
    var franco = Person.new({
        name = "Franco",
        age = 1234,
        gender = "Male"
    })
    print(franco)

    var text = "Hello, " + str(franco) + "!"
    print(text)
extends Node

var name
var age
var gender

func _init(data = {}):
    self.name = data.get("name", "")
    self.age = data.get("age", 0)
    self.gender = data.get("gender", "")

func _ready():
    _init({
        name = "Franco",
        age = 1234,
        gender = "Male"
    })

    var franco = self
    printt(self, franco)

    var text = "Hello, " + str(franco) + "!"
    print(text)

    text = "Hello, " + str(self) + "!"
    print(text)

func _to_string():
    var result = self.name + " (" + str(self.age) + " years old, gender " + self.gender + ")"

    return result

In GDScript using the file as a class, the example is strange. A better alternative would be exporting variables for visual editing using the editor, with the reserved word export before the declaration of each variable (documentation; the development version also has a tutorial with the new features that will be part of the version 4 of Godot Engine). As the editor has not yet been detailed, this information is a curiosity for the time.

Except in JavaScript, that enables the initialization using destructuring assignment, I particularly would not recommend using the technique. The reason is that it hides the expected parameters from the subroutine definition, requiring documentation of the use of a default value as an example.

Methods

Subroutines can be functions, procedures or methods. Functions and procedures are subroutines that are independent. In OOP, methods are functions or subroutines that are part of classes, which make them able to access and modify the state (the attributes) of the class.

A way of thinking about the method is like a function that receives a variable with the type of the class as the first parameter. This parameter can be called, for instance, this or self. In some programming languages, this is exactly what happens.

const MINIMUM_CIVIL_AGE_OF_MAJORITY = 18

class Person {
    constructor(name = "", age = 0, gender = "") {
        this.name = name
        this.age = age
        this.gender = gender
    }

    has_civil_majority() {
        return (this.age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)
    }
}

let franco = new Person("Franco", 1234, "Male")
console.log(franco.has_civil_majority())
const MINIMUM_CIVIL_AGE_OF_MAJORITY = 18

function has_civil_majority(person) {
    if (!person.age) {
        return undefined
    }

    return (person.age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)
}

function new_person(name = "", age = 0, gender = "") {
    return {
        "name": name,
        "age": age,
        "gender": gender,
        "has_civil_majority": has_civil_majority
    }
}

let franco = new_person("Franco", 1234, "Male")
console.log(franco.has_civil_majority(franco))
from typing import Final

MINIMUM_CIVIL_AGE_OF_MAJORITY: Final = 18

class Person:
    def __init__(self, name = "", age = 0, gender = ""):
        self.name = name
        self.age = age
        self.gender = gender

    def has_civil_majority(self):
        return (self.age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)

franco = Person("Franco", 1234, "Male")
print(franco.has_civil_majority())
local MINIMUM_CIVIL_AGE_OF_MAJORITY <const> = 18

function has_civil_majority(person)
    if (person.age == nil) then
        return false
    end

    return (person.age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)
end

function new_person(name, age, gender)
    local person = {
        name = name or "",
        age = age or 0,
        gender = gender or "",
        -- Key               Function reference
        has_civil_majority = has_civil_majority
    }

    return person
end

local franco = new_person("Franco", 1234, "Male")
print(franco.has_civil_majority(franco))
-- The operator : pass the variable itself as the first parameter to the called method.
print(franco:has_civil_majority())
extends Node

const MINIMUM_CIVIL_AGE_OF_MAJORITY = 18

class Person:
    var name
    var age
    var gender

    func _init(name = "", age = 0, gender = ""):
        self.name = name
        self.age = age
        self.gender = gender

    func has_civil_majority():
        # Or return (self.age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)
        return (age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)

func _ready():
    var franco = Person.new("Franco", 1234, "Male")
    print(franco.has_civil_majority())
extends Node

const MINIMUM_CIVIL_AGE_OF_MAJORITY = 18

var name
var age
var gender

func _init(name = "", age = 0, gender = ""):
    self.name = name
    self.age = age
    self.gender = gender

func _ready():
    name = "Franco"
    age = 1234
    gender = "Male"

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

func has_civil_majority():
    # Ou: return (self.age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)
    return (age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)

In the examples, languages that support classes (and using class, in the case of JavaScript) allow defining subroutines inside the definition of the class. This has previously been done for polymorphism and operator overloading. The inclusion in a class of an arbitrary subroutine defined by a programmer defines a method.

In the example for JavaScript Object, the call must include the own variable: franco.has_civil_majority(franco). The same happens for one of the examples in Lua.

For ease of use, Lua defines the operator : for use with table. The operator pass the variable itself as the first parameter of the called subroutine. This is the reason why subroutines such as table.insert(my_table, value) can be called as my_table:insert(value). The creation of a table in Lua includes insert() and other subroutines for manipulating tables as attributes (they are variables that stores a generic function by reference). The same happens for subroutines for string in Lua.

If one wishes to create a similar implementation available in Lua for Java, a possibility would be defining an anonymous function (lambda) that called the original function.

const MINIMUM_CIVIL_AGE_OF_MAJORITY = 18

function has_civil_majority(person) {
    if (!person.age) {
        return undefined
    }

    return (person.age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)
}

function new_person(name = "", age = 0, gender = "") {
    let person = {
        "name": name,
        "age": age,
        "gender": gender
    }
    person["has_civil_majority"] = function() {
        return has_civil_majority(person)
    }

    return person
}

let franco = new_person("Franco", 1234, "Male")
console.log(franco.has_civil_majority())

With the modification, a call to franco.has_civil_majority() would work with a JavaScript Object. JavaScript define closures (documentation) for subroutines, allowing using the local variable person defined in new_person() in the created anonymous function.

However, this is not possible in every programming language. To reproduce the same technique in other programming languages, the language must provide anonymous functions, and either closures or variable captures.

Object-Oriented Programming (OOP) in Lua

With the current information, it is possible to start prototyping simple object orientation systems in Lua. This section is mostly for trivia at this time; it is not required to understand it to follow the remainder of this topic.

With metatable, one can make the Lua's code snippet closer to OOP code. The approach is detailed in Lua's Programming in Lua (Chapter 16) and (Section 16.1). The following code snippet is an adaptation of the approach.

local MINIMUM_CIVIL_AGE_OF_MAJORITY = 18

-- Person serves as the type and the metatable for the type.
local Person = {
    name = "",
    age = 0,
    gender = ""
}

function Person:has_civil_majority()
    -- self is the implicit parameter provided by the : operator.
    if (self.age == nil) then
        return false
    end

    return (self.age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)
end

-- This is the constructor.
function Person:new(name, age, gender)
    local result = {}
    -- self acts as the metatable for Person.
    setmetatable(result, self)
    self.__index = self

    result.name = name or self.name
    result.age = age or self.age
    result.gender = gender or self.gender

    return result
end

-- NOTE The creation must use : operator instead of the .
local franco = Person:new("Franco", 1234, "Male")
print(franco.name, franco.age, franco.gender)

local you = Person:new("You", 4321, "???")
print(you.name, you.age, you.gender)

-- All these calls access data from franco.
print(franco.name, franco.age, franco.gender)
print(franco.has_civil_majority(franco))
print(franco:has_civil_majority())

The definition of a subroutine using : (as in Person:has_civil_majority() and Person:new()) adds an implicit self parameter to the signature. Thus, for instance, Person:new() is equivalent to Person:new(self).

The metatable uses Person as a class definition. The set-up of __index modifies the behavior of table indexing. It makes it possible to use the subroutines and attributes of the Person in the new variable (object) created by Person.new(). As name, age and gender are attributes of primitive types, the result are independent copies. However, if one attribute was a table (for instance, an array or dictionary), the variable would be shared, requiring a deep copy instead.

The following example highlights alternatives to define methods in Lua. In particular, one construction should be avoided: ClassName.method_name(), because it uses the ClassName metatable instead of the variable created with ClassName:new(). The other constructions are equivalent.

local MINIMUM_CIVIL_AGE_OF_MAJORITY = 18

-- Person serves as the type and the metatable for the type.
local Person = {
    name = "",
    age = 0,
    gender = "",
    -- This is a virtual method.
    has_civil_majority = function(self)
                             if (self.age == nil) then
                                 return false
                             end

                             return (self.age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)
                         end
}

-- This is a virtual method.
Person.has_civil_majority_alternative_1 = function(self)
    if (self.age == nil) then
        return false
    end

    return (self.age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)
end

-- This is a virtual method.
function Person:has_civil_majority_alternative_2()
    -- self is the implicit parameter provided by the : operator.
    if (self.age == nil) then
        return false
    end

    return (self.age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)
end

-- This is a base class method.
-- This one will have a different result.
-- In OOP, it would access the data from the parent class (superclass).
function Person.has_civil_majority_alternative_3()
    -- Person is the local variable defined previously.
    if (Person.age == nil) then
        return false
    end

    -- This means that age is 0.
    -- print(Person.age)

    return (Person.age >= MINIMUM_CIVIL_AGE_OF_MAJORITY)
end

-- This is the constructor.
function Person:new(name, age, gender)
    local result = {}
    -- self acts as the metatable for Person.
    setmetatable(result, self)
    self.__index = self

    result.name = name or self.name
    result.age = age or self.age
    result.gender = gender or self.gender

    return result
end

-- NOTE The creation must use : operator instead of the .
local franco = Person:new("Franco", 1234, "Male")
print(franco.name, franco.age, franco.gender)

local you = Person:new("You", 4321, "???")
print(you.name, you.age, you.gender)

-- All these calls access data from franco.
print(franco.name, franco.age, franco.gender)
print(franco.has_civil_majority(franco))
print(franco:has_civil_majority())
print(franco.has_civil_majority_alternative_1(franco))
print(franco:has_civil_majority_alternative_1())
print(franco.has_civil_majority_alternative_2(franco))
print(franco:has_civil_majority_alternative_2())

-- These do not make the call using franco, they make the call using
-- the values of the Person metatable.
print(franco.has_civil_majority_alternative_3())
print(franco:has_civil_majority_alternative_3())
print(Person.has_civil_majority(Person))
print(Person:has_civil_majority())
print(Person.has_civil_majority_alternative_1(Person))
print(Person:has_civil_majority_alternative_1())
print(Person.has_civil_majority_alternative_2(Person))
print(Person:has_civil_majority_alternative_2())
print(Person.has_civil_majority_alternative_3())
print(Person:has_civil_majority_alternative_3())

For a list of OOP libraries for Lua, you can refer to this entry of lua-users.org.

Techniques Using Records

This section returns to the basics of registers as groupings of data. Additional features such as polymorphism and operator overloading are not necessary to understand the basics. Constructors with or without parameters can be used as simple subroutines.

To focus on data instead of OOP, the version of the implementation using JavaScript will use class and the version in GDScript will use inner classes. You can opt to define subroutines as method, if you prefer. The examples will implement subroutines as independent functions and procedures.

Array of Records (Array of Structures)

A way of relating data in different arrays consists of using the technique of parallel arrays (structures of arrays). Indeed, now it is possible to explore the structure part of the name. However, the most common technique is to define an array of records (array of structures), especially in OOP.

An array of structures combines all data that must be represented about an entity in a record. Instead of sharing data using an index, all data of the entity will be available in the defined record.

For instance, to store the name, the extension and an example for an arbitrary programming languages, one could define three parallel arrays: languages_name, languages_extension e languages_example. In this case, languages_ was adopted as the prefix for the arrays.

With an array of structures, one can define a single array that stores all data into a single record (for instance, Language or ProgrammingLanguage) with three fields: name, extension and example. Each position of the array will store one record. All data will be grouped together in the record.

class ProgrammingLanguage {
    constructor(name = "", extension = "", example = "") {
        this.name = name
        this.extension = extension
        this.example = example
    }
}

let languages = [
    new ProgrammingLanguage("JavaScript", ".js", "console.log(\"Hello, my name is Franco!\")"),
    new ProgrammingLanguage("Python", ".py", "print(\"Hello, my name is Franco!\")"),
    new ProgrammingLanguage("Lua", ".lua", "print(\"Hello, my name is Franco!\")"),
    new ProgrammingLanguage("GDScript", ".gd", "extends Node\nfunc _ready():\n    print(\"Hello, my name is Franco!\")"),
]

for (let language of languages) {
    console.log("Programming Language: " + language.name)
    console.log("Extension: " + language.extension)
    console.log("Example:")
    console.log(language.example)
    console.log("---")
}
class ProgrammingLanguage:
    def __init__(self, name = "", extension = "", example = ""):
        self.name = name
        self.extension = extension
        self.example = example

languages = [
    ProgrammingLanguage("JavaScript", ".js", "console.log(\"Hello, my name is Franco!\")"),
    ProgrammingLanguage("Python", ".py", "print(\"Hello, my name is Franco!\")"),
    ProgrammingLanguage("Lua", ".lua", "print(\"Hello, my name is Franco!\")"),
    ProgrammingLanguage("GDScript", ".gd", "extends Node\nfunc _ready():\n    print(\"Hello, my name is Franco!\")"),
]

for language in languages:
    print("Programming Language: " + language.name)
    print("Extension: " + language.extension)
    print("Example:")
    print(language.example)
    print("---")
function new_programming_language(name, extension, example)
    local result = {
        name = name or "",
        extension = extension or "",
        example = example or ""
    }

    return result
end

local languages = {
    new_programming_language("JavaScript", ".js", "console.log(\"Hello, my name is Franco!\")"),
    new_programming_language("Python", ".py", "print(\"Hello, my name is Franco!\")"),
    new_programming_language("Lua", ".lua", "print(\"Hello, my name is Franco!\")"),
    new_programming_language("GDScript", ".gd", "extends Node\nfunc _ready():\n    print(\"Hello, my name is Franco!\")"),
}

for _, language in ipairs(languages) do
    print("Programming Language: " .. language.name)
    print("Extension: " .. language.extension)
    print("Example:")
    print(language.example)
    print("---")
end
extends Node

class ProgrammingLanguage:
    var name
    var extension
    var example

    func _init(name = "", extension = "", example = ""):
        self.name = name
        self.extension = extension
        self.example = example

func _ready():
    var languages = [
        ProgrammingLanguage.new("JavaScript", ".js", "console.log(\"Hello, my name is Franco!\")"),
        ProgrammingLanguage.new("Python", ".py", "print(\"Hello, my name is Franco!\")"),
        ProgrammingLanguage.new("Lua", ".lua", "print(\"Hello, my name is Franco!\")"),
        ProgrammingLanguage.new("GDScript", ".gd", "extends Node\nfunc _ready():\n    print(\"Hello, my name is Franco!\")"),
    ]

    for language in languages:
        print("Programming Language: " + language.name)
        print("Extension: " + language.extension)
        print("Example:")
        print(language.example)
        print("---")

Arrays of structures are common in object-oriented programming. For programming beginners, they provide a good approach to organize and model solutions for problems.

Record of Arrays (Structure of Arrays) or Parallel Arrays in a Record

For a proper structure of arrays, one can modify the version using parallel arrays. All arrays representing all entities will be stored in a single record; each entity is accessed by its shared index among the different arrays. In other words, the approach is the contrary of the data abstraction proposed by OOP.

class ProgrammingLanguages {
    constructor(names = [], extensions = [], examples = []) {
        this.names = names
        this.extensions = extensions
        this.examples = examples
    }
}

let languages = new ProgrammingLanguages(
    ["JavaScript", "Python", "Lua", "GDScript"],
    [".js", ".py", ".lua", ".gd"],
    [
        "console.log(\"Hello, my name is Franco!\")",
        "print(\"Hello, my name is Franco!\")",
        "print(\"Hello, my name is Franco!\")",
        "extends Node\nfunc _ready():\n    print(\"Hello, my name is Franco!\")"
    ]
)

for (let language_index in languages.names) {
    console.log("Programming Language: " + languages.names[language_index])
    console.log("Extension: " + languages.extensions[language_index])
    console.log("Example:")
    console.log(languages.examples[language_index])
    console.log("---")
}
class ProgrammingLanguages:
    def __init__(self, names = "", extensions = "", examples = ""):
        self.names = names
        self.extensions = extensions
        self.examples = examples

languages = ProgrammingLanguages(
    ["JavaScript", "Python", "Lua", "GDScript"],
    [".js", ".py", ".lua", ".gd"],
    [
        "console.log(\"Hello, my name is Franco!\")",
        "print(\"Hello, my name is Franco!\")",
        "print(\"Hello, my name is Franco!\")",
        "extends Node\nfunc _ready():\n    print(\"Hello, my name is Franco!\")"
    ]
)

for language_index in range(len(languages.names)):
    print("Programming Language: " + languages.names[language_index])
    print("Extension: " + languages.extensions[language_index])
    print("Example:")
    print(languages.examples[language_index])
    print("---")
function new_programming_languages(names, extensions, examples)
    local result = {
        names = names or {},
        extensions = extensions or {},
        examples = examples or {}
    }

    return result
end

local languages = new_programming_languages(
    {"JavaScript", "Python", "Lua", "GDScript"},
    {".js", ".py", ".lua", ".gd"},
    {
        "console.log(\"Hello, my name is Franco!\")",
        "print(\"Hello, my name is Franco!\")",
        "print(\"Hello, my name is Franco!\")",
        "extends Node\nfunc _ready():\n    print(\"Hello, my name is Franco!\")"
    }
)

for language_index = 1, #languages.names do
    print("Programming Language: " .. languages.names[language_index])
    print("Extension: " .. languages.extensions[language_index])
    print("Example:")
    print(languages.examples[language_index])
    print("---")
end
extends Node

class ProgrammingLanguages:
    var names
    var extensions
    var examples

    func _init(names = "", extensions = "", examples = ""):
        self.names = names
        self.extensions = extensions
        self.examples = examples

func _ready():
    var languages = ProgrammingLanguages.new(
        ["JavaScript", "Python", "Lua", "GDScript"],
        [".js", ".py", ".lua", ".gd"],
        [
            "console.log(\"Hello, my name is Franco!\")",
            "print(\"Hello, my name is Franco!\")",
            "print(\"Hello, my name is Franco!\")",
            "extends Node\nfunc _ready():\n    print(\"Hello, my name is Franco!\")"
        ]
    )

    for language_index in range(len(languages.names)):
        print("Programming Language: " + languages.names[language_index])
        print("Extension: " + languages.extensions[language_index])
        print("Example:")
        print(languages.examples[language_index])
        print("---")

Depending on the characteristics for data use and iteration, the performance of an implementation with an array of structures or with a register of arrays can greatly vary. For instance, in digital games with millions of entities, the use of a structure of arrays can have better performance, because it stores the data of each array sequentially in memory. This contributes to optimize the use of cache memory in loops with repetitions for thousands or millions of game entities. This approach is commonly used in a software architecture called Entity-Component-System (ECS).

On the other hand, business systems may process data from one entity a time (instead of thousands of similar entities a time). In this case, an array of structures can have better performance, if all data from a very same entity are stored sequentially (all of them could be retrieved at once). In practice, this does not always happen, though, because objects are commonly modeled as hierarchies or compositions of other objects. In other words, there may not be a guarantee they will be stored in contiguous memory positions. The use of good database management systems (DBMSs) can ease the problem, though care is required when storing the data in primary memory upon retrieval.

In general, as in every case, the ideal situation is measuring and comparing the performance using a tool such as a profile instead of assuming the behavior for a particular program. Each case is its own case.

Records Composition: Records With Records

A record can be defined by primitive types and composite types. In potential, one can compose a record by other preexisting records. To do this, it suffices to add an attribute to the record that is of the type of the other record.

For instance, a recipe can have a name, preparation_steps, and a list of ingredients. Each ingredient can have a name, a quantity and unit of measure (for instance, mass or volume -- measure_unit). One can define two records for this modeling: one for Ingredient, one for Recipe. The record recipe Recipe can have an array (or a list) of Ingredient.

class Ingredient {
    constructor(name = "", quantity = 0.0, measure_unit = "") {
        this.name = name
        this.quantity = quantity
        this.measure_unit = measure_unit
    }
}

class Recipe {
    constructor(name = "", preparation_steps = "", ingredients = []) {
        this.name = name
        this.preparation_steps = preparation_steps
        this.ingredients = ingredients
    }
}

let water = new Ingredient("Water", 3.0, "Cups")
let flour = new Ingredient("Flour", 4.0, "Cups")
let salt = new Ingredient("Salt", 2.0, "Tablespoons")
let yeast = new Ingredient("Yeast", 2.0, "Teaspoons")

let bread = new Recipe("Bread")
bread.ingredients.push(water)
bread.ingredients.push(flour)
bread.ingredients.push(salt)
bread.ingredients.push(yeast)
bread.preparation_steps = "..."

console.log(bread.name)
console.log("Ingredients:")
for (let ingredient of bread.ingredients) {
    console.log("- " + ingredient.name + ": " + ingredient.quantity + " " + ingredient.measure_unit)
}

console.log("Preparation steps:")
console.log(bread.preparation_steps)
class Ingredient:
    def __init__(self, name = "", quantity = 0.0, measure_unit = ""):
        self.name = name
        self.quantity = quantity
        self.measure_unit = measure_unit

class Recipe:
    def __init__(self, name = "", preparation_steps = "", ingredients = None):
        self.name = name
        self.preparation_steps = preparation_steps
        self.ingredients = ingredients if (ingredients != None) else []

water = Ingredient("Water", 3.0, "Cups")
flour = Ingredient("Flour", 4.0, "Cups")
salt = Ingredient("Salt", 2.0, "Tablespoons")
yeast = Ingredient("Yeast", 2.0, "Teaspoons")

bread = Recipe("Bread")
bread.ingredients.append(water)
bread.ingredients.append(flour)
bread.ingredients.append(salt)
bread.ingredients.append(yeast)
bread.preparation_steps = "..."

print(bread.name)
print("Ingredients:")
for ingredient in bread.ingredients:
    print("- " + ingredient.name + ": " + str(ingredient.quantity) + " " + ingredient.measure_unit)

print("Preparation steps:")
print(bread.preparation_steps)
function new_ingredient(name, quantity, measure_unit)
    local result = {
        name = name or "",
        quantity = quantity or 0.0,
        measure_unit = measure_unit or ""
    }

    return result
end

function new_recipe(name, preparation_steps, ingredients)
    local result = {
        name = name or "",
        preparation_steps = quantity or "",
        ingredients = ingredients or {}
    }

    return result
end

local water = new_ingredient("Water", 3.0, "Cups")
local flour = new_ingredient("Flour", 4.0, "Cups")
local salt = new_ingredient("Salt", 2.0, "Tablespoons")
local yeast = new_ingredient("Yeast", 2.0, "Teaspoons")

local bread = new_recipe("Bread")
table.insert(bread.ingredients, water)
table.insert(bread.ingredients, flour)
table.insert(bread.ingredients, salt)
table.insert(bread.ingredients, yeast)
bread.preparation_steps = "..."

print(bread.name)
print("Ingredients:")
for _, ingredient in ipairs(bread.ingredients) do
    print("- " .. ingredient.name .. ": " .. ingredient.quantity .. " " .. ingredient.measure_unit)
end

print("Preparation steps:")
print(bread.preparation_steps)
extends Node

class Ingredient:
    var name
    var quantity
    var measure_unit

    func _init(name = "", quantity = 0.0, measure_unit = ""):
        self.name = name
        self.quantity = quantity
        self.measure_unit = measure_unit

class Recipe:
    var name
    var preparation_steps
    var ingredients

    func _init(name = "", preparation_steps = "", ingredients = []):
        self.name = name
        self.preparation_steps = preparation_steps
        self.ingredients = ingredients

func _ready():
    var water = Ingredient.new("Water", 3.0, "Cups")
    var flour = Ingredient.new("Flour", 4.0, "Cups")
    var salt = Ingredient.new("Salt", 2.0, "Tablespoons")
    var yeast = Ingredient.new("Yeast", 2.0, "Teaspoons")

    var bread = Recipe.new("Bread")
    bread.ingredients.append(water)
    bread.ingredients.append(flour)
    bread.ingredients.append(salt)
    bread.ingredients.append(yeast)
    bread.preparation_steps = "..."

    print(bread.name)
    print("Ingredients:")
    for ingredient in bread.ingredients:
        print("- " + ingredient.name + ": " + str(ingredient.quantity) + " " + ingredient.measure_unit)

    print("Preparation steps:")
    print(bread.preparation_steps)

It is possible to modify the example to change the initialization. For instance, instead of inserting each Ingredient in the ingredients array, one could initialize the array directly in the subroutine used as constructor. For instance:

bread = new Recipe("Bread",
                  "...",
                  [
                      water,
                      flour,
                      salt,
                      yeast
                  ])

In particular, it would be even more convenient defining subroutines for valid operations to manipulate the records, as it will be discussed in one of the following subsections.

Data Abstraction and Modeling Granularity

If one wishes, she/he could define records for unit of measure (MeasureUnit, that could combine quantity and measure_unit in a single type) or for the preparation steps (for instance, it could be divided into initial_preparation, instructions, preparation_time, tips, how_to_serve...).

Thus, it is always possible to create more (or less) data types to abstract values. More types mean that the solution has higher data abstraction and greater granularity. The granularity defines the detailing and structuring of the data type.

The choice of granularity depends on the problem to be modeled. Greater granularity is not always better; ideally, there should be an equilibrium. Greater granularity means that the solution will be more detailed and compartmentalized, which makes it easier to access data (because there will exist more fields to access and consult). Although more structured, the program can be harder to use and require more effort to implement. Smaller granularity means the type will be closer to a primitive type. Although simpler to store, data extraction will be more complex (because it will possibly require processing the primitive type).

In particular, a useful criterion to define granularity is determined what values will be searched for in a program. Variables that represent a same object or a same entity can have the same instance of a type, to make searches easier.

For instance, one can consider modeling an address. The address may be a simple string. It could also be a record composed of fields such as street, number, city, province, country, postal_code, complement... In the first case, the extraction of a street would require manipulating the string. If there did not exist a standard to define the address, the extraction could be complex or even impossible (where is the city in the string? It can be anywhere). In the second case, the extraction is simple: address.street. On the other hand, the set-up of the record requires mode operations (both for the initial implementation and when using the system). In the simplest form, instead of requesting a single value (the complete address), the program should read a value for street, another for number, and so on for every field.

It is worth noticing forms in websites or in program. Each provided field in a sign up form allows inferring how the data is stored and processed in the system, as well as how they are consulted.

For instance, in the second approach, a more sophisticated form and data model could continue the composition. One could define a record Country, composed by a collection of Provice, each of which composed by a collection of City... The greater the number of unique structures, the greater the granularity. It is also possible saving memory instancing each value a single time, and referencing the created variable whenever needed. A benefit is centralizing the local of the updates: for instance, to update the information about one City in all addresses that mentions it, it would suffice modifying the original reference (instead of modifying each individual address).

Record With Reference to the Record Itself

As there exists recursive subroutines, there are recursive data types (or recursive structures or recursive classes). In other words, a data type that has an attribute of its own data type.

For instance, a system can be composed of subsystems (that are also system). A section can be composed of subsections (which are also sections). A list can be composed of sublists (which are also lists and have been used over this topic).

In fact, data structures are commonly implemented as recursive data types. One can think of a list as a record that stores a value and a reference to the own type of the list (the next entry).

In some programming languages, a forward declaration of the new type before adding an attribute of the record itself. This happens because some compilers and interpreters must know the existence of a data type before using it (for instance, to know the required quantity of memory to allocate a variable of the type). The forward declaration informs the tool that the type will exist (at some time), even if it was not yet implement. This is possible because references to memory address typically have fixed addressing size (4 bytes for 32-bit systems and 8 bytes for 64-bit systems).

The following example presents a prototype of a record for a type for an linked item list. The type ListItem has two fields: the stored value and a reference to the next item of the list. An entry on which the next value is an invalid or empty reference (null or nil, in the considered languages) means the ending of the list. To make it easier to read, one could define a constant LIST_END = null, although the use an empty reference is common and idiomatic.

class ListItem {
    constructor(value, next = null) {
        this.value = value
        this.next = next}
}

let numbers = new ListItem(1,
                           new ListItem(2,
                                        new ListItem(3)))
let list_item = numbers
while (list_item !== null) {
    console.log(list_item.value)
    list_item = list_item.next
}
class ListItem:
    def __init__(self, value, next = None):
        self.value = value
        self.next = next

numbers = ListItem(1,
                   ListItem(2,
                            ListItem(3)))
list_item = numbers
while (list_item != None):
    print(list_item.value)
    list_item = list_item.next
function new_list_item(value, next)
    local result = {
        value = value,
        next = next or nil
    }

    return result
end

local numbers = new_list_item(1,
                              new_list_item(2,
                                            new_list_item(3)))
local list_item = numbers
while (list_item ~= nil) do
    print(list_item.value)
    list_item = list_item.next
end
extends Node

class ListItem:
    var value
    var next

    func _init(value, next = null):
        self.value = value
        self.next = next

func _ready():
    var numbers = ListItem.new(1,
                               ListItem.new(2,
                                            ListItem.new(3)))
    var list_item = numbers
    while (list_item != null):
        print(list_item.value)
        list_item = list_item.next

A linked list is one of the most basic to implement dynamic data structures. The example presents insertions at the end (push(), push_back() or append()). A complete implementation would offer insertions at arbitrary positions and removals. To do this, it is worth integrating records with subroutines.

Records and Subroutines

Records can abstract data (data abstraction). Subroutines can abstract processing (functional abstraction). The combination of records and subroutines allows abstracting data and processing. Indeed, such combination is one of the fundamental characteristics of OOP.

Instead of rewriting operations to manipulate every record created as a data type, one can define subroutines to do so. Subroutine for records work as subroutines and operators for primitive types: they can define predefined operations to manipulate data with convenience and safety. In OOP, this is part of operations defined as method (and, possibly, as overloaded operators) and of a desirable characteristic names encapsulation.

For instance, OOP implementation commonly defined code with methods called getters and setters. Generically, these methods are called accessor methods. The goal of a get() method is retrieving the value of an attribute. The goal of a set() method is to set (initialize or modify) a value that is considered valid for the class, and prohibit invalid changes. In other words, they guarantee that an object always has a valid state, which ensures it consistency and safety for use.

For an introductory example, one can consider an elevator. An elevator can have a current_floor, minimum_floor and a maximum_floor. The current_floor must be less than or equal to maximum_floor. It also must be greater than or equal to minimum_floor. If one considers underground floors as negative floors, floors value could be negative as well. For English readers, it is important noticing that floor conventions vary between countries, though they are not particularly important for this example.

class Elevator {
    constructor(starting_floor = 0, minimum_floor = 0, maximum_floor = 5) {
        let maximum = minimum_floor
        let minimum = minimum_floor
        if (minimum_floor > maximum_floor) {
            minimum = maximum_floor
        } else {
            maximum = maximum_floor
        }

        let initial = starting_floor
        if (starting_floor < minimum) {
            initial = minimum
        } else if (starting_floor > maximum) {
            initial = maximum
        }

        this.minimum_floor = minimum
        this.maximum_floor = maximum
        this.current_floor = initial
    }
}

function set_floor(elevator, new_floor) {
    if ((new_floor >= elevator.minimum_floor) && (new_floor <= elevator.maximum_floor)) {
        elevator.current_floor = new_floor
    }
}

function go_up_one_floor(elevator) {
    set_floor(elevator, elevator.current_floor + 1)
}

function go_down_one_floor(elevator) {
    set_floor(elevator, elevator.current_floor - 1)
}

function random_integer(minimum_inclusive, maximum_inclusive) {
    let minimum = Math.ceil(minimum_inclusive)
    let maximum = Math.floor(maximum_inclusive)

    return Math.floor(minimum + Math.random() * (maximum + 1 - minimum))
}

let elevator = new Elevator(0, 0, 5)
for (i = 0; i < 10; ++i) {
    let starting_floor = elevator.current_floor
    console.log("The elevator is on the " + starting_floor + "º floor")

    if (random_integer(0, 1) === 0) {
        console.log("Next command: go down")
        go_down_one_floor(elevator)
    } else {
        console.log("Next command: go up")
        go_up_one_floor(elevator)
    }

    let floor_final = elevator.current_floor
    if (starting_floor !== floor_final) {
        console.log("Vruuum!")
    } else {
        console.log("... Nothing happens.")
    }
}
console.log("The elevator is on the " + elevator.current_floor + "º floor")
import random

class Elevator:
    def __init__(self, starting_floor = 0, minimum_floor = 0, maximum_floor = 5):
        maximum = minimum_floor
        minimum = minimum_floor
        if (minimum_floor > maximum_floor):
            minimum = maximum_floor
        else:
            maximum = maximum_floor

        initial = starting_floor
        if (starting_floor < minimum):
            initial = minimum
        elif (starting_floor > maximum):
            initial = maximum

        self.minimum_floor = minimum
        self.maximum_floor = maximum
        self.current_floor = initial

def set_floor(elevator, new_floor):
    if ((new_floor >= elevator.minimum_floor) and (new_floor <= elevator.maximum_floor)):
        elevator.current_floor = new_floor

def go_up_one_floor(elevator):
    set_floor(elevator, elevator.current_floor + 1)

def go_down_one_floor(elevator):
    set_floor(elevator, elevator.current_floor - 1)

random.seed()
elevator = Elevator(0, 0, 5)
for i in range(10):
    starting_floor = elevator.current_floor
    print("The elevator is on the " + str(starting_floor) + "º floor")

    if (random.randint(0, 1) == 0):
        print("Next command: go down")
        go_down_one_floor(elevator)
    else:
        print("Next command: go up")
        go_up_one_floor(elevator)

    floor_final = elevator.current_floor
    if (starting_floor != floor_final):
        print("Vruuum!")
    else:
        print("... Nothing happens.")

print("The elevator is on the " + str(elevator.current_floor) + "º floor")
function new_elevator(starting_floor, minimum_floor, maximum_floor)
    starting_floor = starting_floor or 0
    minimum_floor = minimum_floor or 0
    maximum_floor = maximum_floor or 0

    local maximum = minimum_floor
    local minimum = minimum_floor
    if (minimum_floor > maximum_floor) then
        minimum = maximum_floor
    else
        maximum = maximum_floor
    end

    local initial = starting_floor
    if (starting_floor < minimum) then
        initial = minimum
    elseif (starting_floor > maximum) then
        initial = maximum
    end

    local result = {
        minimum_floor = minimum,
        maximum_floor = maximum,
        current_floor = initial
    }

    return result
end

function set_floor(elevator, new_floor)
    if ((new_floor >= elevator.minimum_floor) and (new_floor <= elevator.maximum_floor)) then
        elevator.current_floor = new_floor
    end
end

function go_up_one_floor(elevator)
    set_floor(elevator, elevator.current_floor + 1)
end

function go_down_one_floor(elevator)
    set_floor(elevator, elevator.current_floor - 1)
end

math.randomseed(os.time())
local elevator = new_elevator(0, 0, 5)
for i = 1, 10 do
    local starting_floor = elevator.current_floor
    print("The elevator is on the " .. starting_floor .. "º floor")

    if (math.random(0, 1) == 0) then
        print("Next command: go down")
        go_down_one_floor(elevator)
    else
        print("Next command: go up")
        go_up_one_floor(elevator)
    end

    local floor_final = elevator.current_floor
    if (starting_floor ~= floor_final) then
        print("Vruuum!")
    else
        print("... Nothing happens.")
    end
end

print("The elevator is on the " .. elevator.current_floor .. "º floor")
extends Node

class Elevator:
    var current_floor
    var minimum_floor
    var maximum_floor

    func _init(starting_floor = 0, minimum_floor = 0, maximum_floor = 5):
        var maximum = minimum_floor
        var minimum = minimum_floor
        if (minimum_floor > maximum_floor):
            minimum = maximum_floor
        else:
            maximum = maximum_floor

        var initial = starting_floor
        if (starting_floor < minimum):
            initial = minimum
        elif (starting_floor > maximum):
            initial = maximum

        self.minimum_floor = minimum
        self.maximum_floor = maximum
        self.current_floor = initial

func set_floor(elevator, new_floor):
    if ((new_floor >= elevator.minimum_floor) and (new_floor <= elevator.maximum_floor)):
        elevator.current_floor = new_floor

func go_up_one_floor(elevator):
    set_floor(elevator, elevator.current_floor + 1)

func go_down_one_floor(elevator):
    set_floor(elevator, elevator.current_floor - 1)

func random_integer(minimum_inclusive, maximum_inclusive):
    var minimum = ceil(minimum_inclusive)
    var maximum = floor(maximum_inclusive)

    # randi(): [0.0, 1.0[
    return randi() % int(maximum + 1 - minimum) + minimum

func _ready():
    randomize()
    var elevator = Elevator.new(0, 0, 5)
    for i in range(10):
        var starting_floor = elevator.current_floor
        print("The elevator is on the " + str(starting_floor) + "º floor")

        if (random_integer(0, 1) == 0):
            print("Next command: go down")
            go_down_one_floor(elevator)
        else:
            print("Next command: go up")
            go_up_one_floor(elevator)

        var floor_final = elevator.current_floor
        if (starting_floor != floor_final):
            print("Vruuum!")
        else:
            print("... Nothing happens.")

    print("The elevator is on the " + str(elevator.current_floor) + "º floor")

The example provides the most robust constructor presented in this topic. The implementation ensures that all used values are valid to guarantee the consistency of the data to be processed. The provided checks guarantee that the values are initialized according to the problem's specification. Technically, as the languages in the examples are dynamically typed, there still exists the possibility of assigning a value of an invalid type to the values; this will be commented in one of the next subsections.

For a possible improvement, one could define a procedure write_floor(). This way, it could write the ground floor instead of floor zero. It also could use the correct suffix for the ordinal number, which currently uses the Portuguese convention.

Regardless, procedural languages normally do not guarantee data integrity against direct assignments. Although set_floor() does not allow the assignment of an invalid value to the variable current_floor, one could still write elevator.current_floor = 1234 (assuming that 1234 is greater than maximum_floor) or elevator.current_floor = "Franco". In OOP, it is possible to inhibit such misuse with access modifier access specifiers. Three of most common options are private, public and protected, which appear, for instance, in languages such as C++, Java and C#. An attribute with public access is similar to a record attribute: it can be accessed and modified anywhere in the code where the object is in scope. An attribute with private access can be modified only by the class that defines it. An attribute with protected access can be modified only by the class that has defined it or by its derived classes (subclasses). Thus, private and protected could be used to inhibit restricted values' reads and writes (provided they were used, the subroutines were converted to methods, and that a method get_current_floor() provided access to the restricted value).

As the topic is not about OOP, one must have responsibility and maturity to correctly use the implementation. Modifications must be performed using only the defined subroutines, even if values could be changed directly. This must be self-imposed. In fact, many good OOP practices can be applied to other programming languages by means of common sense, judgment and self-control.

Abstract Data Types (ADTs)

The combination of data abstraction with functional abstraction can be used to create abstract data types (or ADTs). A possible implementation of an abstract data type is called a concrete data type.

An abstract data type defines an interface to hide implementation details of a record. Instead of direct manipulating the internal variables, one use the defined subroutines to program using the ADT. The definition of the subroutines is responsible to correctly modify the attributes. In other words, the data and the code to process them are mere implementation details. The use of the type must be performed only with the provided subroutines.

The specification of interfaces to data structures is a typical example of abstract data types. For instance, a list can include following operations:

  • Create a list;
  • Add elements to the list;
  • Remove elements from the list;
  • Access list element;
  • Iterate over the list.

To demonstrate the potential of the approach, the example for the the ListItem can be expanded to define a type List, that implements a linked list (hence called LinkedList). Although the implementation does not use any features that has not been previously mentioned (in Python, del allows deallocating memory, as previously mentioned in dictionaries), it is more complex than other examples presented hitherto. For this topic, the main takeaway is not to completely understand the example, though to notice that the operations defined as subroutines allow to hide the implementation details of the record.

class ListItem {
    constructor(value, next = null) {
        this.value = value
        this.next = next
    }
}

class LinkedList {
    constructor() {
        this.beginning = null
        this.size = 0
    }
}

function access_list_item(a_list, index) {
    if ((index < 0) || (index >= a_list.size)) {
        return null
    }

    let current_index = 0
    let list_item = a_list.beginning
    while (current_index < index) {
        list_item = list_item.next
        ++current_index
    }

    return list_item
}

// Negative index inserts to the end of the list.
function add_to_list(a_list, value, index = -1) {
    if (index > a_list.size) {
        // Index after the final of the list; usage error.
        return a_list
    }

    let new_item = new ListItem(value)
    if (!a_list.beginning) {
        a_list.beginning = new_item
    } else {
        if (index === 0) {
             new_item.next = a_list.beginning
             a_list.beginning = new_item
        } else {
            if (index < 0) {
                index = a_list.size
            }

            let previous_item = access_list_item(a_list, index - 1)
            new_item.next = previous_item.next
            previous_item.next = new_item
        }
    }

    ++a_list.size

    return a_list
}

// Negative index removes from the end of the list.
function remove_from_list(a_list, index = -1) {
    if (!a_list.beginning) {
        // Empty list, there is nothing to remove.
        return a_list
    } else if (index >= a_list.size) {
        // Index after the final of the list; usage error.
        return a_list
    }

    if (index === 0) {
        let item_to_remove = a_list.beginning
        a_list.beginning = a_list.beginning.next
        item_to_remove = null
    } else {
        if (index < 0) {
            index = a_list.size - 1
        }

        let previous_item = access_list_item(a_list, index - 1)
        let item_to_remove = previous_item.next
        previous_item.next = item_to_remove.next
        item_to_remove = null
    }

    --a_list.size

    return a_list
}

function write_list(a_list) {
    let list_item = a_list.beginning
    let text = "["
    while (list_item) {
        text += list_item.value + ", "
        list_item = list_item.next
    }
    text += "]"

    console.log(text)
}

let numbers = new LinkedList()
add_to_list(numbers, 1)
add_to_list(numbers, 2)
add_to_list(numbers, 3)
add_to_list(numbers, 0, 0)
add_to_list(numbers, 1.5, 2)
add_to_list(numbers, 2.5, 4)
add_to_list(numbers, 4, 6)
add_to_list(numbers, 3.5, 6)
write_list(numbers)

remove_from_list(numbers)
remove_from_list(numbers, 0)
remove_from_list(numbers, 1)
remove_from_list(numbers, 3)
write_list(numbers)
class ListItem:
    def __init__(self, value, next = None):
        self.value = value
        self.next = next

class LinkedList:
    def __init__(self):
        self.beginning = None
        self.size = 0

def access_list_item(a_list, index):
    if ((index < 0) or (index >= a_list.size)):
        return None

    current_index = 0
    list_item = a_list.beginning
    while (current_index < index):
        list_item = list_item.next
        current_index += 1

    return list_item

# Negative index inserts to the end of the list.
def add_to_list(a_list, value, index = -1):
    if (index > a_list.size):
        # Index after the final of the list; usage error.
        return a_list

    new_item = ListItem(value)
    if (not a_list.beginning):
        a_list.beginning = new_item
    else:
        if (index == 0):
             new_item.next = a_list.beginning
             a_list.beginning = new_item
        else:
            if (index < 0):
                index = a_list.size

            previous_item = access_list_item(a_list, index - 1)
            new_item.next = previous_item.next
            previous_item.next = new_item

    a_list.size += 1

    return a_list

# Negative index removes from the end of the list.
def remove_from_list(a_list, index = -1):
    if (not a_list.beginning):
        # Empty list, there is nothing to remove.
        return a_list
    elif (index >= a_list.size):
        # Index after the final of the list; usage error.
        return a_list

    if (index == 0):
        item_to_remove = a_list.beginning
        a_list.beginning = a_list.beginning.next
        del item_to_remove
    else:
        if (index < 0):
            index = a_list.size - 1

        previous_item = access_list_item(a_list, index - 1)
        item_to_remove = previous_item.next
        previous_item.next = item_to_remove.next
        item_to_remove

    a_list.size -= 1

    return a_list

def write_list(a_list):
    list_item = a_list.beginning
    text = "["
    while (list_item):
        text += str(list_item.value) + ", "
        list_item = list_item.next

    text += "]"

    print(text)

numbers = LinkedList()
add_to_list(numbers, 1)
add_to_list(numbers, 2)
add_to_list(numbers, 3)
add_to_list(numbers, 0, 0)
add_to_list(numbers, 1.5, 2)
add_to_list(numbers, 2.5, 4)
add_to_list(numbers, 4, 6)
add_to_list(numbers, 3.5, 6)
write_list(numbers)

remove_from_list(numbers)
remove_from_list(numbers, 0)
remove_from_list(numbers, 1)
remove_from_list(numbers, 3)
write_list(numbers)
function new_list_item(value, next)
    local result = {
        value = value,
        next = next or nil
    }

    return result
end

function new_linked_list()
    local result = {
        beginning = nil,
        size = 0
    }

    return result
end

function access_list_item(a_list, index)
    if ((index < 1) or (index > a_list.size)) then
        return nil
    end

    local current_index = 1
    local list_item = a_list.beginning
    while (current_index < index) do
        list_item = list_item.next
        current_index = current_index + 1
    end

    return list_item
end

-- Zero or negative index inserts to the end of the list.
function add_to_list(a_list, value, index)
    index = index or -1

    if (index > (a_list.size + 1)) then
        -- Index after the final of the list; usage error.
        return a_list
    end

    local new_item = new_list_item(value)
    if (not a_list.beginning) then
        a_list.beginning = new_item
    else
        if (index == 1) then
             new_item.next = a_list.beginning
             a_list.beginning = new_item
        else
            if (index <= 0) then
                index = a_list.size + 1
            end

            local previous_item = access_list_item(a_list, index - 1)
            new_item.next = previous_item.next
            previous_item.next = new_item
        end
    end

    a_list.size = a_list.size + 1

    return a_list
end

-- Zero or negative index removes from the end of the list.
function remove_from_list(a_list, index)
    index = index or -1

    if (not a_list.beginning) then
        -- Empty list, there is nothing to remove.
        return a_list
    elseif (index > a_list.size) then
        -- Index after the final of the list; usage error.
        return a_list
    end

    if (index == 1) then
        local item_to_remove = a_list.beginning
        a_list.beginning = a_list.beginning.next
        item_to_remove = nil
    else
        if (index <= 0) then
            index = a_list.size
        end

        local previous_item = access_list_item(a_list, index - 1)
        local item_to_remove = previous_item.next
        previous_item.next = item_to_remove.next
        item_to_remove = nil
    end

    a_list.size = a_list.size - 1

    return a_list
end

function write_list(a_list)
    local list_item = a_list.beginning
    local text = "["
    while (list_item) do
        text = text .. tostring(list_item.value) .. ", "
        list_item = list_item.next
    end

    text = text .. "]"

    print(text)
end

local numbers = new_linked_list()
add_to_list(numbers, 1)
write_list(numbers)
add_to_list(numbers, 2)
write_list(numbers)
add_to_list(numbers, 3)
write_list(numbers)
add_to_list(numbers, 0, 1)
write_list(numbers)
add_to_list(numbers, 1.5, 3)
write_list(numbers)
add_to_list(numbers, 2.5, 5)
write_list(numbers)
add_to_list(numbers, 4, 7)
write_list(numbers)
add_to_list(numbers, 3.5, 7)
write_list(numbers)

remove_from_list(numbers)
remove_from_list(numbers, 1)
remove_from_list(numbers, 2)
remove_from_list(numbers, 4)
write_list(numbers)
extends Node

class ListItem:
    var value
    var next

    func _init(value, next = null):
        self.value = value
        self.next = next

class LinkedList:
    var beginning
    var size

    func _init():
        self.beginning = null
        self.size = 0

func access_list_item(a_list, index):
    if ((index < 0) or (index >= a_list.size)):
        return null

    var current_index = 0
    var list_item = a_list.beginning
    while (current_index < index):
        list_item = list_item.next
        current_index += 1

    return list_item

# Negative index inserts to the end of the list.
func add_to_list(a_list, value, index = -1):
    if (index > a_list.size):
        # Index after the final of the list; usage error.
        return a_list

    var new_item = ListItem.new(value)
    if (not a_list.beginning):
        a_list.beginning = new_item
    else:
        if (index == 0):
             new_item.next = a_list.beginning
             a_list.beginning = new_item
        else:
            if (index < 0):
                index = a_list.size

            var previous_item = access_list_item(a_list, index - 1)
            new_item.next = previous_item.next
            previous_item.next = new_item

    a_list.size += 1

    return a_list

# Negative index removes from the end of the list.
func remove_from_list(a_list, index = -1):
    if (not a_list.beginning):
        # Empty list, there is nothing to remove.
        return a_list
    elif (index >= a_list.size):
        # Index after the final of the list; usage error.
        return a_list

    if (index == 0):
        var item_to_remove = a_list.beginning
        a_list.beginning = a_list.beginning.next
        # item_to_remove.unreference()
        item_to_remove = null
    else:
        if (index < 0):
            index = a_list.size - 1

        var previous_item = access_list_item(a_list, index - 1)
        var item_to_remove = previous_item.next
        previous_item.next = item_to_remove.next
        # item_to_remove.unreference()
        item_to_remove = null

    a_list.size -= 1

    return a_list

func write_list(a_list):
    var list_item = a_list.beginning
    var text = "["
    while (list_item):
        text += str(list_item.value) + ", "
        list_item = list_item.next

    text += "]"

    print(text)

func _ready():
    var numbers = LinkedList.new()
    add_to_list(numbers, 1)
    add_to_list(numbers, 2)
    add_to_list(numbers, 3)
    add_to_list(numbers, 0, 0)
    add_to_list(numbers, 1.5, 2)
    add_to_list(numbers, 2.5, 4)
    add_to_list(numbers, 4, 6)
    add_to_list(numbers, 3.5, 6)
    write_list(numbers)

    remove_from_list(numbers)
    remove_from_list(numbers, 0)
    remove_from_list(numbers, 1)
    remove_from_list(numbers, 3)
    write_list(numbers)

The example does not provide a subroutine for iteration, although it could be implemented, for instance, as a function access_next_item().

Although the implementation of the linked list is more complex than usual, it is simple to operate with a variable of the type LinkedList and it is similar to the use of arrays and lists in languages such as JavaScript, Python, GDScript and Lua:

  • The creation of the list uses the provided constructor if supported by the language or the provided creation function (new_linked_list());
  • The insertion of a value to the list uses the function add_to_list();
  • The removal of a value from the list uses the function remove_from_list();
  • The access to a value from the list uses the function access_list_item().

In languages that provide support for operator overloading, it would be possible, for instance, to define the square brackets' operator to access the value of an index in the list. That would make reading a value similar to retrieving a value in an array.

Record as Parameter to Subroutine

In complex problems, it is possible that a subroutine need to receive many parameters. For instance, complex simulations can have tens or hundreds of variables as configuration parameters. However, the use of subroutines with multiple parameters become complex. In particular, it can become hard to introduce or remove a parameter to/from the definition of a subroutine.

In such cases, instead of defining a long list of parameters, it can be better defining a record with the single parameter to the subroutine. All other parameters can be added to the record. The default values for the record can be the most commonly used values for the subroutine. Thus, one could modify the desired values for custom calls.

class ComplexFunctionParameters {
    constructor() {
        this.greetings = "Hello"
        this.name = "Franco"
        this.goodbye = "Goodbye."
    }
}

function my_complex_subroutine(parameters) {
    console.log(parameters.greetings, parameters.name, parameters.goodbye)
}

var parameters = new ComplexFunctionParameters()
parameters.greetings = "Hi"
my_complex_subroutine(parameters)
class ComplexFunctionParameters:
    def __init__(self):
        self.greetings = "Hello"
        self.name = "Franco"
        self.goodbye = "Goodbye."

def my_complex_subroutine(parameters):
    print(parameters.greetings, parameters.name, parameters.goodbye)

parameters = ComplexFunctionParameters()
parameters.greetings = "Hi"
my_complex_subroutine(parameters)
function new_complex_function_parameters()
    local result = {
        greetings = "Hello!",
        name = "Franco",
        goodbye = "Goodbye."
    }

    return result
end

function my_complex_subroutine(parameters)
    print(parameters.greetings, parameters.name, parameters.goodbye)
end

local parameters = new_complex_function_parameters()
parameters.greetings = "Hi"
my_complex_subroutine(parameters)
extends Node

class ComplexFunctionParameters:
    var greetings = "Hello!"
    var name = "Franco"
    var goodbye = "Goodbye."

func my_complex_subroutine(parameters):
    printt(parameters.greetings, parameters.name, parameters.goodbye)

func _ready():
    var parameters = ComplexFunctionParameters.new()
    parameters.greetings = "Hi"
    my_complex_subroutine(parameters)

The example defines only three parameters, though they could be 10 or 20, for instance. It is important noticing that the values are not defined in the constructor; otherwise, the utility of the technique could be practically none. After all, the only modification would be in the place of the problem.

Besides, an initialization with predefined values can reduce the performance of a program, if it was computationally expensive and all values were redefined medially afterwards. Therefore, it is wise to balance values that are default and those that must be, obligatorily, defined before the call. However, before assuming, ideally one should measure the impact using a profile to check if an optimization is truly needed.

Verification of Types of Parameters

In programming languages with dynamic typing, assertions can be used to verify the types for parameters used when building a record. This can guarantee that the values are initialized with correct types (as the language does not restrict assignments according to types).

class MyNumber {
    constructor(value) {
        console.assert(typeof(value) === "number", "value must be a number.")
        this.value = value
    }
}

var valid = new MyNumber(1)
var invalid = new MyNumber("Franco")
class MyNumber:
    def __init__(self, value):
        assert isinstance(value, (int, float)), "value must be a number."
        self.value = value

valid = MyNumber(1)
invalid = MyNumber("Franco")
function new_my_number(value)
    assert(type(value) == "number", "value must be a number.")

    local result = {
        value = value
    }

    return result
end

local valid = new_my_number(1)
local invalid = new_my_number("Franco")
extends Node

class MyNumber:
    var value

    func _init(value):
        assert(typeof(value) == TYPE_INT or typeof(value) == TYPE_REAL, "value must be a number.")
        self.value = value

func _ready():
    var valid = MyNumber.new(1)
    var invalid = MyNumber.new("Franco")

As it has been previously done in Subroutines (Functions and Procedures), assertions can also be used to verify values. Thus, they could inhibit an initial creation with invalid types and/or initial values.

Examples

This section provides additional examples to complement the ones provided over the text. The examples are complete programs, structured using records and processed by combinations of subroutines. The goal is showing how to organize programs and that is already possible to starting writing more complete and complex systems using the knowledge acquired hitherto.

High Level Program

The combination of records, subroutines and collections allow creating complex programs that are elegant and easy to read. This section expands the example of recipes and ingredients to add features to the program. New features are defined with subroutines.

class Ingredient {
    constructor(name = "", quantity = 0.0, measure_unit = "") {
        this.name = name
        this.quantity = quantity
        this.measure_unit = measure_unit
    }
}

class Recipe {
    constructor(name = "", preparation_steps = "", ingredients = []) {
        this.name = name
        this.preparation_steps = preparation_steps
        this.ingredients = ingredients
    }
}

function write_ingredient(ingredient) {
    alert("- " + ingredient.name + ": " + ingredient.quantity + " " + ingredient.measure_unit)
}

function read_ingredient() {
    let ingredient = new Ingredient()
    ingredient.name = prompt("Ingredient name:")
    ingredient.quantity = prompt("Quantity of " + ingredient.name + ":")
    ingredient.measure_unit = prompt("Measure unit:")

    return ingredient
}

function write_recipe(recipe) {
    alert(recipe.name)
    alert("Ingredients:")
    for (let ingredient of recipe.ingredients) {
        write_ingredient(ingredient)
    }

    alert("Preparation steps:")
    alert(recipe.preparation_steps)
}

function read_recipe() {
    let recipe = new Recipe()
    recipe.name = prompt("Recipe name:")
    alert("Ingredients")
    let add_new_ingredient = true
    while (add_new_ingredient) {
        let ingredient = read_ingredient()
        recipe.ingredients.push(ingredient)

        let answer = prompt("Do you want to add a new ingredient?\nType yes to continue, any other value to finish")
        add_new_ingredient = (answer.toLowerCase() === "yes")
    }

    recipe.preparation_steps = prompt("Preparation steps for " + recipe.name + ":")

    return recipe
}

function write_recipes(recipes) {
    alert("Recipes Book")
    let recipe_number = 1
    for (recipe of recipes) {
        alert("Recipe #" + recipe_number)
        write_recipe(recipe)
        alert("")
        ++recipe_number
    }
}

function write_menu() {
    let message = "Options\n\n"
    message += "1. Show all registered recipes;\n"
    message += "2. Register a new recipe;\n"
    message += "\n"
    message += "Any other value ends the program.\n"

    alert(message)
}

function load_recipes() {
    let recipes = [
        new Recipe("Bread",
                    "...",
                    [
                        new Ingredient("Water", 3.0, "Cups"),
                        new Ingredient("Flour", 4.0, "Cups"),
                        new Ingredient("Salt", 2.0, "Tablespoons"),
                        new Ingredient("Yeast", 2.0, "Teaspoons")
                    ]),
        new Recipe("Sweet Bread",
                    "...",
                    [
                        new Ingredient("Water", 3.0, "Cups"),
                        new Ingredient("Flour", 4.0, "Cups"),
                        new Ingredient("Sugar", 2.0, "Cups"),
                        new Ingredient("Salt", 2.0, "Tablespoons"),
                        new Ingredient("Yeast", 2.0, "Teaspoons")
                    ]),
    ]

    return recipes
}

function main() {
    let recipes = load_recipes()
    let finished = false
    while (!finished) {
        write_menu()
        let chosen_option = prompt("Choose an option:")
        switch (chosen_option) {
            case "1": {
                write_recipes(recipes)
                break
            }

            case "2": {
                let new_recipe = read_recipe()
                recipes.push(new_recipe)
                break
            }

            default: {
                finished = true
            }
        }
    }
}

main()
class Ingredient:
    def __init__(self, name = "", quantity = 0.0, measure_unit = ""):
        self.name = name
        self.quantity = quantity
        self.measure_unit = measure_unit

class Recipe:
    def __init__(self, name = "", preparation_steps = "", ingredients = None):
        self.name = name
        self.preparation_steps = preparation_steps
        self.ingredients = ingredients if (ingredients != None) else []

def write_ingredient(ingredient):
    print("- " + ingredient.name + ": " + str(ingredient.quantity) + " " + ingredient.measure_unit)

def read_ingredient():
    ingredient = Ingredient()
    ingredient.name = input("Ingredient name: ")
    ingredient.quantity = float(input("Quantity of " + ingredient.name + ": "))
    ingredient.measure_unit = input("Measure unit: ")

    return ingredient

def write_recipe(recipe):
    print(recipe.name)
    print("Ingredients: ")
    for ingredient in recipe.ingredients:
        write_ingredient(ingredient)

    print("Preparation steps: ")
    print(recipe.preparation_steps)

def read_recipe():
    recipe = Recipe()
    recipe.name = input("Recipe name: ")
    print("Ingredients")
    add_new_ingredient = True
    while (add_new_ingredient):
        ingredient = read_ingredient()
        recipe.ingredients.append(ingredient)

        answer = input("Do you want to add a new ingredient?\nType yes to continue, any other value to finish ")
        add_new_ingredient = (answer.lower() == "yes")

    recipe.preparation_steps = input("Preparation steps for " + recipe.name + ": ")

    return recipe

def write_recipes(recipes):
    print("Recipes Book")
    recipe_number = 1
    for recipe in recipes:
        print("Recipe #" + str(recipe_number))
        write_recipe(recipe)
        print("")
        recipe_number += 1

def write_menu():
    message = "Options\n\n"
    message += "1. Show all registered recipes;\n"
    message += "2. Register a new recipe;\n"
    message += "\n"
    message += "Any other value ends the program.\n"

    print(message)

def load_recipes():
    recipes = [
        Recipe("Bread",
                "...",
                [
                    Ingredient("Water", 3.0, "Cups"),
                    Ingredient("Flour", 4.0, "Cups"),
                    Ingredient("Salt", 2.0, "Tablespoons"),
                    Ingredient("Yeast", 2.0, "Teaspoons")
                ]),
        Recipe("Sweet Bread",
                "...",
                [
                    Ingredient("Water", 3.0, "Cups"),
                    Ingredient("Flour", 4.0, "Cups"),
                    Ingredient("Sugar", 2.0, "Cups"),
                    Ingredient("Salt", 2.0, "Tablespoons"),
                    Ingredient("Yeast", 2.0, "Teaspoons")
                ]),
    ]

    return recipes

def main():
    recipes = load_recipes()
    finished = False
    while (not finished):
        write_menu()
        chosen_option = input("Choose an option: ")
        if (chosen_option == "1"):
            write_recipes(recipes)
        elif (chosen_option == "2"):
            new_recipe = read_recipe()
            recipes.append(new_recipe)
        else:
            finished = True

if (__name__ == "__main__"):
    main()
function new_ingredient(name, quantity, measure_unit)
    local result = {
        name = name or "",
        quantity = quantity or 0.0,
        measure_unit = measure_unit or ""
    }

    return result
end

function new_recipe(name, preparation_steps, ingredients)
    local result = {
        name = name or "",
        preparation_steps = quantity or "",
        ingredients = ingredients or {}
    }

    return result
end

function write_ingredient(ingredient)
    print("- " .. ingredient.name .. ": " .. ingredient.quantity .. " " .. ingredient.measure_unit)
end

function read_ingredient()
    local ingredient = new_ingredient()
    io.write("Ingredient name: ")
    ingredient.name = io.read("*line")
    io.write("Quantity of " .. ingredient.name .. ": ")
    ingredient.quantity = io.read("*number", "*line")
    io.write("Measure unit: ")
    ingredient.measure_unit = io.read("*line")

    return ingredient
end

function write_recipe(recipe)
    print(recipe.name)
    print("Ingredients: ")
    for _, ingredient in ipairs(recipe.ingredients) do
        write_ingredient(ingredient)
    end

    print("Preparation steps: ")
    print(recipe.preparation_steps)
end

function read_recipe()
    local recipe = new_recipe()
    io.write("Recipe name: ")
    recipe.name = io.read("*line")
    print("Ingredients")
    local add_new_ingredient = true
    while (add_new_ingredient) do
        local ingredient = read_ingredient()
        table.insert(recipe.ingredients, ingredient)

        io.write("Do you want to add a new ingredient?\nType yes to continue, any other value to finish ")
        local answer = io.read("*line*")
        add_new_ingredient = (string.lower(answer) == "yes")
    end

    io.write("Preparation steps for " .. recipe.name .. ": ")
    recipe.preparation_steps = io.read("*line")

    return recipe
end

function write_recipes(recipes)
    print("Recipes Book")
    local recipe_number = 1
    for _, recipe in ipairs(recipes) do
        print("Recipe #" .. recipe_number)
        write_recipe(recipe)
        print("")
        recipe_number = recipe_number + 1
    end
end

function write_menu()
    local message = "Options\n\n"
    message = message .. "1. Show all registered recipes;\n"
    message = message .. "2. Register a new recipe;\n"
    message = message .. "\n"
    message = message .. "Any other value ends the program.\n"

    print(message)
end

function load_recipes()
    local recipes = {
        new_recipe("Bread",
                     "...",
                     {
                         new_ingredient("Water", 3.0, "Cups"),
                         new_ingredient("Flour", 4.0, "Cups"),
                         new_ingredient("Salt", 2.0, "Tablespoons"),
                         new_ingredient("Yeast", 2.0, "Teaspoons")
                     }),
        new_recipe("Sweet Bread",
                     "...",
                     {
                         new_ingredient("Water", 3.0, "Cups"),
                         new_ingredient("Flour", 4.0, "Cups"),
                         new_ingredient("Sugar", 2.0, "Cups"),
                         new_ingredient("Salt", 2.0, "Tablespoons"),
                         new_ingredient("Yeast", 2.0, "Teaspoons")
                     }),
    }

    return recipes
end

function main()
    local recipes = load_recipes()
    local finished = false
    while (not finished) do
        write_menu()
        io.write("Choose an option: ")
        local chosen_option = io.read("*line")
        if (chosen_option == "1") then
            write_recipes(recipes)
        elseif (chosen_option == "2") then
            local new_recipe = read_recipe()
            table.insert(recipes, new_recipe)
        else
            finished = true
        end
    end
end

main()
extends Node

func random_integer(minimum_inclusive, maximum_inclusive):
    var minimum = ceil(minimum_inclusive)
    var maximum = floor(maximum_inclusive)

    # randi(): [0.0, 1.0[
    return randi() % int(maximum + 1 - minimum) + minimum

func input(message, value_esperado):
    print(message)

    if (value_esperado == "sorteio menu"):
        randomize()
        return str(random_integer(1, 3))

    return value_esperado

class Ingredient:
    var name
    var quantity
    var measure_unit

    func _init(name = "", quantity = 0.0, measure_unit = ""):
        self.name = name
        self.quantity = quantity
        self.measure_unit = measure_unit

class Recipe:
    var name
    var preparation_steps
    var ingredients

    func _init(name = "", preparation_steps = "", ingredients = []):
        self.name = name
        self.preparation_steps = preparation_steps
        self.ingredients = ingredients

func write_ingredient(ingredient):
    print("- " + ingredient.name + ": " + str(ingredient.quantity) + " " + ingredient.measure_unit)

func read_ingredient():
    var ingredient = Ingredient.new()
    ingredient.name = input("Ingredient name: ", "flour")
    ingredient.quantity = float(input("Quantity of " + ingredient.name + ": ", "1.23"))
    ingredient.measure_unit = input("Measure unit: ", "kg")

    return ingredient

func write_recipe(recipe):
    print(recipe.name)
    print("Ingredients: ")
    for ingredient in recipe.ingredients:
        write_ingredient(ingredient)

    print("Preparation steps: ")
    print(recipe.preparation_steps)

func read_recipe():
    var recipe = Recipe.new()
    recipe.name = input("Recipe name: ", "Flour")
    print("Ingredients")
    var add_new_ingredient = true
    while (add_new_ingredient):
        var ingredient = read_ingredient()
        recipe.ingredients.append(ingredient)

        var answer = input("Do you want to add a new ingredient?\nType yes to continue, any other value to finish ", "não")
        add_new_ingredient = (answer.to_lower() == "yes")

    recipe.preparation_steps = input("Preparation steps for " + recipe.name + ": ", "Não comestível!")

    return recipe

func write_recipes(recipes):
    print("Recipes Book")
    var recipe_number = 1
    for recipe in recipes:
        print("Recipe #" + str(recipe_number))
        write_recipe(recipe)
        print("")
        recipe_number += 1

func write_menu():
    var message = "Options\n\n"
    message += "1. Show all registered recipes;\n"
    message += "2. Register a new recipe;\n"
    message += "\n"
    message += "Any other value ends the program.\n"

    print(message)

func load_recipes():
    var recipes = [
        Recipe.new("Bread",
                    "...",
                    [
                        Ingredient.new("Water", 3.0, "Cups"),
                        Ingredient.new("Flour", 4.0, "Cups"),
                        Ingredient.new("Salt", 2.0, "Tablespoons"),
                        Ingredient.new("Yeast", 2.0, "Teaspoons")
                    ]),
        Recipe.new("Sweet Bread",
                    "...",
                    [
                        Ingredient.new("Water", 3.0, "Cups"),
                        Ingredient.new("Flour", 4.0, "Cups"),
                        Ingredient.new("Sugar", 2.0, "Cups"),
                        Ingredient.new("Salt", 2.0, "Tablespoons"),
                        Ingredient.new("Yeast", 2.0, "Teaspoons")
                    ]),
    ]

    return recipes

func _ready():
    var recipes = load_recipes()
    var finished = false
    while (not finished):
        write_menu()
        var chosen_option = input("Choose an option: ", "sorteio menu")
        if (chosen_option == "1"):
            write_recipes(recipes)
        elif (chosen_option == "2"):
            var new_recipe = read_recipe()
            recipes.append(new_recipe)
        else:
            finished = true

In the version with JavaScript, alert() can be replaced with console.log() to write the result in the browser's console (instead of an alert panel). A desirable improvement when using alert() would be returning strings to show recipes in a single dialog (instead of a dialog per phrase). To do this, one could create a function ingredient_to_string() that returned the text (instead of writing it as performed by write_ingredient()). The list of ingredients as strings could be concatenated in a single message, to generate the alert with the other data from recipes. The procedure write_ingredient() could also be refactored to use the new function.

As GDScript does not have subroutines for console (terminal) input, the implementation defines an input() function with a second parameter containing a value expected as the return. To use the main menu, the function returns a pseudorandom value between 1 and 3, simulating the choice of an option. Thus, the result can be different every use of the program. Such technique can serve as a useful tool to automate testing, as a way to simulate end user input in a program. For tests, instead of random values, one could define an array with a sequence of inputs (and return the value of the next position at every request). Besides, at this point it already is possible to start implementing graphical interfaces using Godot Engine. They will be explored in future topics, possibly after a brief introduction to OOP. If you want to try yourself, you could follow the steps defined in setup of the development environment for GDScript (Godot) to start with simpler forms than the one in this example.

Ceasing the digression, the resulting program for recipes is modular, organized and easy to read. The names of the subroutines describe what they do, making it simpler to read the program's code. For instance, when one reads main() (created as the main program), she/he can quickly understand what the program does:

  • Initialize recipes;
  • Write a menu with options;
  • Request the input of an option:
    1. Write saved recipes;
    2. Add a new recipe;
    3. (Or any other value) Ends the program.

It is also easy to modify and extend the program, because it suffices to add or remove features from the respective subroutines (or create new options for the menu). For instance, to add data validation when reading an ingredient, one could change the function read_ingredient(). After the introduction to files, it would also be possible to save and load recipes stored in files to persist data among several use sessions of the program. Thus, it would become unnecessary presetting recipes in the source code of the program.

As one acquires programming experience, she/he must start thinking about software architectures to organize and structure programs. Good architectures make it easier to implement and maintaining systems.

For instance, it is a good programming practice to separate data input and output features from data modeling and data processing. This can be achieved, among other options, using a three-tier architecture, a multitier architecture, or the software pattern Model-View-Controller (MVC). The next section provides an example to implement a simulation. Although the concept of layers is normally associated to OOP, it can be explored in a procedural (or functional) way.

Conway's Game Of Life

To illustrate how to separate the logic a program from its presentation, the next example provides an implementation for an algorithm called Conway's Game of Life. The Game of Live is a simple example of cellular automaton. It will be the first example in this material presenting a simulation instead of a static output (consequently, the program will be inaccessible for non-sighted people).

The following quote presents the rules of the Game of Life defined in the Wikipedia's entry:

  1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
  2. Any live cell with two or three live neighbours lives on to the next generation.
  3. Any live cell with more than three live neighbours dies, as if by overpopulation.
  4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

To implement the game, a matrix is defined as a grid, with values as cells. To implement the results, the system should check the neighbours (table) cells at every position of the matrix. Value comparison is performed using the indices of the matrix. For instance, m[line][column], m[line][column + 1] corresponds to the cell in the next column.

Each position can have up to 8 neighbours (case of the 5 in the next table), though there can exist less. For instance, in the extremities of the table, the cell can have only 3 neighbours (case of 1, 3, 7 and 9 in the following example).

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

A way to make it easier to solve the problem is inserting additional lines and columns before the first and after the last ones.

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

With this modification, all cells with data will have 8 neighbours. This makes the implementation easier because it is possible to check all adjacent cells without worrying with corner cases. In other words, it is possible to compare all neighbours the same way, for all valid values behave like the 5 in the previous table. This can be easier to understand with offsets from the central value:

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

The central element (the pair line 0, column 0, that is, (0, 0)) is ignored, as a cell cannot be neighbour of itself. The other offsets can be implemented as indices in the matrix.

grid[line - 1][column - 1]
grid[line - 1][column]
grid[line - 1][column + 1]
grid[line][column - 1]
grid[line][column + 1]
grid[line + 1][column - 1]
grid[line + 1][column]
grid[line + 1][column + 1]

One can apply the previous values directly in the solution or write two nested loops to calculate the indices.

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

Both solutions are valid. The first is simpler; the second is more generic and would make it easier to modify the solution if one wished to verify for more distance cells. With the previous information, all that remains is implementing the rules.

const GRID_LINES = 20
const GRID_COLUMNS = 20

const DEAD_CELL = 0
const ALIVE_CELL = 1

function random_integer(minimum_inclusive, maximum_inclusive) {
    let minimum = Math.ceil(minimum_inclusive)
    let maximum = Math.floor(maximum_inclusive)

    return Math.floor(minimum + Math.random() * (maximum + 1 - minimum))
}

class Grid {
    constructor(lines = GRID_LINES, columns = GRID_COLUMNS) {
        this.lines = lines
        this.columns = columns
        this.values = []
        lines += 2
        columns += 2
        for (let line = 0; line < lines; ++line) {
            let new_column = []
            this.values.push(new_column)
            for (let column = 0; column < columns; ++column) {
                new_column.push(DEAD_CELL)
            }
        }
    }
}

function initialize_grid(grid) {
    let lines = grid.lines + 1
    let columns = grid.columns + 1
    for (let line = 1; line < lines; ++line) {
        for (let column = 1; column < columns; ++column) {
            if (random_integer(0, 100) < 30) {
                grid.values[line][column] = ALIVE_CELL
            } else {
                grid.values[line][column] = DEAD_CELL
            }
        }
    }
}

function update_grid(grid) {
    let lines = grid.lines + 1
    let columns = grid.columns + 1
    // Reference.
    let values = grid.values

    // Count of alive neighbours.
    let count_grid = new Grid(grid.lines, grid.columns)
    let count_values = count_grid.values
    for (let line = 1; line < lines; ++line) {
        for (let column = 1; column < columns; ++column) {
            let alive_neighbours = 0
            for (let neighbour_line = -1; neighbour_line < 2; ++neighbour_line) {
                for (let neighbour_column = -1; neighbour_column < 2; ++neighbour_column) {
                    if (!((neighbour_line === 0) && (neighbour_column === 0))) {
                        if (values[line + neighbour_line][column + neighbour_column] === ALIVE_CELL) {
                            ++alive_neighbours
                        }
                    }
                }
            }

            count_values[line][column] = alive_neighbours
        }
    }

    // State update based on the neightbor count.
    for (let line = 1; line < lines; ++line) {
        for (let column = 1; column < columns; ++column) {
            if (values[line][column] === ALIVE_CELL) {
                if (count_values[line][column] < 2) {
                    // Any live cell with fewer than two live neighbours
                    // dies, as if by underpopulation.
                    values[line][column] = DEAD_CELL
                } else if (count_values[line][column] > 3) {
                    // Any live cell with more than three live neighbours
                    // dies, as if by overpopulation.
                    values[line][column] = DEAD_CELL
                } /* else {
                    // Redundant, as the change is in-place.
                    // Any live cell with two or three live neighbours
                    // lives on to the next generation.
                    values[line][column] = values[line][column]
                } */
            } else {
                if (count_values[line][column] === 3) {
                    // Any dead cell with exactly three live neighbours
                    // becomes a live cell, as if by reproduction.
                    values[line][column] = ALIVE_CELL
                }
            }
        }
    }
}

function draw_grid(grid) {
    let lines = grid.lines + 1
    let columns = grid.columns + 1
    let message = ""
    for (let line = 1; line < lines; ++line) {
        for (let column = 1; column < columns; ++column) {
            if (grid.values[line][column] === ALIVE_CELL) {
                message += "⏹" // Or "X" or "*" (any character).
            } else {
                message += " "
            }
        }

        message += "\n"
    }

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

async function main() {
    let grid = new Grid()
    initialize_grid(grid)
    let number_of_iterations = 100
    for (let iteration = 0; iteration < number_of_iterations; ++iteration) {
        update_grid(grid)
        draw_grid(grid)
        // sleep(): Wait 300ms before the next update.
        await new Promise(resolve => setTimeout(resolve, 300))
    }
}

main()
import random
import time
from typing import Final

GRID_LINES: Final = 20
GRID_COLUMNS: Final = 20

DEAD_CELL: Final = 0
ALIVE_CELL: Final = 1

def clear_console(number_of_lines = GRID_LINES):
    for counter in range(number_of_lines):
        print("\n")

class Grid:
    def __init__(self, lines = GRID_LINES, columns = GRID_COLUMNS):
        self.lines = lines
        self.columns = columns
        self.values = []
        lines += 2
        columns += 2
        for line in range(lines):
            new_column = []
            self.values.append(new_column)
            for column in range(columns):
                new_column.append(DEAD_CELL)

def initialize_grid(grid):
    lines = grid.lines + 1
    columns = grid.columns + 1
    for line in range(1, lines):
        for column in range(1, columns):
            if (random.randint(0, 100) < 30):
                grid.values[line][column] = ALIVE_CELL
            else:
                grid.values[line][column] = DEAD_CELL

def update_grid(grid):
    lines = grid.lines + 1
    columns = grid.columns + 1
    # Reference.
    values = grid.values

    # Count of alive neighbours.
    count_grid = Grid(grid.lines, grid.columns)
    count_values = count_grid.values
    for line in range(1, lines):
        for column in range(1, columns):
            alive_neighbours = 0
            for neighbour_line in range(-1, 2):
                for neighbour_column in range(-1, 2):
                    if (not ((neighbour_line == 0) and (neighbour_column == 0))):
                        if (values[line + neighbour_line][column + neighbour_column] == ALIVE_CELL):
                            alive_neighbours += 1

            count_values[line][column] = alive_neighbours

    # State update based on the neightbor count.
    for line in range(1, lines):
        for column in range(1, columns):
            if (values[line][column] == ALIVE_CELL):
                if (count_values[line][column] < 2):
                    # Any live cell with fewer than two live neighbours
                    # dies, as if by underpopulation.
                    values[line][column] = DEAD_CELL
                elif (count_values[line][column] > 3):
                    # Any live cell with more than three live neighbours
                    # dies, as if by overpopulation.
                    values[line][column] = DEAD_CELL
                # else:
                    # Redundant, as the change is in-place.
                    # Any live cell with two or three live neighbours
                    # lives on to the next generation.
                    # values[line][column] = values[line][column]
            else:
                if (count_values[line][column] == 3):
                    # Any dead cell with exactly three live neighbours
                    # becomes a live cell, as if by reproduction.
                    values[line][column] = ALIVE_CELL

def draw_grid(grid):
    lines = grid.lines + 1
    columns = grid.columns + 1
    message = ""
    for line in range(1, lines):
        for column in range(1, columns):
            if (grid.values[line][column] == ALIVE_CELL):
                message += "⏹" # Or "X" or "*" (any character).
            else:
                message += " "

        message += "\n"

    clear_console()
    print(message)

def main():
    random.seed()
    grid = Grid()
    initialize_grid(grid)
    number_of_iterations = 100
    for iteration in range(number_of_iterations):
        update_grid(grid)
        draw_grid(grid)
        # sleep(): Wait 300ms before the next update.
        time.sleep(0.3)

if (__name__ == "__main__"):
    main()
local GRID_LINES = 20
local GRID_COLUMNS = 20

local DEAD_CELL = 0
local ALIVE_CELL = 1

-- Inefficient implementation using busy wait.
function sleep(time_seconds)
    local finished = tonumber(os.clock() + time_seconds);
    while (os.clock() < finished) do
        -- Wait the passage of time, wasting cycles of the processor.
    end
end

function clear_console(number_of_lines)
    number_of_lines = number_of_lines or GRID_LINES
    for counter = 1, number_of_lines do
        print("\n")
    end
end

function new_grid(lines, columns)
    lines = lines or GRID_LINES
    columns = columns or GRID_COLUMNS
    local result = {
        lines = lines,
        columns = columns,
        values = {}
    }

    lines = lines + 2
    columns = columns + 2
    for line = 2, lines do
        local new_column = {}
        table.insert(result.values, new_column)
        for column = 2, columns do
            table.insert(new_column, DEAD_CELL)
        end
    end

    return result
end

function initialize_grid(grid)
    local lines = grid.lines
    local columns = grid.columns
    for line = 2, lines do
        for column = 2, columns do
            if (math.random(0, 100) < 30) then
                grid.values[line][column] = ALIVE_CELL
            else
                grid.values[line][column] = DEAD_CELL
            end
        end
    end
end

function update_grid(grid)
    local lines = grid.lines
    local columns = grid.columns
    -- Reference.
    local values = grid.values

    -- Count of alive neighbours.
    local count_grid = new_grid(grid.lines, grid.columns)
    local count_values = count_grid.values
    for line = 2, lines do
        for column = 2, columns do
            local alive_neighbours = 0
            for neighbour_line = -1, 1 do
                for neighbour_column = -1, 1 do
                    if (not ((neighbour_line == 0) and (neighbour_column == 0))) then
                        if (values[line + neighbour_line][column + neighbour_column] == ALIVE_CELL) then
                            alive_neighbours = alive_neighbours + 1
                        end
                    end
                end
            end

            count_values[line][column] = alive_neighbours
        end
    end

    -- State update based on the neightbor count.
    for line = 2, lines do
        for column = 2, columns do
            if (values[line][column] == ALIVE_CELL) then
                if (count_values[line][column] < 2) then
                    -- Any live cell with fewer than two live neighbours
                    -- dies, as if by underpopulation.
                    values[line][column] = DEAD_CELL
                elseif (count_values[line][column] > 3) then
                    -- Any live cell with more than three live neighbours
                    -- dies, as if by overpopulation.
                    values[line][column] = DEAD_CELL
                -- else:
                    -- Redundant, as the change is in-place.
                    -- Any live cell with two or three live neighbours
                    -- lives on to the next generation.
                    -- values[line][column] = values[line][column]
                end
            else
                if (count_values[line][column] == 3) then
                    -- Any dead cell with exactly three live neighbours
                    -- becomes a live cell, as if by reproduction.
                    values[line][column] = ALIVE_CELL
                end
            end
        end
    end
end

function draw_grid(grid)
    local lines = grid.lines
    local columns = grid.columns
    local message = ""
    for line = 2, lines do
        for column = 2, columns do
            if (grid.values[line][column] == ALIVE_CELL) then
                message = message .. "⏹" -- Or "X" or "*" (any character).
            else
                message = message .. " "
            end
        end

        message = message .. "\n"
    end

    clear_console()
    print(message)
end

function main()
    math.randomseed(os.time())
    local grid = new_grid()
    initialize_grid(grid)
    local number_of_iterations = 100
    for iteration = 1, number_of_iterations do
        update_grid(grid)
        draw_grid(grid)
        -- sleep(): Wait 300ms before the next update.
        sleep(0.3)
    end
end

main()
extends Node

const GRID_LINES = 20
const GRID_COLUMNS = 20

const DEAD_CELL = 0
const ALIVE_CELL = 1

func random_integer(minimum_inclusive, maximum_inclusive):
    var minimum = ceil(minimum_inclusive)
    var maximum = floor(maximum_inclusive)

    # randi(): [0.0, 1.0[
    return randi() % int(maximum + 1 - minimum) + minimum

func clear_console(number_of_lines = GRID_LINES):
    for counter in range(number_of_lines):
        print("\n")

class Grid:
    var lines
    var columns
    var values

    func _init(lines = GRID_LINES, columns = GRID_COLUMNS):
        self.lines = lines
        self.columns = columns
        self.values = []
        lines += 2
        columns += 2
        for line in range(lines):
            var new_column = []
            self.values.append(new_column)
            for column in range(columns):
                new_column.append(DEAD_CELL)

func initialize_grid(grid):
    var lines = grid.lines + 1
    var columns = grid.columns + 1
    for line in range(1, lines):
        for column in range(1, columns):
            if (random_integer(0, 100) < 30):
                grid.values[line][column] = ALIVE_CELL
            else:
                grid.values[line][column] = DEAD_CELL

func update_grid(grid):
    var lines = grid.lines + 1
    var columns = grid.columns + 1
    # Reference.
    var values = grid.values

    # Count of alive neighbours.
    var count_grid = Grid.new(grid.lines, grid.columns)
    var count_values = count_grid.values
    for line in range(1, lines):
        for column in range(1, columns):
            var alive_neighbours = 0
            for neighbour_line in range(-1, 2):
                for neighbour_column in range(-1, 2):
                    if (not ((neighbour_line == 0) and (neighbour_column == 0))):
                        if (values[line + neighbour_line][column + neighbour_column] == ALIVE_CELL):
                            alive_neighbours += 1

            count_values[line][column] = alive_neighbours

    # State update based on the neightbor count.
    for line in range(1, lines):
        for column in range(1, columns):
            if (values[line][column] == ALIVE_CELL):
                if (count_values[line][column] < 2):
                    # Any live cell with fewer than two live neighbours
                    # dies, as if by underpopulation.
                    values[line][column] = DEAD_CELL
                elif (count_values[line][column] > 3):
                    # Any live cell with more than three live neighbours
                    # dies, as if by overpopulation.
                    values[line][column] = DEAD_CELL
                # else:
                    # Redundant, as the change is in-place.
                    # Any live cell with two or three live neighbours
                    # lives on to the next generation.
                    # values[line][column] = values[line][column]
            else:
                if (count_values[line][column] == 3):
                    # Any dead cell with exactly three live neighbours
                    # becomes a live cell, as if by reproduction.
                    values[line][column] = ALIVE_CELL

func draw_grid(grid):
    var lines = grid.lines + 1
    var columns = grid.columns + 1
    var message = ""
    for line in range(1, lines):
        for column in range(1, columns):
            if (grid.values[line][column] == ALIVE_CELL):
                message += "X" # Or "X" or "*" (any character).
            else:
                message += " "

        message += "\n"

    clear_console()
    print(message)

func _ready():
    randomize()
    var grid = Grid.new()
    initialize_grid(grid)
    var number_of_iterations = 100
    for iteration in range(number_of_iterations):
        update_grid(grid)
        draw_grid(grid)
        # sleep(): Wait 300ms before the next update.
        yield(get_tree().create_timer(0.3), "timeout")

For the example, it is convenient to start by noting the solution structuring. Every change of the program's logic is performed at update_grid(). Similarly, the presentation of the grid is defined in draw_grid(). The program does not read input from end-users; if it required input, one could define a new subroutine called read_input(). The read values would, then, be passed as parameters to update_grid() (thus, updating the program logic would not read input data directly).

Furthermore, this program introduces some new features used for the frame to frame animation.

  1. A subroutine to clear the console(terminal). JavaScript provides console.clear() (documentation) to clean it. In Python, Lua and GDScript, the procedure clear_console() write empty lines to simulate an alternative. It could be worth writing more lines for a better result;
  2. A subroutine to wait (pause) the program for a given time, normally called sleep(). JavaScript uses a Promise (documentation) and setTimeout() (documentation). In JavaScript, it is also worth noticing that main() must be declared async for the use of the previous subroutines. Python provides time.sleep() (documentation). GDScript provides get_tree().create_timer() (documentation). Lua does not provide a predefined subroutine; the implementation of sleep() uses a technique called busy wait. The technique repeats a code block until a condition becomes false, to waste time. In this case, this is used to wait a certain time until ending the repetition. A simpler implementation could use an empty block that was repeated a few thousand times.

In the implementations, line and column increments allow ignoring empty values when drawing or updating the grid. Instead of adding lines = grid.lines + 2 to use a condition lines - 1, one can make the code simpler with grid.lines + 1 directly. In Lua, as the indexing starts from 1, the original value can be used directly.

Depending on the initial sequence that is generated, the Game of Life can generate patterns that repeat forever. The Wikipedia page describes some patterns. If you run the program a few times, you may find such patterns (depending on your luck). Alternatively, you can create a starting board as with a suitable configuration to the pattern that you wish to use.

For instance, for a grid 48x48 (50x50 with zeros padding the extremities):

Hide/Show Board

For better visualization, it is advisable to use a character such as X instead of the square (to guarantee equal spacing between characters).

The example provides the configuration for JavaScript, though the principles (and values) are the same for the other languages. In Lua, square brackets should be replaced by curly ones.

const GRID_LINES = 48
const GRID_COLUMNS = 48

const DEAD_CELL = 0
const ALIVE_CELL = 1

function initialize_grid(grid) {
    grid.values = [
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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