Object oriented progamming : advanced concepts

By Samir Tine, published on January 2023

Destructors

You probably remember the constructor concept, which allows you to initialize an instance with specific attributes/methods/properties. The concept of destructor is the same, but relates to when the instance is destroyed in memory :

-- Defines an INIFile object that inherits from sys.File INIFile = Object(sys.File) -- Adds a constructor function INIFile:constructor(filename) -- Adds an instance field "file" self.file = sys.File(filename) -- Checks that its an ini file if self.file.extension ~= ".ini" then error("INIFile constructor expects a INI file") end -- Opens the ini file for reading self.file:open() print("INIFile instance constructed") end -- Adds a destructor function INIFile:destructor() print("INIFile instance destructed") self.file:close() end -- Creates a new INIFile instance local config = INIFile("config.ini")

Once the program is finished, the garbage collector will do the necessary to erase all objects created in memory and you will see the message "INIFIle object destroyed". Using a destructor is simple as adding a destructor() method when defining an object. Destructors are very handy for finalizing/freeing up resources once the object is no longer in use. But be careful, we don't control when the destructor will be called by the garbage collector.

-- Creates a new INIFile instance local config = INIFile("config.ini") -- Read the entire file content using the File:read() method (inherited) local content = config:read() -- set config to nil to destruct the INIFile instance config = nil print("End of program") -- The destructor is not called before the "end of the program" message !

Setting to nil and instance or going out from scope (using a local instance), do not call the instance destructor method immediately. Because Luart uses a garbage collector, the destructor is called only when a new garbage collection cycle is started. You can however force garbage collection if needed, to ensure that a destructor is immediately called upon instance destruction :

-- Creates a new INIFile instance local config = INIFile("config.ini") -- Read the entire file content using the File:read() method (inherited) local content = config:read() -- set config to nil to destruct the INIFile instance config = nil -- forces a garbage colleciton cycle.. collectgarbage() -- ..that will call this time the INIFile destructor before "End of program" message print("End of program")

Iterators

The iterator construct allows you to traverse elements of a container or collection with for..in..do instructions. In Lua, these collections are often tables, which are used to construct arrays and other data structures. Collections can also be represented by objects in Luart. An iterator can iterate over an instance that represents a collection. Our object definition only needs to include a new behaviour/method to use such functionality :

-- Defines a BookStore object BookStore = Object {} -- Adds a constructor function BookStore:constructor(books) -- Adds an iterator function function self.iterator(index) -- index is set to nil when the iteration begins index = index or 1 -- the new index value should be returned as the last value return books[index], index+1 end end -- Creates a new BookStore instance local bookstore = BookStore { "Romeo and Juliet", "Of mice and men", "The Wizard of Oz" } -- prints the books in the bookstore using a for loop for i, book in bookstore do print("Title : "..i.." "..book) end

This example illustrates a BookStore instance that defines an iterator using the provided books table in the instance constructor. An iterator method has a single parameter that represent the current indexed value in the loop. During the first iteration, this parameter is set to nil. Once the value associated with the requested index has been calculated, that value is returned, followed by the next index (which will be used during the next iteration/call of the iterator method). The iteration stops when the nil value is returned. Note that an iterator method can return more than one value :

function BookStore:constructor(books) -- Adds an iterator function function self.iterator(index)local Iterable = { items = {}, iterator = function (self, index) -- index is set to nil when the iteration begins -- the new index value should be returned as the last value local index, value = next(self.items, index) return index, value, index end } -- Defines a BookStore object BookStore = Object({}, Iterable) -- Adds a constructor function BookStore:constructor(books) self.items = books end -- Creates a new BookStore instance local bookstore = BookStore { "Romeo and Juliet", "Of mice and men", "The Wizard of Oz" } -- prints the books in the bookstore using a for loop for i, book in bookstore do print("Book #"..i..": "..book) end -- index is set to nil when the iteration begins index = index or 1 -- the new index value should be returned as the last value -- if the value is not nil, return the index and the associated value local value = books[index] return value and index or nil, value, index+1 end end -- Creates a new BookStore instance local bookstore = BookStore { "Romeo and Juliet", "Of mice and men", "The Wizard of Oz" } -- prints the index and the books in the bookstore using a for loop for i, book in bookstore do print("Book #"..i..": "..book) end

Iterable mixin

Now we will use two concepts seen previously: mixins and iterators. We will try to define a behavior called Iterable which can be used on any object. Let's do it :

-- Defines an "Iterable" table local Iterable = { -- Adds an "items" field items = {}, -- Adds an iterator method iterator = function (self, index) local index, value = next(self.items, index) return index, value, index end } -- Defines a BookStore object and use the previous Iterable table as mixin BookStore = Object({}, Iterable) -- Adds a constructor function BookStore:constructor(books) self.items = books end -- Creates a new BookStore instance local bookstore = BookStore { "Romeo and Juliet", "Of mice and men", "The Wizard of Oz" } -- prints the books in the bookstore using a for loop -- the "iterator" method from the mixin will be used for i, book in bookstore do print("Book #"..i..": "..book) end

Very interesting isn't it ? But there's a problem, at any time someone can change the self.items field... Let's protect this field by defining a readonly property :

-- Defines an "Iterable" table local Iterable = { -- Adds an iterator method iterator = function (self, index) local index, value = next(self.items or error("'items' field not defined"), index) return index, value, index end } -- Defines a BookStore object and use the previous Iterable table as mixin BookStore = Object({}, Iterable) -- Adds a constructor function BookStore:constructor(books) -- self.items is now a readonly property function self:get_items() return books end end -- Creates a new BookStore instance local bookstore = BookStore { "Romeo and Juliet", "Of mice and men", "The Wizard of Oz" } -- prints the books in the bookstore using a for loop -- the "iterator" method from the mixin will be used for i, book in bookstore do print("Book #"..i..": "..book) end

In summary, object oriented programming with Luart offers very advanced programming paradigms by pooling the different concepts discussed here: properties, mixins, constructors, iterators...

This tutorial on object programming with Luart has now come to an end. I hope you have found it useful. Please feel free to ask for help on the Luart community forum if you have any questions.