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
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
: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
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:
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 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.