DEV Community

Alex Esoposting
Alex Esoposting

Posted on

Factor pt. 3 - Tuple classes

From my previous tutorials you should already know how to operate with values on the stack and use sequences and combinators to make control structures. Data types paid a crucial role in both of these articles and are very important all over Factor. Today I will explain how Factor class system works based on tuple classes.

Object oriented programming

What I know about it

As far as I know OOP has been the most popular programming paradigm for some time. It's sometimes presented in opposition to Functional Programming, but although both of them are called "paradigms" they touch very different aspects of programming and can easily coexist. Factor manages to be both an object oriented and a concatenative language.

In OOP programs are based on the concept of "objects" that contain data (called fields) and code (called methods). These objects interact with each other and create new objects to perform required tasks. In most languages, including Factor, each object is an instance of a class which tells us its type, what data it holds and what we can do with it. For example 1 is an instance of an integer class. It represents a number and arithmetic operations can be performed on it. [ dup * ] is an instance of quotation. It holds a snippet of code and can be passed as an argument to combinators.

The thing that makes OOP so useful is that a program may define new classes with their own fields and methods. In Factor this is done using class tuples.

Defining a class

Just fields for now

Just as with most definitions in Factor a syntax word is used to create new tuple classes: TUPLE:. It's syntax is:

TUPLE: class-name slots... ;
Enter fullscreen mode Exit fullscreen mode

Slots define fields of a class and can take one of the following forms:

  • name - A slot that can hold any object
  • { name attributes... } - A slot that can hold any object with optional attributes
  • { name class attributes... } - A slot that can hold an instance of the class with optional attributes

All this can sound very confusing, so let's look at an example. Let's define a class for books. Let's say each book shall hold information about it's title, author, genre, and how many times it has been borrowed. A tuple class for such books would be defined like that:

TUPLE: book
    { title string read-only }
    { author string read-only }
    { genre string read-only }
    { times-borrowed integer initial: 0 }
Enter fullscreen mode Exit fullscreen mode

Each slot of this tuple can only store a particular type of data. It may seem like an unnecessary restriction, but it makes code more readable and can prevent some mistakes later on in the program. Three slots have a read-only attribute which means once they are set they can't be changed. The last slot has an initial: attribute which specifies what value it should have at the start

Instantiating classes

Making actual objects holding data

There are two basic constructor words in Factor that let you create class instances. The simpler one, new ( class -- tuple ), only takes a class as an argument and creates a new tuple with default slot values. Continuing the book example this is how to create a default book:

book new
! S: T{ book f "" "" "" 0 }
Enter fullscreen mode Exit fullscreen mode

This book's prettyprint shows three empty strings and a zero respectively. To actually insert some information into that book we need to use the other constructor: boa ( slots... class -- tuple ). In addition to a class it requires values to be put into the slots by order of arguments - hence the name. For example to describe the toki pona dictionary one would write:

"Toki Pona Dictionary" "Sonja Lang" "Dictionary" 1 book boa
! S: T{ book f "Toki Pona Dictionary" "Sonja Lang" "Dictionary" 1 }
Enter fullscreen mode Exit fullscreen mode

This time you can see contents of each of the slots initialised to a proper value. Disregard the f between the class name and the title: it's just a quirk of Factor literal tuple syntax. For more info visit \ T{ help.

Getters and setters

Working with objects

It is useful to change values of slots of a tuple after we created it. In our example that would be incrementing the times-borrowed slot whenever someone borrows a book. It would also be nice to extract values from the object to use in the program, that's what they are there for after all. To facilitate that each class in factor comes with a set of getter and setter words.

Getters extract values of slots from an object. They are defined automatically based on object's slot names. They consume the object and return the value of a corresponding slot.

slot-name>> ( object -- value )
Enter fullscreen mode Exit fullscreen mode

For example to get a book's title you would use a word title>>.

Setters (as the name suggests) set a slot to a given value. They are also defined by the system for each class but only for writeable slots: those without the read-only attribute. They consume an object and a value and return the same object.

>>slot-name ( object value -- object )
Enter fullscreen mode Exit fullscreen mode

The only writeable slot in our example is times-borrowed, and its setter is >>times-borrowed.

Setters are actually a wrapper over writers: lower level words that consume a value and an object and return nothing. They aren't commonly used in programs because it's usually more convenient to keep the object on the stack.

slot-name<< ( value object -- )
Enter fullscreen mode Exit fullscreen mode

There is one more group of accessor words: changers. They take a quotation and an object and apply the quotation to the slot updating it.

change-slot-name ( object quot -- object )
Enter fullscreen mode Exit fullscreen mode

A change-times-borrowed changer will be very useful to update the counter whenever a book is borrowed.

It is important to remember that most of the time if an object is copied (for example by dup) it is actually still the same object existing on the stack twice. If its slots are changed they are changed for all copies. This is called a "shallow copy" and it is common among high level languages. For those of you familiar with C++ it's comparable to copying a pointer.


How to actually instantiate the class

Using only new and boa can get tedious, especially if data needs some processing before being put into slots or if you need some slots to have default initial values. In the book example whenever I get a new book it's times-borrowed value is always 0. Based on the premise that every time some code is about to be repeated it should be factored out I should write a better constructing word for my book class.

By convention constructors that create an instance of a class are called <class-name>. I want my constructor to have a stack effect of ( title author genre -- book ). A constructor word can be defined using new or boa, but in our case new is not useful: it will set read-only slots to their default values before my defined constructor can affect them. In this case a constructor using boa is quite simple: it only needs to add a default 0 and the class.

: <book> ( title author genre -- book )
    0 book boa
Enter fullscreen mode Exit fullscreen mode

There is another constructor naming convention used in Factor: new-class-name. These constructors, apart from usual arguments, also take a class as an argument. They are mostly useful when subclassing which is out of scope of this tutorial so I won't be presenting an example.

Defining methods

How to make the same word do different things

Usefulness of methods in OOP comes from the fact that you don't need to know anything about the object except that it implements a particular method to use that method. For example if a program is to do something based on length of an object it can call a length word on it, not matter if it's an array, a quotation, a linked list or a vector. Calculating length works differently for each of these classes, but it is always invoked by the same name.

Usually this is implemented using the dot operator: class.method(). In Factor however there are no operators, only words. Instead of the dot operator Factor allows to define generic words: words that first check what type are their arguments and execute different functions associated with that type. I've already shown examples of generic words: all accessors are generic and they will work on any object that has slots of a certain name.

A generic words are declared with syntax words GENERIC: and GENERIC#:. The first one creates words that check the type of the top value on the stack. The other allows to specify which value to check. Their syntax is:

GENERIC: word ( stack -- effect )
GENERIC#: word n ( stack -- effect )
Enter fullscreen mode Exit fullscreen mode

Notice that generic words have their stack effects defined at declaration. Each method connected to the same generic word must have the same stack effect. What is not declared is the body of the generic word. There will be multiple bodies defined for each class separately.

To attach a method to a generic word we need a syntax word M: with syntax:

M: class generic definition... ;
Enter fullscreen mode Exit fullscreen mode

Stack effect of the word were declared with the word itself. This only serves to attach a method to a word and a class.

As an example let's define a method that would be called whenever someone borrows a book. It needs to take a book as input, increase its times-borrowed by one and not return anything. It's declaration and definition will look like:

GENERIC: borrow ( object -- )

M: book borrow
    [ 1 + ] change-times-borrowed drop
Enter fullscreen mode Exit fullscreen mode

For now borrow only has effect on book objects, but it could be implemented for other classes in ways that make sense for these classes.


The tip of the iceberg

What I explained barely scratches the surface of what Factor can do with classes. It is heavily based on them and even though you can do without this knowledge you will be using classes without knowing. This article hopefully shown you enough that you will be able to noticed Object Oriented features and start using them in your own programs. Next time I will be explaining vocabularies.

Discussion (0)