Wednesday, January 07, 2009

A Scheme Application Configuration Strategy

I've developing a Scheme app for a client, and of course, the people he's selling it to want customizations. Some of the customizations were pretty basic and I simply stored them in a prefab structure on disk.

Lately he's picked up a client that wants all sorts of random'ish customizations - from changes in labels on the screen to application functionality. Storing this information in a structure just wasn't going to work. I needed a more dynamic and scalable approach. Here's the rough idea I came up with.

First, every client can have their own customizer function. The implementation of each client's customizer is in their own scheme module. A customizer has the signature:

(define (foo-customer-customizer key default)
 ...)

We'll come back to how these arguments are used in just a moment. To make the customizer available to the rest of the code, I make use of a paramter, which is a kind of cleaner approach to global variables. At the beginning of my application, I effectively say:

(define the-customizer
  (make-parameter (match customer-name
                    ["foo" foo-customer-customizer]
                    ...)))

Finally, let's define a helper function so we don't need to see the-customizer throughout our code:

(define (cv key default)
 ((the-customizer) key default)))

OK, now we can get to the interesting stuff. Let's look at how this can be used. Let's say I want to customize a button on the Welcome screen, I can say:

(new button% [label (cv '(welcome-screen enter-button) "Welcome")] ...)

And suppose I'd like to change what happens when the hits the exit button:

(new button% [callback (lambda (b e)
                         (let ([f (cv '(application exit-button action) quit)])
                           (f)))] ...)

Or perhaps the customer likes to see people's name in all upper case:

 (define choices (map (cv '(format names) identity)
                          (get-user-names)))

You might need to customize the value based on some context, such as whether a row is odd or even:

(for-each (lambda (row-number row-value)
            ((cv `(ui table format ,row-number ,row-value) list) 
             row-number row-value))
          (get-row-numbers)
          (get-row-values))

Here's how this customer's customizer could be implemented:

(require scheme/match)
(define (foo-customer-customizer key default)
  (match key
    [`(welcome-screen enter-button) "Ahoy-hoy"]
    [`(application exit-button action) 
      (lambda () (ask-user "Are you sure?" ...) ...)]
    [`(format-names) string-upcase]
    [`(ui table format ,row-number ,row-value)
      (if (odd? row-number) (handle-odd-row row-value) (handle-even-row row-value))]
    ;; No match for our key, choose to return the default  
    [_ default]))

I realize that this isn't exactly rocket science, but there were a few aspects of this solution I was especially happy with:

  • Adding in new configuration points using (cv ...) was easy to do
  • The list notation for a key (ui table format ...) turned out to be both natural and extensible
  • Having functions be first class values here absolutely makes all the difference. Instead of being limited to customizing strings, I can customize pretty much anything I want.
  • The customer's customizer functions turned out relatively clean, with no need to try to account for every customized value in the system.
  • The use of (match ...) was absolutely key, as it allowed me to skip writing a special parser for configuration values. The fact that it handles including values along with constant data, such as `(format ui table ,row-number) makes it especially flexible.

When it comes down to it, this solution could have been written in any language, yet it turned out to be quite naturally done in Scheme.

5 comments:

  1. This is essentially a super-lightweight AOSD tool, right?

    Along these lines: pretty soon, you're going to run into scoping / hygiene issues. Say, for instance, you have this code:

    (oh heck, I can't use "pre" or "code" tags...)

    (let ([customer-name (get-customer-name)])
    (cv '(application render-name)
    (show-string customer-name
    (lookup-address customer-name))))

    Note that 'customer-name' is used twice in the customizable piece of code.

    New if you want to customize this, you're probably going to want to be able to write a piece of code that refers to 'customer-name'.

    Of course, you can solve this problem by refactoring so that the customizable point is a fresh function that takes the customer-name as an argument, but at that point I would suggest you're probably better off just re-using an existing framework for generic functions.

    John Clements

    ReplyDelete
  2. Did you look to DrScheme for inspiration?

    ReplyDelete
  3. What approaches did you consider that you didn't like or didn't pan out?

    ReplyDelete
  4. John -

    Thanks for the feedback - that's an interesting suggestion about how generic functions could play out here.

    I'll have to keep an eye out to see if the configuration pattern grows in that direction.

    ReplyDelete
  5. Grant -

    I actually didn't investigate customization all that much. I had started off storing some customized values in a structure - like, say the max number of floors in a building this client may have.

    But, it quickly became clear that have a structure to hold onto all this configuration data was just not going to work.

    The structure would be huge, and it would require every client to fill out their structure completely.

    So, I basically stumbled on this approach (not that it's rocket science - it's pretty basic) and really liked the shape of it.

    I need a better story than this ;-)

    ReplyDelete