Off To The Races
Actors are ideal for coordinating asynchronous activity. A good example of that is arranging a “race” among multiple services. The idea is you make a request to multiple services and the first one to reply wins the race. All other replies are ignored.
Each request to a service actor includes a “reply-to” address called the customer actor. In a race, we provide a “once” customer that forwards one message (to the real customer) and ignores any subsequent messages.
once_beh:                   ; rcvr <- msg
    push #?                 ; data=#?
    push sink_beh           ; data code=sink_beh
    actor become            ; --
fwd_beh:                    ; rcvr <- msg
    msg 0                   ; msg
    state 0                 ; msg rcvr
    actor send              ; --
sink_beh:                   ; _ <- _
    end commit
    By labeling multiple entry-points
    and sharing common tail-sequences
    of instructions,
    we define three useful actor behaviors.
    The sink_beh
    ignores/discards all messages.
    The fwd_beh
    forwards messages to another actor
    (not used in this example).
    And the once_beh
    forwards exactly one message.
    The receiver of the forwarded message
    is provided as the private data/state
    of the actor at creation.
    The actor primitive become
    is similar to create but
    provides new code and data for the current actor
    rather than creating a new actor.
    This is important because become
    is the only mutation mechanism available in uFork.
    Note that actor primitives
    like become and send
    do not take effect until and unless
    the actor executes end commit.
Iteration
In Imperative Programming iteration over a collection is usually expressed as some kind of loop. In Functional Programming iteration can be expressed by recursion. In Actor-Based Programming iteration can be expressed through asynchonous messaging.
We will need a broadcast actor to send a request to each participant in the race. The broadcast actor sends the same message to each actor in a nil-terminated pair-list. We use nil-termination because we don't know how many actors there will be.
broadcast_beh:              ; value <- actors
    msg 0                   ; actors
    typeq #pair_t           ; is_pair(actors)
    if_not broadcast_done   ; --
    msg 0                   ; actors
    part 1                  ; rest first
    state 0                 ; rest first value
    roll 2                  ; rest value first
    actor send              ; rest
    actor self              ; rest SELF
    actor send              ; --
broadcast_done:
    end commit
    If the message is not a pair,
    the broadcast is done.
    Otherwise we split the pair
    and send the value to the first actor.
    We send the rest of the list to ourself,
    thus iterating over the list.
Delta Service
    A service is an actor
    that responds to each cust,args message
    with exactly one reply to the cust actor.
    For the puposes of our race,
    we will create several instances
    of a “delta” service.
delta_beh:                  ; delta <- cust,num
    msg -1                  ; num
    state 0                 ; num delta
    alu add                 ; reply=num+delta
    msg 1                   ; reply cust
    actor send              ; --
    end commit
    When a cust,num message arrives,
    the delta service sends
    num+delta to cust.
Delay Proxy
    There is no point in running a race
    if the winner is decided deterministically.
    Instead, we will introduce random delays
    to simulate variable network/processing time.
    The first step is to request a random number
    between min and min+range-1.
delay_beh:                  ; rcvr,min,range,random,timer <- msg
    state 3                 ; range
    state 0                 ; range cfg
    msg 0                   ; range cfg msg
    pair 1                  ; range data=msg,cfg
    push k_delay_beh        ; range data code=k_delay_beh
    actor create            ; range k_delay=k_delay_beh.data
    pair 1                  ; k_delay,range
    state 4                 ; k_delay,range random
    actor send              ; --
    end commit
    A delay actor serves as a proxy
    with the same API as the service.
    It has more private state
    than our previous examples.
    It needs capabilities for both
    timer and random devices.
    Configuration parameters min and range
    specify the desired delay in milliseconds.
    And finally, rcvr is the service
    to eventually invoke.
    The delay actor introduces a new
    asynchronous coordination challenge.
    We need a number from the random device
    before we can do anything else,
    but the device is accessed via actor messages.
    Our solution is to create
    a continuation customer k_delay
    with all the information needed
    to perform the rest of the computation.
    Since all data-structures are immutable,
    we can safely share the configuration data
    while adding the msg to the front.
k_delay_beh:                ; msg,rcvr,min,range,random,timer <- num
    state 1                 ; msg
    state 2                 ; msg rcvr
    state 3                 ; msg rcvr min
    msg 0                   ; msg rcvr min num
    alu add                 ; msg rcvr delay=min+num
    pair 2                  ; delay,rcvr,msg
    state -5                ; delay,rcvr,msg timer
    actor send              ; --
    end commit
    When the random device provides num,
    we compose the request for the timer device,
    including delay=min+num.
    Note that the timer is the 5th tail
    of the state pair-list, not the 6th element.
    After the requested delay,
    the timer device will send
    the original msg
    to the rcvr actor.
Boot Behavior
With these definitions in place, we can now put all the pieces together and demonstrate a race among services.
boot:                       ; _ <- {caps}
    msg 0                   ; {caps}
    push dev.timer_key      ; {caps} timer_key
    dict get                ; timer_dev
    msg 0                   ; timer_dev {caps}
    push dev.random_key     ; timer_dev {caps} random_key
    dict get                ; timer_dev random_dev
    push 100                ; timer_dev random_dev range=100ms
    push 20                 ; timer_dev random_dev range min=20ms
    pair 3                  ; cfg=min,range,random,timer
We start by obtaining the device capabilities the services will need, and define the configuration parameters they will share.
    dup 1                   ; cfg cfg
    push 1                  ; cfg cfg delta=1
    push delta_beh          ; cfg cfg delta code=delta_beh
    actor create            ; cfg cfg rcvr=delta_beh.1
    pair 1                  ; cfg data=rcvr,cfg
    push delay_beh          ; cfg data code=delay_beh
    actor create            ; cfg delay_inc
    The first service uses
    a delta value of +1.
    We wrap the service with a delay-proxy
    and call it delay_inc.
    pick 2                  ; cfg delay_inc cfg
    push 0                  ; ... cfg delta=0
    push delta_beh          ; ... cfg delta code=delta_beh
    actor create            ; ... cfg rcvr=delta_beh.0
    pair 1                  ; ... data=rcvr,cfg
    push delay_beh          ; ... data code=delay_beh
    actor create            ; ... delay_zero
    The second service uses
    a delta value of 0.
    We wrap the service with a delay-proxy
    and call it delay_zero.
    roll 3                  ; delay_inc delay_zero cfg
    push -1                 ; ... cfg delta=-1
    push delta_beh          ; ... cfg delta code=delta_beh
    actor create            ; ... cfg rcvr=delta_beh.-1
    pair 1                  ; ... data=rcvr,cfg
    push delay_beh          ; ... data code=delay_beh
    actor create            ; ... delay_dec
    The third service uses
    a delta value of -1.
    We wrap the service with a delay-proxy
    and call it delay_dec.
    push #nil               ; delay_inc delay_zero delay_dec #nil
    roll -4                 ; #nil delay_inc delay_zero delay_dec
    pair 3                  ; list=delay_dec,delay_zero,delay_inc,#nil
    Our broadcast actor wants
    a nil-terminated list of actors,
    so we roll a #nil into place
    and build the list.
    push 5                  ; list num=5
    msg 0                   ; list num {caps}
    push dev.debug_key      ; list num {caps} debug_key
    dict get                ; list num debug_dev
    push once_beh           ; list num debug_dev once_beh
    actor create            ; list num cust=once_beh.debug_dev
    pair 1                  ; list msg=cust,num
    push broadcast_beh      ; list msg broadcast_beh
    actor create            ; list broadcast_beh.msg
    actor send              ; ---
    end commit
    The service request we broadcast
    has a num value of 5
    and a cust that is a forward-once
    to the debug device.
    Each time we run this example
    one value will appear on the console,
    either 4, 5, or 6
    depending on which delta-service
    replies first.
.import
    dev: "https://ufork.org/lib/dev.asm"
once_beh:                   ; rcvr <- msg
    ...
broadcast_beh:              ; value <- actors
    ...
delta_beh:                  ; delta <- cust,num
    ...
delay_beh:                  ; rcvr,min,range,random,timer <- msg
    ...
boot:                       ; _ <- {caps}
    ...
.export
    boot