Exporting a variable from a macro

2 views
Skip to first unread message

AndrewW

unread,
Jan 21, 2009, 4:57:18 PM1/21/09
to
I'm fairly new to Scheme and have recently discovered the power of
define-syntax macros but I'm struggling with something that I'm hoping
someone can correct me on.

I'm writing a little program to automate windows applications like
this :-

(using-window "Home - Microsoft Internet Explorer"
(send-keys "{TAB}Hello")
(click-button "OK")
)

send-keys and click-button are procedures that operate on a window
handle (hwnd) so I could have written

(let ((hwnd (find-window "Home - Microsoft Internet Explorer")))
(send-keys hwnd "{TAB}Hello")
...
)

In order to avoid all that nasty passing around hwnd, I made send-keys
assume that hwnd was already defined and defined the macro using-
window :-

(define-syntax using-window
(syntax-rules ()
((using-window title expr...)
(let ((hwnd (find-window title)))
expr...)
)
)
)

and of course it doesn't work because the macro is hygienic and hwnd
only exists within the macro and not when it's expanded.

This desire for encapsulation must crop up all the time, what's the
correct way to do it in Scheme ?

Thanks

Andrew


Aaron W. Hsu

unread,
Jan 21, 2009, 7:12:13 PM1/21/09
to
Hello Andrew:

I hope I can help a little bit here...

AndrewW <and...@trailgauge.com> writes:

>I'm writing a little program to automate windows applications like
>this :-

>(using-window "Home - Microsoft Internet Explorer"
> (send-keys "{TAB}Hello")
> (click-button "OK")
>)

>send-keys and click-button are procedures that operate on a window
>handle (hwnd) so I could have written

Must these be separate procedures? It is possible for you to do this
exact example using either a syntax with aux keywords or to get rid of
the need for keywords entirely, by just passing USING-WINDOW three
arguments.

>(let ((hwnd (find-window "Home - Microsoft Internet Explorer")))
> (send-keys hwnd "{TAB}Hello")
>...
>)

>In order to avoid all that nasty passing around hwnd, I made send-keys
>assume that hwnd was already defined and defined the macro using-
>window :-

>(define-syntax using-window
> (syntax-rules ()
> ((using-window title expr...)
> (let ((hwnd (find-window title)))
> expr...)
> )
> )
> )

>and of course it doesn't work because the macro is hygienic and hwnd
>only exists within the macro and not when it's expanded.

>This desire for encapsulation must crop up all the time, what's the
>correct way to do it in Scheme ?

You could handle USING-WINDOW like CALL-WITH-CURRENT-INPUT-PORT

(define using-window
(lambda (name proc)
(proc (find-window name))))

(using-window "something" (lambda (hwnd) ...))

Or, you could introduce a name as well as the title:

(define-syntax using-window
(syntax-rules ()
[(_ id title expr ...)
(let ([id (find-window title)])
(void)
expr ...)]))

(using-window hwnd "title" (send-keys hwnd ...))

But regardless, you are going to have to deal with a window object
somehow, and you need to obey the scoping rules. This isn't really an
issue of encapsulation, I think. What you are wanting is a shortcut to
avoid having to pass a specific name all the time. You can use
parameters to store these values, such as WITH-INPUT-FROM-STRING does.

If this really is an issue of encapsulation, then the normal way of
doing it *is* to use a let to bind the handle only in the scope you
want. If it's a matter of saving a few keystrokes, there are some
non-hygenic solutions, but I don't think that's a good idea for this.

If you are relaly so addicted to the api you gave, you could possibly do
it with something like a maker:

(define make-window-procs
(lambda (title)
(let ([hwnd (find-window title)])
(values (lambda (blah) (send-keys hwnd blah))
(lambda (blah) (something hwnd blah))))))

(let-values ([(send-keys something) (make-window-procs title)])
(send-keys "...")
(something "..."))

But that may or may not be what you want. Module systems are another
way of hiding values for encapsulation. As are internal definitions.

The bottom line is that I would recommend you reevaluate the way you are
designing your library. You can do what you want, but I don't recommend
it.

--
Aaron W. Hsu <arc...@sacrideo.us> | <http://www.sacrideo.us>
"Government is the great fiction, through which everybody endeavors to
live at the expense of everybody else." -- Frederic Bastiat
+++++++++++++++ ((lambda (x) (x x)) (lambda (x) (x x))) ++++++++++++++

AndrewW

unread,
Jan 22, 2009, 5:36:27 AM1/22/09
to
Aaron,

Thanks kindly for your answer which has some useful suggestions. I
didn't really like the idea of putting the hwnd as an extra argument
although functionally it was probably the correct thing to do because
it is needless typing and detracts from the readabilty. Whilst it
wouldn't be a problem for this simple example, imagine a case where a
larger set of data was setup and needed to be shared. Also I read the
chapter on makers in SICP and these would be a nice idea but each of
my operations would have to be messages to pass into the function and
I couldn't therefore mix operations on the hwnd with other arbritary
scheme operations. In the end I went for a dirty CL style macro :-

(define-macro (u-w-m title proc)
`(let ((_hwnd (find-window ,title)))
,proc
)
)

Gambit seems to support these with greater ease than define-syntax. I
know it's disgusting and possibly not very portable but the rest of
the program depends on c-lambdas so that won't be portable either.

The one disadvantage with this is it will only accept one s-expression
so I have to wrap the operations in a (begin ...) but I can fix this
with a Scheme macro on top :-

(define-syntax using-window
(syntax-rules ()
((using-window title expr ...)

(u-w-m title (begin expr ...))
)
)
)

I can hear your scorn already :-)

Thanks again - I really appreciate the service that you and others
provide via this group to us novices.

Andrew

Thomas Munro

unread,
Jan 22, 2009, 6:47:57 AM1/22/09
to
On Jan 22, 10:36 am, AndrewW <and...@trailgauge.com> wrote:
> Aaron,
>
> Thanks kindly for your answer which has some useful suggestions. I
> didn't really like the idea of putting the hwnd as an extra argument
> although functionally it was probably the correct thing to do because
> it is needless typing and detracts from the readabilty. Whilst

Hi Andrew

I think the approach Aaron was suggesting when he talked about current-
input-port could be done using optional window arguments. Then you
can use a scope 'with-' macro for good readability, but you still
retain the option of passing explicit windows to functions when
necessary (say, when you want to interact with two different windows
within the same scope). Here is my attempt at doing something like
that:

;; A dynamic variable in the style of current-input-port (SRFI 39).
(define current-window (make-parameter #f))

;; Looks up a window. For this example just returns the name itself.
(define (find-window name)
name)

;; Sends keys to a window. The second parameter is an optional
window, but
;; if not provided, then the parameter current-window is used. This
is
;; modelled on display, read, write and friends which use current-XXX-
port
;; if you don't specify a port.
(define (send-keys data . rest)
(let ((window (if (null? rest)
(current-window)
(car rest))))
(display "send-keys: sending ")
(display data)
(display " to window ")
(display window)
(newline)))

;; A syntax for looking up a window, and making it current for a
scope.
(define-syntax with-window
(syntax-rules ()
((_ name expression ...)
(parameterize ((current-window (find-window name)))
expression ...))))

;; Example of using the macro.
(with-window "Home - Microsoft Internet Explorer"
(send-keys "{TAB}Hello"))

;; Example of using explicit windows.
(let ((window (find-window "Windows is pants")))
(send-keys "{CTRL-ALT-DELETE}" window))

- Thomas

AndrewW

unread,
Jan 22, 2009, 7:27:42 AM1/22/09
to
Thomas,

That looks perfect and more versatile, I've not come across make-
parameter and parameterize before - I'll read up on them. I presume
that is copes with this sort of thing ? :-

(with-window "first window"
(send-keys "first window keys")
(with-window "second window"
(send-keys "second window keys))
(send-keys "more first window keys")
)

I'll give it a try.

AndrewW

unread,
Jan 22, 2009, 8:58:17 AM1/22/09
to
Well after some wrestling with Gambit, I discovered that define-syntax
can't be used with Gambit special forms of which parameterize is one.
So I changed with-window to :-

(define-syntax with-window


(syntax-rules ()
((using-window title expr ...)

(let ((old-window (current-window)))
(current-window (find-window title))
expr ...
(current-window old-window)
)
)
)
)

which saves away the value of current-window parameter, assigns it to
your new window and then restores the old one when you return. All
seems to work.

I had a great deal of difficulty getting Gambit to recognise define-
syntax first in the interpreter and then again when compiler. It seems
that you need to pass the -:s parameter to both gsi and gsc in order
to use define-syntax.

Thanks again both, I now have a solution that I'm happy with and I've
learnt a little more in the process.

Andrew

Thomas Munro

unread,
Jan 22, 2009, 9:03:38 AM1/22/09
to
On Jan 22, 1:58 pm, AndrewW <and...@trailgauge.com> wrote:
> (define-syntax with-window
> (syntax-rules ()
> ((using-window title expr ...)
> (let ((old-window (current-window)))
> (current-window (find-window title))
> expr ...
> (current-window old-window)
> )
> )
> )
> )

But what happens if expr ... raises an exception? How about something
like this:

(define-syntax with-window
(syntax-rules ()
((_ name expression ...)

(let ((old-window (current-window)))
(dynamic-wind
(lambda () (current-window (find-window name)))
(lambda () expression ...)
(lambda () (current-window old-window)))))))

Thomas Munro

unread,
Jan 22, 2009, 9:21:03 AM1/22/09
to
On Jan 22, 1:58 pm, AndrewW <and...@trailgauge.com> wrote:
> Well after some wrestling with Gambit, I discovered that define-syntax
> can't be used with Gambit special forms of which parameterize is one.

Alternatively you could ditch define-syntax and keep parameterize:

(define-macro (with-window name . rest)
`(parameterize ((current-window (find-window ,name)))
,@rest))

Grant Rettke

unread,
Jan 23, 2009, 2:37:26 PM1/23/09
to
Hi Andrew,

Great question. I always wonder what is the idiomatic way to solve
problems in Scheme. It is helpful and fun to see how other people
solve it.

That said, you mention two topics:

1. How to deal with this particular example.
2. Encapsulation in general in Scheme

I assume you only care about #1 in this post, so my reponse is to that
topic.

I would deal with the problem by starting out with a simple functions
as humanly possible. Those functions are small, easy to document and
easy to test: find-window, send-keys, and click-button are all
candidates here. Write and test those.

Next, write a function called "using-window" that takes a window name
and "the work that you want to do", as you have listed. You can do all
of this in a function. The problem you need to solve, though; is how
to passa HWND reference to all of the functions that want to operate
on that window, when you don't know what is the HWND for that window
yet? The solution is simple, use lambda.

You can create thunks for each of the thunks of work you want to
perform, and within using-window you can create a binding for the HWND
and pass it to all of the thunks. Still, you want a nice syntax. The
first step is to simply creation of the list of thunks; it is a
trivial change but "nice" nonetheless. The big one is to write a macro
that provides for you your original syntax:

(using-window "Home - Microsoft Internet Explorer"
(send-keys "{TAB}Hello")
(click-button "OK"))

You can use this syntax, hygienically, if you satisfy one contract:
Every function that works with a window (given its HWND) will take the
HWND reference as its first argument. When you stick with that
approach, you can simply compile down your desired shape into the one
that satisfies the contract you defined here. The following code walks
through the whole thing; along with providing macro expansions. It
should work on any R5RS with syntax-case, or R6RS (I tested it in PLT
#lang scheme and Chez 7.4). An aside, if you want you could use
SRFI-26 to create the thunks.

Let me know what you guys think:

; find-window : string -> HWND
; to determine the Window handle for the window with the given title
(define (find-window title)
; do something
1024)

; send-keys : HWND list -> boolean
; to generate "key events" on the window specified by the handle for
the given
; list of keys
(define (send-keys hwnd keys)
; do something
#t)

; click-button : HWND string -> boolean
; to generate a "click event" on the for the button specified by the
label
; that exists on the window specified by the handle.
(define (click-button hwnd label)
; do something
#t)

; using-window-v1 : string list-of-thunks -> list-of-booleans
; a helper function to provide a convenient syntax for looking up
; windows and performing actions upon them in an unspecified order
until
; one of them fails. Each thunk expects the HWND of the window
specified by
; the title string as an argument.
(define (using-window-v1 title thunks)
(let ((hwnd (find-window title)))
(map (lambda (thunk) (thunk hwnd)) thunks)))

(display
(using-window-v1


"Home - Microsoft Internet Explorer"

(list
(lambda (hwnd) (send-keys hwnd "{TAB}Hello"))
(lambda (hwnd) (click-button hwnd "OK")))))
(newline)

; using-window-v2 : syntax
; a helper syntax to make using using-window-v1 more pleasant
(define-syntax using-window-v2
(syntax-rules ()
((_ title thunks ...)
(using-window-v1 title (list thunks ...)))))

(display
(using-window-v2


"Home - Microsoft Internet Explorer"

(lambda (hwnd) (send-keys hwnd "{TAB}Hello"))
(lambda (hwnd) (click-button hwnd "OK"))))
(newline)

; =>

;(using-window-v1
; "Home - Microsoft Internet Explorer"
; (list
; (lambda (hwnd) (send-keys hwnd "{TAB}Hello"))
; (lambda (hwnd) (click-button hwnd "OK"))))

; using-window-v3 : syntax
; a helper syntax to make using using-window-1 even more pleasant
; by reusing using-window-v2. This macro makes the assumption that
; all functions that operate on HWND refences ALWAYS take the HWND as
the first
; argument to the function. Consequently, expressions that are
enclosed by this
; macro adhere to this approach and we can rewrite them accordingly.
(define-syntax using-window-v3
(syntax-rules ()
((_ title (fun args ...) ...)
(using-window-v2
title
(lambda (hwnd) (fun hwnd args ...)) ...))))

(display
(using-window-v3


"Home - Microsoft Internet Explorer"
(send-keys "{TAB}Hello")

(click-button "OK")))
(newline)

; =>

;(using-window-v1
; "Home - Microsoft Internet Explorer"
; (list
; (lambda (hwnd) (send-keys hwnd "{TAB}Hello"))
; (lambda (hwnd) (click-button hwnd "OK"))))

; You may wnat to use SRFI-26 to create the thunks.

Aaron W. Hsu

unread,
Jan 24, 2009, 2:35:02 PM1/24/09
to
Grant Rettke <gre...@gmail.com> writes:

>Next, write a function called "using-window" that takes a window name
>and "the work that you want to do", as you have listed. You can do all
>of this in a function. The problem you need to solve, though; is how
>to passa HWND reference to all of the functions that want to operate
>on that window, when you don't know what is the HWND for that window
>yet? The solution is simple, use lambda.

I think Andrew's problem is not passing the HWND to the procedures, but
how to do so without having to explicitly pass them.

>You can create thunks for each of the thunks of work you want to
>perform, and within using-window you can create a binding for the HWND
>and pass it to all of the thunks. Still, you want a nice syntax. The
>first step is to simply creation of the list of thunks; it is a
>trivial change but "nice" nonetheless. The big one is to write a macro
>that provides for you your original syntax:

This is of course one very common idiom for doing things, but it still
requires the explicit passing of the hwnd value to all the procedures,
and then wrapping lot's of single argument procedures around this code.
In the end, this is what Andrew -- I believe -- is trying to avoid.
[Also, as a side note, "thunks" are procedures of no arguments.]

However, if he really wants to eliminate such things, and wants to do it
in clean ways, as has already been discussed, one could use parameters,
or do some rather nasty macrology to get it working.

Another option are fluid bindings:

(define hwnd)

(define-syntax using-window
(syntax-rules ()
[(_ title expr1 expr2 ...)
(fluid-let ([hwnd (find-window title)])
expr1 expr2 ...)]))

This is essentially just another way of using parameterize and
parameters, but it's a little more flexible. Parameters are probably
nicer if you can use them.

Reply all
Reply to author
Forward
0 new messages