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
=
(listof (or/c $message? (recursive-contract subprogram-log/c)))
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))
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)
9.2 Alternative Constructors
procedure
(subprogram-unit v) → subprogram?
v : any/c
procedure
(subprogram-failure variant) → subprogram?
variant : any/c
procedure
(subprogram-attachment v next) → subprogram?
v : any/c next : (or/c $message? (listof $message?))
procedure
(subprogram-map f to-map) → subprogram?
f : (-> $message? $message?) to-map : subprogram?
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
procedure
(subprogram-acyclic key proc) → subprogram?
key : any/c proc : (-> (listof $message?) (values any/c subprogram-log/c))
struct
key : any/c
9.3 Subprogram Control
syntax
(define-subprogram (id formals ...) body ...)
($use v [messages]): Aborts the program with v as the result and the given message log. messages defaults to the current subprogram log.
($fail [msg]): Abort the program with a FAILURE result and an optional new message. If no message is passed, then the program log is unaffected.
($attach v [msg]): Abort the program with v as the result and an optional new message. If no message is passed, then the program log is unaffected.
($run! l): Equivalent to (run-subprogram l m), where m is bound to current subprogram log.
$messages: A reference to the current subprogram log.
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?
procedure
(subprogram-branch [ #:discard? discard?] test on-failure) → subprogram? discard? : any/c = #f test : subprogram? on-failure : subprogram?
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?))
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
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?
Additionally, that list is flattened, then reversed.
procedure
(get-subprogram-value program) → any/c
program : subprogram?
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?
(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?
procedure
(check-subprogram-value subprg continue) → void?
subprg : subprogram? continue : procedure?
procedure
(test-subprogram-value test-message subprg continue) → void? test-message : string? subprg : subprogram? continue : procedure?