DEV Community

Alex Esoposting
Alex Esoposting

Posted on • Updated on

Factor pt. 4 - Vocabularies

Over the last three tutorials I dumped a lot of information at you, explained it semi-coherently and only gave a couple practical examples. Now is the time to fix this all at once: this article is all about a big practical example that will touch on everything I have already covered and expand on it a bit. Today we are writing our own Factor vocabulary.

What is a vocabulary?

A bit of theory before the practice

In most languages only the most essential part of available functionality is present by default and if you need any specialised functions or objects you need to import or #include or whatever your favourite language does to load features from an external file. Most languages call those extensions "libraries" or "packages", but because in Factor everything is a word these files are just lists of words: vocabularies.

If you followed the examples I gave on your computer you may have already used vocabularies. Whenever the Listener encounters a word that is in an unloaded vocabulary it will throw an error and present options, one of which will be to use the library including that word. It's useful to not load all libraries by default because it would take up a lot of space to compile all Factor libraries, and some of them use conflicting word names (for example range is in math.ranges, math.statistics and model.range).

You can order Factor to load vocabularies with the words USE: and USING:. Their syntax is as follows:

USE: vocab-name
USING: vocab-names... ;
Enter fullscreen mode Exit fullscreen mode

Usually a Factor program will have a big USING: line at its start that loads all the vocabularies it needs. Words you define in the Listener go into the scratchpad vocabulary which exists as long as the Listener is open.

Creating the vocabulary

Let's get down to business

A user created vocabulary usually sits in the "work" folder in your Factor installation directory. Vocabularies are stored in plaintext source files so in theory you could just find the folder and create the required structure yourself, but Factor conveniently has a vocabulary for defining vocabularies so it can do the hard work for us.

Before we do anything we need to USE: tools.scaffold because this vocabulary will not be automatically suggested by the Listener. I will be making a library-managing vocabulary, so I will call it "esoposting-library". To scaffold a library push it's name as a string onto the stack and call scaffold-work. Now you can do it again, but call edit instead and we will be ready to start.

In summary this is what you should type into the listener to create and edit your vocabulary:

USE: tools.scaffold
"esoposting-library" scaffold-work
"esoposting-library" edit
Enter fullscreen mode Exit fullscreen mode

At this point Factor may ask you for your editor of choice which should launch immediately, provided it is installed. The initialised file should look like this:

! Copyright (C) 2021 Your name.
! See http://factorcode.org/license.txt for BSD license.
USING: ;
IN: esoposting-library
Enter fullscreen mode Exit fullscreen mode

Feel free to substitute your actual name for "Your name" in the header.

Designing the vocabulary

Design first, code second.

Before coding it's good to have an idea what the code is for and how it should work. My idea is a program to keep track of my personal library. It should support adding new books and removing old ones, searching for the book by title, and borrowing books: keeping track of whether a book is currently borrowed and to whom and how many times it has been borrowed. I should also be able to see a list of books I own in alphabetical order.

A more serious approach would look at Factor's database support, but I'll settle on a data structure holding the books that will sit atop the stack. Book management words will act on that object and if I choose the structure well they should mostly turn out to be aliases of already existing words. Because I want the books to be searchable by title I will implement the library with a hashtable.

Classes

What objects do we need?

From the problem specification two object types are apparent: books and the library containing them. Let's start from the simpler one.

Book

What information should a book carry? From what was explained earlier we need to know a book's title, how many times it was borrowed, is it borrowed now and if so then who borrowed it. In addition to that I want know the book's author and genre. That would give us 6 fields for class book.

Not all of these have to be slots of the book tuple though. Info on whether the book is borrowed and by whom could be stored by having a false value when the book is in the library and a string with the borrower's name if it's out. That is actually its own type of data called "maybe string". Additionally the title will be stored as a key to the library hashtable, but I want the redundancy of keeping it in the tuple as well. That way every word that uses a book can automatically use its title.

The final book tuple class declaration looks like this:

USING: classes.maybe math strings ;
TUPLE: book
    { title string read-only }
    { author string read-only }
    { genre string read-only }
    { times-borrowed integer }
    { borrower maybe{ string } }
;
Enter fullscreen mode Exit fullscreen mode

A "maybe string" class in Factor is written as maybe{ string } and that's the type of the borrower slot. Also note that we already need to use some vocabularies. All of them should be gathered in the third line of the generated vocabulary header. To check if everything works correctly call "esoposting-library" reload in the listener.

Library

The library object doesn't need to be anything more than a collection of books searchable by the title. We can get away with not implementing a separate class for this purpose because a hashtable does exactly what we want. A drawback of not declaring a new class is that words we define for working with the library will also attempt to work with other objects if used incorrectly which can lead to hidden bugs and unhelpful error messages.

In our case the project is too small for that to matter. If our vocabulary was supposed to be a part of some bigger application it would be a good practice to define a library class tuple, but it would be an overkill for this purpose.

Words

A top-down approach

I have already mentioned in a previous article that when writing code I like to start with the top level words I need and see what other words will be necessary to implement them. For our application there will be a few of those.

Creating the library

First the user needs to be able to init-library to get the library object and not need to care about how it's implemented:

USE: kernel
: init-library ( -- library )
    H{ } clone
;
Enter fullscreen mode Exit fullscreen mode

H{ } is syntax for a default empty hashtable, and clone makes sure every call to init-library creates a new hashtable and not just a reference to the same one. It's a preferred way of initialising hashtables if you don't need to specify capacity.

Then an add-new-book word would be nice that would take a title, author and genre, create a new book and add it to the library:

USE: accessors
: add-new-book ( library title author genre -- library )
    <book> swap over
    title>> swap
    [ set-at ] keep
;
Enter fullscreen mode Exit fullscreen mode

To add a key-pair value to a hashtable you use the word set-at which stack effect is ( value key assoc -- ). The first problem is that both value (the book) and key (the title) have to be below the assoc (the library hashtable), and this is dealt with by some stack shuffling. The second problem is that set-at consumes the hashtable, but it can be remedied by enclosing it in a quotation and calling via the keep combinator which saves the top value, executes the quotation and restores the saved value.

The third problem with add-new-book is that we didn't define the constructor <book> yet, so let's do that. It needs to take the initial data (title, author and genre), fill the default values of the other slots and use a boa constructor to make a book:

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

Accessing the library

After adding all the books to the library it would be nice to be able to interact with them. From what I specified in design we need words for searching, deleting and borrowing books. Let's also make them user friendly, so if I try to interact with a nonexistent book I get a warning message. For these I will use the print word from the io vocabulary which takes a string and displays it on the console.

To get a book from the library we need to pass it's title and the library itself to the generic word at:

USE: io
: find-book ( library title -- library book )
    over at dup
    [ "WARNING: a book of this title was not found!" print ]
    unless
;
Enter fullscreen mode Exit fullscreen mode

at will not throw any errors when the book is not found, it will simply return f. This means we can check the returned value and warn the user unless the book was found.

To delete a book from the library we need the same information as when searching passed to the generic word delete-at:

: delete-book ( library title -- library )
    over at dup
    [ title>> over delete-at ]
    [ "WARNING: a book of this title was not found!" print drop ]
    if
;
Enter fullscreen mode Exit fullscreen mode

Instead of simply deleting the book, which would work even if the book was not present, I check if the books exists beforehand so I can warn the user about it. The action they attempted was not carried out successfully, so they need to know that. Maybe they made a typo?

The final utility expected from this vocabulary is handling borrowed books. For this I will define two words: borrow-book and return-book that will adjust appropriate slots of the book's slot. In addition to that there are two new warnings: borrow-book should not work if the book is already borrowed, and the same for return-book and not borrowed books.

: borrow-book ( library title borrower -- library )
    [ drop find-book ] keep over    ! Fetch the book and warn if isn't found
    [ over borrower>>               ! If is found check if already borrowed
        [ "WARNING: the book has already been borrowed!" print 2drop ]
        [ >>borrower [ 1 + ] change-times-borrowed drop ]
        if ]
    [ 2drop ] if                    ! If not just do nothing
;

: return-book ( library title -- library )
    over at dup             ! Fetch the book
    [ dup borrower>>        ! If found check if borrowed
        [ f >>borrower ]    ! If borrowed return
        [ "WARNING: the book is not borrowed!" print ]
        if ]
    [ "WARNING: a book of this title was not found!" print ]
    if drop
;
Enter fullscreen mode Exit fullscreen mode

Now hold up. Remember the guidelines? Whenever a definition is big enough that comments are needed to explain what is going on it's a sign that you need to factor something out.

Factorisation

The best practice of all

There is a big and obvious factor common to all words in our vocabulary: the "book not found" warning. It could be factored out using just find-book, but there is a better way. Let's define a combinator that will take the library, title and a quotation, call the quotation if the book is found and otherwise warn the user and do nothing:

: if-book-exists ( library title quot: ( book -- x ) -- library x|0 )
    [ drop over at dup ] keep
    [ "WARNING: a book of a given title was not found!" print ]
    if
; inline
Enter fullscreen mode Exit fullscreen mode

This word, like all combinators, is and inline word. It means that before compiling all occurrences of it will be replaced with its body. This lets the compiler handle stack effects more efficiently and is required for most words that take quotations as arguments.

Let's now rewrite previous words in terms of this new combinator. Most of them will have a form of [ body ] if-book-exists.

: find-book ( library title -- library book )
    [ ] if-book-exists
;

: delete-book ( library title -- library )
    [ title>> over delete-at f ] if-book-exists
    drop
;

: return-book ( library title -- library )
    [ dup borrower>>
        [ f >>borrower ]
        [ "WARNING: the book is not borrowed!" print ]
    if ] if-book-exists drop
;
Enter fullscreen mode Exit fullscreen mode

Returning the book takes an additional argument - borrower - but the body quotation's stack effect for if-book-exists must only take a book as an argument. To make this happen we need to construct a quotation that already contains the borrower. This brings us to the concept of partial application, or currying. It's a functional programming term which is most easily explained by example.

Addition is a function that takes two numbers and returns a number. But it can sometimes be useful to first supply it with only one argument and then use as a single argument function. For example if we know that a function that adds 5 to a number will be useful we can apply + to 5 getting [ 5 + ] which still requires one argument to return a number. This can be done with any function, producing a new function that takes one less argument and in Factor currying is achieved with the word curry.

value [ body ] curry
! S: [ value body ]
Enter fullscreen mode Exit fullscreen mode

Now we need to make a quotation with stack effect ( book borrower -- x ) and partially apply it to the borrower producing a quotation with stack effect ( book -- x ) which is what we need for if-book-exists.

: borrow-book ( library title borrower -- library )
    [ over borrower>>
        [ drop "WARNING: the book has already been borrowed!" print ]
        [ >>borrower [ 1 + ] change-times-borrowed ]
    if ] curry if-book-exists drop
;
Enter fullscreen mode Exit fullscreen mode

Final code

Works for me

This is how the "esoposting-library.factor" file looks like after all the words have been added:

! Copyright (C) 2021 Aleksander Sabak.
! See http://factorcode.org/license.txt for BSD license.
USING: accessors assocs classes.maybe io kernel math strings ;
IN: esoposting-library

TUPLE: book
    { title string read-only }
    { author string read-only }
    { genre string read-only }
    { times-borrowed integer }
    { borrower maybe{ string } }
;

: init-library ( -- library )
    H{ } clone
;

: <book> ( title author genre -- book )
    0 f book boa
;

: add-new-book ( library title author genre -- library )
    <book> swap over
    title>> swap
    [ set-at ] keep
;

: if-book-exists ( library title quot: ( book -- x ) -- library x|0 )
    [ drop over at dup ] keep
    [ "WARNING: a book of a given title was not found!" print ]
    if
; inline

: find-book ( library title -- library book )
    [ ] if-book-exists
;

: delete-book ( library title -- library )
    [ title>> over delete-at f ] if-book-exists
    drop
;

: borrow-book ( library title borrower -- library )
    [ over borrower>>
        [ drop "WARNING: the book has already been borrowed!" print ]
        [ >>borrower [ 1 + ] change-times-borrowed ]
    if ] curry if-book-exists drop
;

: return-book ( library title -- library )
    [ dup borrower>>
        [ f >>borrower ]
        [ "WARNING: the book is not borrowed!" print ]
    if ] if-book-exists drop
;
Enter fullscreen mode Exit fullscreen mode

All libraries I am USING: are specified at the top in alphabetical order. I encourage you to try and play around with this vocabulary or whatever you came up with during this tutorial. Remember that you can prettyprint whatever is on the stack with ., even tuples.

Conclusion

That was a lot of writing

I hope you enjoyed this more practical tutorial. You are now ready to go and experiment with writing your own vocabularies. As always remember to make use of the built in help, there is a lot of knowledge to be found in the docs. This is the end of the first chapter of this series, next time I will discuss Factor's GUI vocabularies and start building towards making a desktop application.

Discussion (0)