On this page:
9.1 Fundamentals
subprogram-log/  c
subprogram
subprogram/  c
FAILURE
9.2 Alternative Constructors
subprogram-unit
subprogram-failure
subprogram-attachment
subprogram-map
coerce-subprogram
subprogram-acyclic
$cycle
9.3 Subprogram Control
define-subprogram
dump-subprogram
subprogram-branch
subprogram-fold
9.4 Entry Points for Subprograms
run-subprogram
get-subprogram-log
get-subprogram-value
9.5 Testing Subprograms
check-subprogram
test-subprogram
check-subprogram-value
test-subprogram-value
8.12

9 Subprograms🔗ℹ

 (require denxi/subprogram) package: denxi

A subprogram in the context of Denxi is an instance of the monadic value type subprogram. An instance of subprogram contains a Racket procedure that returns a value and some messages representing a subprogram log. When a composition of subprograms execute, a special FAILURE value returned by one subprogram prevents execution of following subprograms.

This control structure is necessary because subprograms are supposed to operate strictly under functional composition in mdo. Any unhandled exceptions raised during subprogram evaluation are understood under the context of FAILURE.

9.1 Fundamentals🔗ℹ

value

subprogram-log/c : contract?

 = 
(listof (or/c $message?
              (recursive-contract subprogram-log/c)))
A subprogram log is a list of any structure where messages are the only non-list elements. Unlike a program log, subprogram logs are “messy” due to being actively under construction. No structure is imposed on the list other than the first message of any list (no matter how nested) in a subprogram log represents the most recent activity.

This contract is not used in some parts of the implementation for performance reasons, but will be cited in this reference for clarification reasons.

struct

(struct subprogram (thnk)
    #:transparent)
  thnk : (-> (listof $message?) (values any/c subprogram-log/c))
A monadic type that computes a value alongside a subprogram log. The thnk must accept a subprogram log representing prior program activity, then perform planned work that may add at least zero new messages.

The latest message must come first in the resulting list, so cons is appropriate for adding a new message.

(subprogram (lambda (messages)
  (values (+ 2 2)
          (cons ($show-string "Putting 2 and 2 together")
                messages))))

If a computation must halt, use FAILURE as the computed value.

(subprogram (lambda (messages)
  (values FAILURE
          (cons ($show-string "I can't go on!")
                messages))))

It’s fine to cons another list of messages onto a subprogram log. It is only important that a message representing the most recent activity comes first should the list be flattened.

(subprogram (lambda (messages)
  (values whatever
          (cons (list ($show-string ...) ($show-datum ...))
                messages))))

syntax

(subprogram/c contract-expr)

Produces a contract for a subprogram. The procedure must return a value matching contract-expr as the first value, unless that value is FAILURE.

value

FAILURE : symbol?

An uninterned symbol that, when returned by a subprogram, prevents evaluation of a subsequent subprogram.

9.2 Alternative Constructors🔗ℹ

procedure

(subprogram-unit v)  subprogram?

  v : any/c
Returns a subprogram instance that yields v as a computed value, with no added messages.

procedure

(subprogram-failure variant)  subprogram?

  variant : any/c
Returns (subprogram (λ (m) (values FAILURE (cons V messages)))), where V is variant if ($message? variant) is true. Otherwise, V is ($show-string (exn->string variant)).

procedure

(subprogram-attachment v next)  subprogram?

  v : any/c
  next : (or/c $message? (listof $message?))
Returns (subprogram (λ (m) (values v (cons next m)))).

procedure

(subprogram-map f to-map)  subprogram?

  f : (-> $message? $message?)
  to-map : subprogram?
Returns a new subprogram instance such that each message produced by to-map is included in the combined log using (map f (run-subprogram to-map null)).

Use this to “scope” messages.

(define-message $build-subprogram-entry (name message))
 
; hypothetical
(define (create-build) (subprogram (lambda (messages) ...)))
 
(define build
  (subprogram-map (curry $build-subprogram-entry "my-build")
              (create-build)))

procedure

(coerce-subprogram v)  subprogram?

  v : any/c
Equivalent to (if (subprogram? v) v (subprogram-unit v))

procedure

(subprogram-acyclic key proc)  subprogram?

  key : any/c
  proc : (-> (listof $message?) (values any/c subprogram-log/c))

struct

(struct $cycle $message (key))

  key : any/c
subprogram-acyclic behaves like (subprogram proc) with cycle detection. If another subprogram instance runs in the context of proc, and that instance was constructed using subprogram-acyclic and a value equal? to key, then evaluation ends early. In that case, the computed value is FAILURE and ($cycle key) appears in the log.

9.3 Subprogram Control🔗ℹ

syntax

(define-subprogram (id formals ...) body ...)

Like (define (id formals ...) body ...), except the procedure runs in a continuation with the following injected procedure bindings:

The following example defines two equivalent procedures that clarify how define-subprogram reduces code volume.

(define-subprogram (interpret variant)
  (cond [(eq? 'no variant)
         ($fail ($show-string "Result is not okay"))]
        [(subprogram? variant)
         (call-with-values ($run! variant) $use)]))
 
(define (interpret result)
  (subprogram
   (lambda (messages)
     (call/cc
       (lambda (return)
         (cond [(eq? 'no variant)
                (return FAILURE ($show-string "Result is not okay"))]
               [(subprogram? variant)
                (call-with-values (run-subprogram variant messages) return)]))))))

procedure

(dump-subprogram [#:dump-message dump-message 
  #:force-value value] 
  preamble ...) 
  (subprogram/c any/c)
  dump-message : (-> $message? any) = writeln
  value : any/c = (void)
  preamble : $message?
Returns a subprogram that applies dump-message to every element of the preamble, then every element in the current subprogram log. The subprogram will use value as the result.

procedure

(subprogram-branch [#:discard? discard?]    
  test    
  on-failure)  subprogram?
  discard? : any/c = #f
  test : subprogram?
  on-failure : subprogram?
Returns a subprogram. If the test program fails, then the result of the returned program depends on on-failure.

If discard? is a true value, the messages accumulated in the subprogram log by test are discarded.

procedure

(subprogram-fold initial fns)  subprogram?

  initial : subprogram?
  fns : (listof (-> any/c subprogram?))
Returns a subprogram.

Bind each function in fns (under the context of subprogram-bind) starting from initial.

Assuming fn_0 is the first element of fns, and fn_N is the last element of fns, the returned subprogram behaves like the following mdo form.

(mdo v_0 := initial
     v_1 := (fn_N v_0)
     v_2 := (fn_N-1 v_1)
     ...
     v_N-1 := (fn_0 v_N-2)
     v_N := (fn_0 v_N-1))

Notice that fns are applied in reverse, so the last element of fns is the first to operate on the output of initial.

An example follows. Also note that the literal subprogram log near the end appears to follow the order of the input functions. Since message logs are assembled using cons, the fact the functions were applied in reverse made the messages also appear in reverse.

(define ((shift amount) accum)
  (subprogram (lambda (messages)
    (values (+ amount accum)
            (cons ($show-datum amount)
                  messages)))))
 
(define sub
  (subprogram-fold (subprogram-unit 0)
                   (list (shift -1)
                         (shift 8)
                         (shift -3))))
 
(define-values (result messages)
  (run-subprogram sub))
 
(equal? result 4)
(equal? messages
        (list ($show-datum -1)
              ($show-datum 8)
              ($show-datum -3)))

9.4 Entry Points for Subprograms🔗ℹ

procedure

(run-subprogram program [messages])  
any/c (listof $message?)
  program : subprogram?
  messages : subprogram-log/c = null
Applies all delayed work in program. Returns a value and an updated subprogram log.

When applying the procedure in the given subprogram, any (negate exn:break?) value V (not just exception types) raised as an exception will be caught. In that case, the invocation of run-subprogram that caught the value will return (run-subprogram (subprogram-failure V) messages).

procedure

(get-subprogram-log program)  (listof $message?)

  program : subprogram?
Like run-subprogram, but returns only the list of messages attached to the computed value.

Additionally, that list is flattened, then reversed.

procedure

(get-subprogram-value program)  any/c

  program : subprogram?
Like run-subprogram, but returns only the computed value.

9.5 Testing Subprograms🔗ℹ

 (require (submod denxi/subprogram test))

procedure

(check-subprogram [#:with initial]    
  subprg    
  continue)  void?
  initial : (listof $message?) = null
  subprg : subprogram?
  continue : procedure?
Equivalent to

(call-with-values
  (λ () (run-subprogram subprg initial))
  continue)

Use to write assertions against values produced by the subprogram.

(check-subprogram subprg
  (lambda (value log)
    (check-eq? value FAILURE)
    (check-equal? log (list ($show-string "whoops")))))

procedure

(test-subprogram [#:with initial]    
  test-message    
  subprogram-procedure    
  continue)  void?
  initial : (listof $message?) = null
  test-message : string?
  subprogram-procedure : subprogram?
  continue : procedure?
The test form of check-subprogram.

procedure

(check-subprogram-value subprg continue)  void?

  subprg : subprogram?
  continue : procedure?
Asserts that subprg produces an empty subprogram log, then applies continue to the subprogram value.

procedure

(test-subprogram-value test-message    
  subprg    
  continue)  void?
  test-message : string?
  subprg : subprogram?
  continue : procedure?
The test form of check-subprogram-value.