12. Looks

Goal

Draw the system your own way.

New in this chapter

Steps

So far the stage has been drawn for you, by a default look. A look is a package like the others, but it only watches: it reacts to the facts of the trace, draws, and never changes what happens. Nothing you write in a look can alter a run, so you can change how a system looks without ever risking what it does.

A look reacts with when followed by a trace event: sim.Started, sim.Added n, net.Sent m from a to b. Trace events have no receiver, because they happen to the whole system. The words after the event name new names for the parts of the fact (n is the node that was added), or, if a name already exists, filter on it.

Look code runs in the fourth context, look. It reads the exported state of nodes, as it was after that fact, and it has the drawing phrases of std/look. Using std/look also switches the default look off, so the package must draw the cards itself. We start a new package, rumorlook, and import it from the demo; a package with observers is active as soon as it is imported. Step 1 puts the nodes on a ring with look.Layout "ring" and draws one card for each node:

Step 1

# package main
use example.com/tutorial/rumor
use example.com/tutorial/rumorlook
use std/net
use std/sim

define main
  sim.Seed 4
  set nodes to (sim.Cluster of 6 running rumor)
  set left to {(item 0 of nodes), (item 1 of nodes), (item 2 of nodes)}
  set right to {(item 3 of nodes), (item 4 of nodes), (item 5 of nodes)}
  rumor.Connect nodes
  deliver (message rumor.Rumor with Text "The leader is n1") to (item 0 of nodes)
  net.DropNext "Rumor" from (item 0 of nodes) to (item 1 of nodes)
  after 300ms
    net.Partition left from right
  after 3s
    net.Heal
# package rumorlook
use std/look
use std/sim

# Put the cards on a ring
when sim.Started
  look.Layout "ring"

# One card for each node, attached to the node as "card"
when (sim.Added n)
  draw (look.Card for n) as card for n

draw (look.Card for n) as card for n creates a shape, gives it the name card, and attaches it to the node n with for n. A shape has the type shape. Attached shapes can be reached later from any observer with card of n. Messages are invisible for now. Step 2 draws a ball per message: it takes off at net.Sent, flies between the two cards with look.Flight, and is removed when the message arrives. The ball is attached to the message id, net.Id m, so ball of (net.Id m) finds it later:

Step 2

# package rumorlook
use std/look
use std/net
use std/sim
use std/theme

# Put the cards on a ring
when sim.Started
  look.Layout "ring"

# One card for each node, attached to the node as "card"
when (sim.Added n)
  draw (look.Card for n) as card for n

# A ball takes off from the sender's card and flies to the receiver's
when (net.Sent m from a to b)
  draw (look.Circle 6) as ball for (net.Id m)
  look.Fill ball with theme.Accent
  look.Flight ball from (card of a) to (card of b) for m

# It lands and disappears
when (net.Arrived m from a to b)
  look.Remove (ball of (net.Id m))

A card should show what the node knows. A draw can end with an indented block that is reactive: it runs again whenever the state it reads changes, and the card morphs to the new look. A binding with a type, [n: rumor.Node], tells the compiler that n runs rumor, so n.rumor.Known is readable. Colors are values too, of type color, but only named ones: a color is a phrase with no holes whose result is the literal. Heard is ours, and theme.Quiet comes from std/theme:

Step 3

# package rumorlook
use example.com/tutorial/rumor
use std/look
use std/net
use std/sim
use std/theme

# The color of a card that knows the rumor
define Heard
  result #F4A340

# Quiet while a node knows nothing, Heard afterwards
define Tint [t: text]
  if (t is "")
    result theme.Quiet
  result Heard

# Put the cards on a ring
when sim.Started
  look.Layout "ring"

# One card for each node, whose color follows what the node knows
when (sim.Added [n: rumor.Node])
  draw (look.Card for n) as card for n
    look.Fill card with (Tint n.rumor.Known)

# A ball takes off from the sender's card and flies to the receiver's
when (net.Sent m from a to b)
  draw (look.Circle 6) as ball for (net.Id m)
  look.Fill ball with theme.Accent
  look.Flight ball from (card of a) to (card of b) for m

# It lands and disappears
when (net.Arrived m from a to b)
  look.Remove (ball of (net.Id m))

That is a lot of code to redo what the default look already did. A look package may use std/look/basic to keep the default and change only what it wants. Step 4 keeps the basic look (cards, messages, captions) and replaces just the card with a tinted one:

Step 4

# package rumorlook
use example.com/tutorial/rumor
use std/look
use std/look/basic
use std/sim
use std/theme

# The color of a card that knows the rumor
define Heard
  result #F4A340

# Quiet while a node knows nothing, Heard afterwards
define Tint [t: text]
  if (t is "")
    result theme.Quiet
  result Heard

# Replace the basic card with one whose color follows what the node knows
when (sim.Added [n: rumor.Node])
  draw (look.Card for n) as card for n
    look.Fill card with (Tint n.rumor.Known)

Looks are the place for taste. The behaviour never changes: run the program without the look and you get the same trace.

What to watch

Step 1: six empty cards on a ring and no messages. Step 2: balls travel between cards; a dropped message stops halfway. Step 3 and 4: a card is grey until its node learns the rumor, then it turns amber, smoothly. The partition is visible: the amber spreads on one side and waits on the other.

Why it looks like this

A look cannot change what happens. An observer that tries to set a node field is refused:

rumorlook/rumorlook.synod:25:7: error[context]: "set n.rumor.Known" is not allowed in look context
   |
25 |   set n.rumor.Known to ""
   |       ^^^^^^^^^^^^^
Error context

synod check says error[context]: setting state is not allowed in look context. This is what lets the trace be the same with and without a look.

Exercises

  1. Change the colors: make unknowing cards theme.Critical.
Expected

Use theme.Critical in Tint. The cards start red and turn amber.

  1. In step 2 use look.Layout "row".
Expected

The six cards stand in a row instead of a ring.

  1. In step 4, remove the use std/look/basic line.
Expected

The program still checks, but messages are no longer drawn, because only the cards are drawn by this package.