The Arvo operating system is divided up into modules called vanes. Gall is the vane responsible for providing an API for building stateful applications in Urbit. Gall is a complex system that takes a lot of getting used to, so let's dip our toes in with a simple egg-timer app.
The following code sample should be saved as /base/app/egg-timer.hoon
After committing: |rein %base [& %egg-timer]
and poke with :egg-timer ~s10
/+ default-agent
|%
+$ card card:agent:gall
--
::
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %.n) bowl)
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
=/ t !<(@dr vase)
:_ this
[%pass /egg-timer %arvo %b %wait (add now.bowl t)]~
::
++ on-arvo
^+ on-arvo:*agent:gall
|= [=wire =sign-arvo]
^- (quip card _this)
~& "Timer went off!"
[~ this]
::
++ on-init on-init:def
++ on-save on-save:def
++ on-load on-load:def
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-agent on-agent:def
++ on-fail on-fail:def
--
The first thing to notice is that we are creating a core (|%
) and a door (|_
). This is a typical style for Gall programming wherein types are defined in the first core and your application is defined in the following door.
Our door will have type agent:gall
as specified in sys/zuse.hoon
|%
+$ card card:agent:gall
--
The core here defines only one type: card
.
A card
is a request to Arvo to do something for us. In this case, the only card used will be a %wait
request to Behn which we'll discuss later.
It's important to note that the name card
is technically arbitrary. By convention, however, you will see it called card
practically everywhere.
The sample of the door(|_
) is:
=bowl:gall
You can find the full definition of bowl
in sys/zuse.hoon
, but for now it's enough to know that includes contextual data used by Gall with particular faces. Below are some important such faces:
our
The ship this code is running onsrc
The ship the current event originated fromeny
Guaranteed-fresh entropynow
The current time
We give bowl:gall
the face bowl
.
Additionally, we use +*
to create two structures for future use:
+* this .
def ~(. (default-agent this %.n) bowl)
this
is our current context(the door).
def
uses default-agent
to produce a standard gall:agent
door, from which we will instance default definitions for our unused arms.
All Gall agents have a fixed set of 10 arms. In this trivial case, we will specify only two and use the default definitions from default-agent
for the remaining arms.
++ on-poke
|= [=mark =vase]
^- (quip card _this)
=/ t !<(@dr vase)
:_ this
[%pass /egg-timer %arvo %b %wait (add now.bowl t)]~
The ++on-poke
arm evaluates to a gate which takes a sample [mark vase]
. mark
is a type specification and vase
is a cell of [specification value]
Note: since ++on-poke
may take many different samples, it would be prudent to add a %noun
mark check here.
In this example, we assume that our sample will contain a @dr
and pull it from the vase
with =/ t !<(@dr vase)
@dr
is an aura for a 128-bit relative date. Here are a few examples.
~s17
17 seconds~m20
20 minutes~m13s37
13 minutes, 37 seconds~d42
42 days
As a matter of good type hygiene, we explicitly cast the output of this gate with ^-
to ensure we are producing the correct structure for Gall to handle.
Our specification is (quip card _this)
which produces a type, [(list card) _this]
. Note that _this
is irregular form of $_(this)
which normalizes to the type of this
, our door.
Next we use the :_
rune which is the inverted form of :-
, the cell construction rune.
:_ this
[%pass /egg-timer %arvo %b %wait (add now.bowl t)]~
Our card should follow the basic form [%pass /my/wire a-note]
In this case, our card will ask Arvo to %pass
a note along the wire /egg-timer
to Behn(%b
) containing a %wait
request.
Behn will start a timer for us, and will respond along our wire, /egg-timer
.
now.bowl
is the current time of type @da
, and t
was declared as @dr
. Because they are both atoms, we can add now
and t
to produce an atom that is t
units of time into the future from now
. That produced atom can be interpreted as a @da
.
The calculated time will be passed via Arvo to Behn with instruction to %wait @da
, i.e., to set a timer.
That's all for our ++on-poke
arm. But what about when the timer goes off? Behn will create a gift
, a similar construct to our card
which will be dispatched back to us via Gall in the ++on-arvo
arm.
++ on-arvo
^+ on-arvo:*agent:gall
|= [=wire =sign-arvo]
^- (quip card _this)
~& "Timer went off!"
[~ this]
++on-arvo
evaluates to a gate whose sample is a cell of wire
and sign-arvo
here given default faces. The syntax of =wire
is a shortcut for wire=wire
; it's a common pattern to shadow the name of a type when you only have one instance of the type and are not going to refer to the type itself.
A wire
is an alias for a path. Our wire
will be the path we provided in our ++on-poke
card. If we needed to do something particular depending on which request triggered this Arvo card, we could use the wire to do so.
Note: since multiple vanes can send many sorts of cards via ++on-arvo
, it would be prudent to check if sign-arvo
is [%b %wake *]
. Since our trivial example assumes the card is %wake
from Behn, we will forgo that better practice here.
Next we have the same cast we used in ++on-poke
to make sure we produce the correct type for Gall. (quip card _this)
resolves to a type [(list card) _this]
~& "Timer went off!"
~&
is the debugging printf rune. Here we're using it to output a message. We could, however, modify this line of code to do any other computation when ++on-arvo
is called.
We need to produce a noun of type (quip card _this)
i.e., [(list card) _this]
.
In this case, we will not produce any card or change application state. Therefore we simply produce a cell of a null list and our original context, [~ this]
.
++ on-init on-init:def
++ on-save on-save:def
++ on-load on-load:def
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-agent on-agent:def
++ on-fail on-fail:def
--
Finally, we define the remaining 8 unused but required Gall Agent arms as instances of the default arms provided in /lib/default-agent.hoon
.