Every Gall app (agent) contains a core with exactly 10 arms. Each of those arms is responsible for handling certain types of messages that Gall feeds in to our agent.
Most programs create a helper core to hold functionality that is used by those 10 arms. In addition to that, there are some types that all Gall programs use to send requests and receive responses, as well as code conventions that are part of nearly every Gall agent's structure.
Gall's arms fall into a few separate families that handle broad categories of work.
These arms handle initialization of app state, initialization of any outside resources and upgrades to new versions of the app with different types of state.
on-init
on-save
on-load
These arms respond to calls initiated by outside entities, such as users pushing buttons, HTTP requests incoming to the app, or other agents initiating contact
on-poke
: handles any calls from outside processes that aren't subscriptions, i.e. one-time actionson-watch
: receives incoming subscription requests from other processeson-leave
: receives notifications that another process is unsubscribing
These 2 arms handle responses to requests we make to other agents or Arvo vanes.
on-agent
: receives responses when we call another Gall agent'son-poke
oron-watch
on-arvo
: receives responses from Arvo vanes (such as a list of files if we ask Clay to list a directory's contents)
on-peek
: a "read-only" arm. Doesn't produce actions, but allows querying and returning of our agent's internal state
on-fail
: if a crash happens in any arm excepton-poke
oron-watch
, Gall sends a crash report message to this arm. Most apps ignore this message.
Some arms take and return simple data types, but Gall also has a few key input and output types that it constantly uses. You can really drill into them here, but let's introduce a couple key ones. I'll refresh them and go more in depth as I explain each arm later.
wire
and path
are the same type: a list of terms. A leading /
is syntactic sugar for a list of terms.
:: the below two lines are identical
/this/is/a/path
[%this %is %a %path ~]
The type used to do "function calls/returns" to Arvo vanes or other Gall agents. They are tuples that look like:
:: For "calling" functions in vanes or agents
[%pass p=path q=note]
:: For "returning" values to vanes or agents
[%give p=gift]
note
and gift
look like:
:: note has 2 forms: 1 for calling Arvo, 1 for calling agents
$% [%arvo =note-arvo]
[%agent [=ship name=term] =task]
:: gift has can return %fact (piece of data) or some type of ack
$% [%fact paths=(list path) =cage]
[%kick paths=(list path) ship=(unit ship)]
[%watch-ack p=(unit tang)]
[%poke-ack p=(unit tang)]
:: quip definition
++ quip
|$ [item state]
[(list item) state]
:: Gall quip: item is card, state is agent-type
(quip card agent-type)
A common Gall pattern is for arms to output a quip. The list of card
is actions to perform, and the tail of the cell contains an updated version of the agent (new state of the app).
By convention, Gall apps generally all follow a similar structure. They do the following, in order:
- Ford imports of types (
/-
rune) and libraries (/+
rune) - define the app's state type
- Wrap in the
dbug
agent for easy debugging - define aliases for the agent, default agent implementation, and helper core (if applicable)
- write the "10-arms" agent core
- write the helper core that contains extra functionality so that the "10 arms" code isn't so long
Let's look at the source for the publish agent (as of June 17, 2020)
Here are links to where the pieces above are defined in it:
- type imports, library imports
- state type definitions lines 19-44
- dbug, line 62
- alias definitions
- arm definitions lines 70-517
- helper core
From line 66 of the above:
+* this .
def ~(. (default-agent this %|) bol)
main ~(. +> bol)
+*
is a macro that inserts code expansions at the start of every arm of the door. So it doesn't add extra arms, meaning that our door here still has only 10 arms, as required.
We have aliases for the this
, the default agent, and the helper core are defined. The ~(. door-name argument)
syntax means "pass this argument
to this door-name
", but instead of returning just one arm in the door, return the whole door. It's a way of setting the door's sample for every arm in the door at once.
default-agent
is a basic implementation of a Gall agent that returns valid but dummy responses for all arms. We give it the alias def
here, and you'll see most Gall programs call it when they don't need a particular arm to have an action.
Finally, note the +>
in line 68. This is a very common idiom in Gall for referring to the helper core. In this case, we have the =<
rune on line 64, which means that our Gall agent is defined with the helper core as the subject. Because our Gall agent is a door, the sample is in the head of the tail. So we select the tail of the tail, which is our helper core.
Many applications now use |^
instead of a helper core. This allows them to put the heavier functions below the main logic, while keeping helpers closer to their use place. You'll see both in these upcoming tutorials.
Near the top of the file, you'll see the line %- agent:dbug
. This wraps our program in the dbug
Gall agent, which lets us inspect the state of the program as it's running. We'll do this in more detail in the lifecycle lesson.
If you type |start %dbug
at the Dojo, you'll be able to browse to yourshipurl/~debug
and see the current state of every app in the ship that uses dbug
.
How does this work? If you look at /lib/dbug/hoon
, you'll see that its agent
arm simply takes a Gall agent as the sample. Then it creates an ag
alias that gives its bowl to that agent. At the top of on-poke
, we test whether the incoming mark is %dbug
. If it's not, then it just forwards the [mark vase]
sample to the agent. If it is, it does dbug
stuff to print the agent's state.
Because dbug
takes the agent
as an argument, it can call any of that agent
's arms. It calls them either to provide default behavior, or to get the agent's state using on-save
.
You've now seen all the pieces that make up a complete Gall app, with a conceptual overview of the kinds of things that each of the arms handle. If this seems overwhelming, don't panic. I'll take you through each part of this program structure in great detail in the upcoming lessons.
In your pier, browse to the /app/
directory. Open up 4 random files there. See if you can find:
- the Ford imports
- the state definition
- the 10 agent arms
- a helper core, if one exists
Submit the line numbers in each file for each part.
To do a chat-admin app, you need to get data from 3 Gall agents:
chat-store
, stores all messages for all chats a ship is a member ofgroup-store
, stores all members and admins of a groupmetadata-store
, stores all group->chat mappings
- List all the features that a group chat admin app will need
- For each feature, map it to one or more arms.
- Spec out what data the app will need to store internally and what it will need to get from each external data source.