I stumbled on this Scheme development pattern last night, and thought it would be worth sharing. My guess is that this is probably already a well established (and better named) pattern, but I can't resist sharing it none the less.
The Problem:
I'm developing this small GUI app using MrEd, and was paying special attention to the code that links the GUI objects to the actual logic of the system.
My preference is to try to decouple the GUI widgets as much as possible from the underlying functionality of the system. This allows me to program the logic of the app in a fairly functional style, and not be tied to the nuances of a particular GUI.
I've got a couple of callbacks on buttons that actually kick off the logic of the app, and I ended up passing in GUI widgets themselves into the function that creates these callbacks. I essentially ended up with:
(define (make-buttons frame logging-on?-checkbox algorithm-choice) (new (button% [label "Foo"] [callback (lambda (b e) (let ([logging-on? (send logging-on?-checkbox get-value)] [algorithm (send algorithm-choice get-value)]) ;; ... do things with logging-on? algorithm ;; that aren't GUI related ))])))
This was OK, because the transition from GUI land to the more functional logic of the app was contained in one spot.
One Solution
It hit me - GUI objects, as a side effect, do all GUI magic at instantiation time. After that, all I want to do is access and set values on them. In fact, wouldn't it be nice to be able to treat them like paremters? That is, access them like they are functions that you can get and set a value from. The fact that they are backed by a GUI control isn't really relevant to the rest of the code.
I quickly rigged up a gui-accessor module:
#lang scheme (require scheme/gui) (provide make-gui-accessor mga) (define (get-value gui-instance) (cond [(is-a? gui-instance message%) (send gui-instance get-label)] [(is-a? gui-instance choice%) (send gui-instance get-string-selection)] [(is-a? gui-instance text-field%) (send gui-instance get-value)] [(is-a? gui-instance check-box%) (send gui-instance get-value)] [else (error (format "Unknown instance type: ~a" gui-instance))])) (define (set-value! gui-instance v) (cond [(is-a? gui-instance message%) (send gui-instance set-label v)] [(is-a? gui-instance choice%) (send gui-instance set-string-selection v)] [(is-a? gui-instance text-field%) (send gui-instance set-value v)] [(is-a? gui-instance check-box%) (send gui-instance set-value v)] [else (error (format "Unknown instance type: ~a" gui-instance))])) (define (make-gui-accessor gui-instance) (lambda arg (cond [(null? arg) (get-value gui-instance)] [else (set-value! gui-instance (car arg))]))) (define mga make-gui-accessor)
This is pretty basic code - it provides a single function make-gui-accessor that takes in a GUI control and returns back a procedure. If you invoke this procedure with no arguments you get the value of the control, and if you invoke it with a single argument it sets the control.
I aliased mga to make-gui-accessor to make this especially terse.
Now, in my above example, when I create a check-box I say:
(let ([logging-on? (mga (new check-box% [label "Enable Logging?"] [parent frame]))]) ...)
cb above is now ready to be used in a procedure context that doesn't need to know anything about GUI controls.
My function above now reduces to:
(define (make-buttons frame logging-on? algorithm) (new (button% [label "Foo"] [callback (lambda (b e) ;; Get the value of logging-on? by saying (logging-on?) ;; and the value of algorithm by saying (algorithm) ;; No GUI logic needed here. ))])))
Is this rocket science? Definitely not. But it does feel like a simplification that will make working with a GUI that much easier.
Thoughts? Am I missing something obvious? Is this terrible style? Is this already done? Feedback appreciated.
I only just read this and it seems neat- I'm going to Try it out. How has it worked out for you?
ReplyDelete