martian-computing

CS 498MC Martian Computing at the University of Illinois at Urbana–Champaign

View the Project on GitHub davis68/martian-computing

Gall II

Learning Objectives

Managing State

Basically everything we have done so far has relied on rebuilding the subject to produce a desired result. Gall apps may have long-deferred calculations, however, and it is infeasible to run apps asynchronously the same way one does with a quick computation.

State

Each Gall agent (“app”) maintains a current state. One of the most common return types for Gall app arms is (quip card _this) or (quip card _state). A ++quip is a tuple of list and mold, where we have a list of cards and the mold of the current state. This means that the arm intends to return cards (moves, events) and the changed state of the app.

It’s worth noting that a common convention in modern Gall programs is to define a face this for the current agent:

+*  this  .
    def   ~(. (default-agent this %|) bowl)

so you’ll see (quip card _this) more frequently.

The program has two pieces of state. One is of mold bowl:gall (in fact, the agent is a door |_ =bowl:gall). A bowl is defined in %zuse:

++  bowl              ::  standard app state
  $:  $:  our=ship    ::  host
          src=ship    ::  guest
          dap=term    ::  agent
      ==              ::
      $:  wex=boat    ::  outgoing subscriptions
          sup=bitt    ::  incoming subs
      ==              ::
      $:  act=@ud     ::  change number
          eny=@uvJ    ::  entropy
          now=@da     ::  current time
          byk=beak    ::  load source
  ==  ==

An app may also maintain internal state. One idiom for updating this state is to use the %= centis rune’s irregular form. (We’ve been doing this all along with $().)

::  Delete a chat message
state(inbox (~(del by inbox) path.action))

Another option is to use a delayed monadic binding, as demonstrated below.

Many Gall apps are now intended to use graph-store, a backend data storage format and database that both provides internally consistent data and external API communications endpoints. (This is new as of summer 2020.)

graph-store handles data access perms at the hook level, not at the store level.

Ultimately, all the existing Tlon-supported apps will be migrated to graph-store. Here is an example showing the difference between the old JSON format and the new JSON graph-store format.

Monads

Functional programming languages and platforms prefer pure functions, meaning that these functions don’t emit effects. Hoon cheats a little bit with ~& sigpam for output, but in general is quite functional-as-in-language. Many functional languages introduce the monad as a way of tracking state (including input/output):

A state monad allows a programmer to attach state information of any type to a calculation. Given any value type, the corresponding type in the state monad is a function which accepts a state, then outputs a new state along with a return value.

Hoon provides three monad/monad-adjacent runes for producing pipelines of computations:

(These are not used in every Gall app, but they can be helpful when deferring parts of a computation based on external data.)

For instance, here is a ;< pipeline:

;<  =state  bind:m  (handle-poke %noun 'fetch')

A generic ;< pipeline

;<  a=@  bind:m  c  d

means to run c giving an output of type a and pass it to d.

Here, the state is an app-local structure (defined with +$); m is a tapp-async thread with a ++bind arm to a strand (Arvo thread).

grep the Urbit source code for examples of these three runes in use.

Scrying

One of the key operations in Gall (and Clay) is scrying (++on-peek handler). To scry is to request data from anywhere in the global immutable namespace. A scry produces a (unit (unit cage)) (recall that a unit lets us distinguish empty/missing data from zero data).

Scrying is accomplished with the .^ dotket rune. This accepts a mold (to cast the response), a tag (like %cx, as you saw with Clay), and a noun (the path to query). Basically, think of a .^ scry as a way of finding out things that aren’t in your subject.

Check the reference agent Gall example for a good example of how scrying works in the ++on-peek arm.

++  on-peek
  |=  =path
  ^-  (unit (unit cage))
  ?+  path  [~ ~]
      [%x %local ~]           :: local counter request
    ``[%atom !>(local)]       :: produce local counter
      [%x %remote who=@ta ~]  :: remote counter request
      =*  location  i.t.t.path
      =/  res  (~(got by remote) (slav %p location))
    ``[%atom !>(res)]         :: produce remote counter
      [%y %remote ~]          :: list of remote counters request
      =/  dir=(map @ta ~)
      %-  molt  %+  turn  ~(tap by remote)
      |=  [who=@p *]  [(scot %p who) ~]
    ``[%arch !>(`arch`[~ dir])] :: produce list of remote counters
  ==

(?+ wutlus is a switch statement.)

This would be used in the following ways:

.^(@ %gx /=example-gall=/local/atom)        :: produce local counter
.^(@ %gx /=example-gall=/remote/~zod/atom)  :: produce counter for ~zod
.^(arch %gy /=example-gall=/remote)         :: produce current list

To access data on a remote ship, you can’t just scry. What you actually do is to subscribe to data, and when the remote agent updates, it will send the information to subscribers.