pipeline.ss
#lang mzscheme

(require srfi/1/list
         (file "base.ss"))

;; A "pipeline" allows a programmer to wrap a procedure in one or more pieces of useful
;; functionality. Pipelines are lists of "stages", each of which performs some function
;; and calls the next stage. The last stage calls the target procedure.
;;
;; An example of this (and the original reason for creating pipelines) is request
;; processing in a web server. The server may consist of a number of controller
;; procedures, each of which serves a different page. All of these procedures will
;; have one or more bits of functionality in common:
;;
;;  - set up cookies
;;  - identify the user's browser
;;  - check the user's security privileges
;;
;; Note that, while many of these functions will be common across many controllers, there
;; will be occasions where one controller will need to do things differently from the others.
;;
;; The items above can be implemented as stages in a request processing pipeline. A standard
;; pipeline can be offered site-wide, and controllers can choose to customise it where
;; appropriate by adding, removing or changing stages.
;;
;; Stages are named so they can be uniquely referred to when manipulating pipelines in this
;; way. This has the added advantage that single stages can be extracted and run out of context
;; with the rest of the pipeline.
;;
;; More formally, given a target procedure:
;;
;;     target : any ... -> any
;;
;; a pipeline is a list of stages:
;;
;;     pipeline : (list-of stage)
;;
;; where a stage is a name and a body procedure:
;;
;;     struct stage : symbol ((any ... -> any) any ... -> any)
;;
;; The body procedure takes at least one argument: a "continuation procedure" that is called
;; to continue the pipeline. The arguments passed to the continuation procedure are passed
;; on to the next stage in the pipeline. The target procedure is considered a "pseudo stage"
;; that is called after all other stages.
;;
;; Any stage can abort the pipeline simply by failing to call the continuation procedure.
;; It is also perfectly reasonable for stages to set up parameters, install exception handlers,
;; change the arguments to subsequent stages and so on.
;;
;; [DJG] Noel says this all has something to do with "equirecursive types":
;;
;;     http://en.wikipedia.org/wiki/Recursive_type#Equirecursive_types
;;
;; but this is beyond me.

;; call-with-pipeline : pipeline (any ... -> any) any ... -> any
;;
;; Calls a procedure via a pipeline. The result returned is either the result of the procedure
;; or that of the last stage invoked.
(define (call-with-pipeline pipeline procedure . args)
  (define (pipe pipeline . args)
    (if (null? pipeline)
        (apply procedure args)
        (let ([stage (car pipeline)]
              [success 
               (lambda args 
                 (apply pipe (cons (cdr pipeline) args)))])
          (apply stage (cons success args)))))
  (apply pipe (cons pipeline args)))

;; struct stage : symbol ((any ... -> any) any ... -> any)
;;
;; The first argument to the body procedure is *always* a continuation procedure that passes
;; control to the next stage in the pipeline.
;;
;; The definition of stage takes advantage of MzScheme's "structures as procedures" functionality
;; such that stages can be called directly as if they are procedures. For example:
;;
;;     (define my-stage
;;       (make-stage 'my-stage
;;         (lambda (continue name age)
;;           (printf "Hello ~a, " name)
;;           (continue age))))
;;
;;     (my-stage
;;       (lambda (age)
;;         (printf "you are ~a years old!" age))
;;       "Dave" 27))
;;
;; would print:
;;
;;     Hello Dave, you are 27 years old!
(define-values 
  (struct:stage
   make-stage
   stage?
   stage-ref
   stage-set!)
  (make-struct-type
   'stage           ; name-symbol
   #f               ; super-struct-type
   2                ; init-field-k
   0                ; auto-field-k
   #f               ; auto-v
   null             ; prop-value-list
   (make-inspector) ; inspector-or-false
   1                ; proc-spec
   '(0)             ; immutable-k-list
   #f))             ; guard-spec

;; stage-name : stage -> symbol
;;
;; Returns the name associated with a stage.
(define stage-name
  (make-struct-field-accessor
   stage-ref        ; accessor-proc
   0))              ; field-pos-k

;; syntax (define-stage (name continue arg ...) expr ...)
(define-syntax define-stage
  (syntax-rules ()
    [(define-stage (name continue args ...)
       expr ...)
     (define name 
       (make-stage 
        'name
        (lambda (continue args ...)
          expr ...)))]
    [(define-stage (name continue args ... . rest)
       expr ...)
     (define name 
       (make-stage 
        'name
        (lambda (continue args ... . rest)
          expr ...)))]))

;; find-stage : (list-of stage) symbol -> (U stage #f)
;;
;; Returns the appropriately named stage in the specified pipeline,
;; or #f if such a stage cannot be found.
(define (find-stage pipeline name)
  (find (lambda (item)
          (eq? (stage-name item) name))
        pipeline))

;; replace-stage : (list-of stage) stage -> (list-of stage)
;;
;; Replaces the equivalently named stage in the supplied pipeline.
;; If no matching stage is found, simply copies the pipeline.
(define (replace-stage pipeline stage)
  (map (lambda (item)
         (if (eq? (stage-name stage) (stage-name item))
             stage
             item))
       pipeline))

;; delete-stage : (list-of stage) symbol -> (list-of stage)
;;
;; Deletes the appropriately named stage from the supplied pipeline
;; (if such a stage can be found).
(define (delete-stage pipeline name)
  (filter (lambda (item)
            (not (eq? name (stage-name item))))
          pipeline))

; Provide statements ---------------------------

(provide call-with-pipeline
         struct:stage
         make-stage
         stage?
         stage-name
         define-stage
         find-stage
         replace-stage
         delete-stage)