INTENDED AUDIENCE: Forth and RetroForth novices. A basic understanding of how to define words and how Forth dictionaries work is assumed.
GOAL: Understand how to use hook
, set-hook
, and unhook
in RetroForth.
Briefly cover the use of DEFER
in standard Forth systems.
What Are Hooks?
The term hook is used generically in many software development ecosystems. It
typically refers to a callback in a library or application which can be defined
or extended without modifying the library/application source code directly.
RetroForth also uses the term hook
in this manner. Let's look at some
real-world use cases to illustrate this point.
Real World Use Cases
Changing application behavior at runtime. Some software systems manage real-world hardware, such as the heating elements of a ceramics kiln or the pumps of an irrigation system. How could you test parts of the system without actually triggering any actuators? Such testing would require the ability to change the behavior of the application's code while it is running.
Using and creating custom callbacks. When developing an application for use
by other developers (instead of end-users), it is advantageous to provide
hooks
to other developers. For example, if you were building an irrigation
pump control system, you may wish to give developers a way to execute custom
code every time the pumps run. An ideal system would allow such behavior without
needing to modify core application code.
How Not To Do It
Both examples in the previous section are solvable in RetroForth by the use of
hooks. Hooks provide a means of
vectored execution
in RetroForth, similarly to the DEFER
word in standard Forths.
Before we cover the use of hooks, let's discuss why we need vectored execution at all. Vectored execution solves two constraints, listed below.
You can't use words that aren't in the dictionary yet. An incorrect approach
to providing callbacks might be to reference callbacks before they are defined
and then fill in the blanks
later on at runtime. Unfortunately, you can't do
this. In Forth, you must define a word before you can reference it. You can't
refer to a word before its declaration. This constraint means that we can't play
fill-in-the-blanks
with callbacks. If our application has an on-start
callback and we wish to reference it in a definition, then there needs to be an
on-start
entry in the dictionary. We must define all words at compile time.
Once compiled, you cannot redefine a word's content. Another approach that
is not possible in Forth systems is defining a placeholder word in the
dictionary and then later redefining the contents of the word. This approach is
also not possible because Forths operate in a
hyper static global environment
.
You can't change the way a word works by redefining a word of the same name. Any
word that referenced the old callback definition will continue to reference the
old version.
We need a way to add a placeholder and then populate the placeholder at runtime
rather than compile time. RetroForth solves this problem with hooks.
How RetroForth Solves This Problem
In RetroForth, you can add a placeholder for the functionality of things you
will define later by adding the word hook
to a hollow callback word.
If we were building toaster oven control software and wanted to add an on
start
callback, we might write something like this:
:activate-burners 'Burners_ON s:put nl ;
:begin-timer 'Timer_SET s:put nl ;
:toaster.on-start hook ;
:activate-toaster
toaster.on-start
activate-burners
begin-timer
;
You will notice that the first word in the definition of toaster.on-start
is
hook
. This marker provides a hook point for this word for us to fill in the
blank later. The hook
entry point must be the first word in a definition. If
left unfilled, the word above will do nothing. Calling activate-toaster
prints
the following text:
Burners ON
Timer SET
Since our target word has a hook point, we can populate the hook using
set-hook
:
:my-custom-callback 'The_toaster_is_running!!!! s:put nl ;
&my-custom-callback &toaster.on-start set-hook
Here's what we see when we run activate-toaster
:
The toaster is running!!!!
Burners ON
Timer SET
Setting a Default Value for Hooks
In the previous example, we mentioned that not calling set-hook
results in a
word that is essentially a no-op. No-op behavior is not the only option, though.
Word definitions that contain hooks may also define default behavior.
Going back to the previous example, we can extend our use case- what if we want
to warn the user that they set no callback? We can accomplish default behavior
by populating the body of toaster.on-start
with a default behavior:
:activate-burners 'Burners_ON s:put nl ;
:begin-timer 'Timer_SET s:put nl ;
:toaster.on-start
hook
'[WARNING]_Your_toaster_does_not_contain_an_`on-start`_callback
s:put nl
;
:activate-toaster
toaster.on-start
activate-burners
begin-timer
;
If we recompile the application with the new code and run activate-toaster
, we
get the following result:
[WARNING] Your toaster does not contain an `on-start` callback
Burners ON
Timer SET
Resetting to Defaults
The last piece of the hooks journey is unhook
. This word allows you to revert
a hook to its default behavior. After we complete our toaster work, we can
deactivate the hooks with the following line:
&toaster.on-start unhook
How Standard Forth Systems Do It
RetroForth hooks are similar to the word DEFER
seen in standards-compliant
Forth systems.
In such systems, you can attain similar behavior via DEFER
and IS
:
DEFER ACTIVATE-TOASTER
: MY-CUSTOM-WORD ." TOASTER ON!" ;
' MY-CUSTOM-WORD IS ACTIVATE-TOASTER
\ Prints "TOASTER ON!"
ACTIVATE-TOASTER
Thanks
Thanks to everyone on /r/Forth, ForthHub and @CRC for the helpful suggestions on my Forth learning journey. If you enjoyed this article, please consider sharing it. It will help more people discover RetroForth.