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.