Implementing debug vs production versions?

Suppose I have this code:

(define (foo)
  (if (getenv "DEBUG")
       plausible-test-value
       (expensive-function-call-that-determines-the-actual-value)))

Perhaps the production version requires a network connection, or is too time-consuming to want to do repeatedly during testing (e.g., it parses data out of a massive file).

Obviously, there are some concerns here, such as the fact that plausible-test-value needs to be kept in sync with what would actually be returned by the expensive function call. Leaving those aside for the moment, it bothers me that (a) this looks ugly and (b) I don't want to pay the getenv cost every time this is run in production. Back in the bad old days of C I could have used the preprocessor to compile in only one of the versions depending on what the environment was at compile time. I feel like there's a way to do the same thing in Racket but I can't quite wrap my head around it. Any suggestions, either on that or on the overall idea?

1 Like

One possible way:

A file called "config.rkt", part of your source, that looks like this:

#lang racket

(define production #f)

Other files can require this. If you want to avoid runtime checks, you can use macros to provide different definitions of specified functions depending on the value of the production binding. If you don't want to require production everywhere, you can define your own #lang that just requires this file (though frankly, it's probably clearer just to require the file where you need it).

Are there obvious problems with this approach?

2 Likes

Huh. No, actually, that seems pretty good. The one drawback is that it requires actually changing a file in order to alter the behavior, but that's a minor issue. Thank you.

2 Likes

When I teach my Sw Dev course, I suggest two sys config ideas:

— re-direct a file into your `main` (possibly via a Makefile script) 
— dynamically require configuration modules 

Neither requires changing a file.

1 Like

To expand on this idea:

For a web app I used a config.rkt that defined a number of parameters (as in make-parameter), the default values of which came from getenv. I mostly used this for things like port numbers, database connection items, AWS connection values, etc.

I ended up DRY-ing this with a macro; this might be overkill for what you need/want.

(define non-secret (make-hash))
(define (config-summary)
  (for/hash ([(k v) (in-hash non-secret)])
    (values k (v))))

;; <base-name> is used both (1) to define a parameter named
;; current-<base-name> and (2) to get an environment variable named
;; "WEB_<BASE_NAME>" (note conversion of kebab-case to SNAKE_CASE).
(define-syntax-parser dp
  [(_ base-name:id
      default:expr
      contract:expr
      (~optional (~seq #:coerce coerce:expr)      #:defaults ([coerce #'values]))
      (~optional (~seq #:secret? secret?:boolean) #:defaults ([secret? #'#f])))
   (with-syntax ([id-name  (format-id #'base-name
                                      "current-~a"
                                      (syntax-e #'base-name))]
                 [env-name (string-upcase
                            (regexp-replace* #px"-"
                                             (format "WEB_~a"
                                                     (syntax-e #'base-name))
                                             "_"))])
     #'(begin
         (define/contract id-name (parameter/c contract)
           (make-parameter (match (getenv env-name)
                             [#f default]
                             [v  (coerce v)])))
         (provide id-name)
         (unless secret?
           (hash-set! non-secret 'id-name id-name))))])

Example uses:

(define scheme? (or/c "http" "https"))
(define port-num? (and/c exact-integer? (between/c 1 65535)))

;; For the actual Racket web server
(dp internal-port  8080 port-num? #:coerce string->number)

;; Values path->external-url should use to form URLs for user. These
;; are distinct to support the Racket server being behind some other
;; server e.g. ELB or nginx.
(dp external-scheme "http"      scheme?)
(dp external-host   "localhost" string?)
(dp external-port   8080        port-num? #:coerce string->number)

Anyway to tie this back to the original question, another example was:

(dp production #f boolean? #:coerce string->boolean)

And I could imagine doing something similar for the value foo returns?

3 Likes