Let's say I want a particular piece of code to execute at most three times within the execution of a larger program.
The obvious code is something like
(define globalcounter 3)
and where I have the piece of code I wrap it in
(if ( > globalcounter 0)
(begin (piece-of-code) (set! globalcounter ( - globalcounter 1)))
(complain)
)
I might want to have a macro.
(atmostthrice foo)
would expand to
(if ( > globalcounter 0)
(begin foo (set! globalcounter ( - globalcounter 1)))
(complain)
)
Then I could use this macro if there were other things I wanted to restrict to excuting thrice:
(thrice dothis)
and elsewhere
(thrice dothat)
But this is wrong! (thrice dothis) and (thrice dothat) will end up sharing the same counter. If I dothis twice, I will no longer be able to count out three executions of dothat.
Somehow I need to generate a new global counter for each use of thrice.
I there any way of getting each static use of to a macro to generate a new initialized global variable?
I've thought of the following ugliness, but I don't know if it is defined to work:
(thrice foo)
could expand to
(define localglobal `,(mcons 3 #nil) )
(if ( > (mcar localglobal) 0)
(begin foo (set-mcar! localglobal ( - (mcar localglobal) 1)
(complain)
)
thereby embedding a mutable cell in the macro-expanded source code, but that seems unconscionably cute. I suspect it would fail in an implementation where compilation is separated from execution.
-- hendrik
Your macro can use syntax-local-lift-expression to ask the expander to create a fresh module-level variable for you:
#lang racket
(require syntax/parse/define)
(define-syntax-parse-rule (atmostthrice body:expr ...+)
#:with globalcounter (syntax-local-lift-expression #'3)
(cond
[(= 0 globalcounter)
(error "too many times")]
[else
(set! globalcounter (sub1 globalcounter))
body ...]))
(define (try-it)
(atmostthrice
(displayln "run"))
(try-it))
(with-handlers ([exn:fail? values])
(try-it))
(try-it)
Note that this version is not thread-safe in the sense that it may be interrupted between the check of the counter and the update. Here's a better version:
#lang racket
(require syntax/parse/define)
(define-syntax-parse-rule (atmostthrice body:expr ...+)
#:with globalcounter (syntax-local-lift-expression #'(box 3))
(let retry ()
(define old (unbox* globalcounter))
(cond
[(= 0 old)
(error "too many times")]
[(box-cas! globalcounter old (sub1 old))
body ...]
[else
(retry)])))
(define (try-it)
(atmostthrice
(displayln "run"))
(try-it))
(with-handlers ([exn:fail? values])
(try-it))
(try-it)
1 Like
For completeness, here's a version that uses a semaphore instead of an atomic box. You might prefer this if you expect there to be contention, because Racket guarantees:
Semaphore waiting is fair: if a thread is blocked on a semaphore and the semaphore’s internal value is non-zero infinitely often, then the thread is eventually unblocked.
(But surely you would not be doing this if you expected there to be contention!)
#lang racket
(require syntax/parse/define)
(define-syntax-parse-rule (atmostthrice body:expr ...+)
#:with sema (syntax-local-lift-expression #'(make-semaphore 3))
(sync/timeout (λ ()
(error "too many times"))
(handle-evt sema
(λ (ignored)
body ...))))
(define (try-it)
(atmostthrice
(displayln "run"))
(try-it))
(with-handlers ([exn:fail? values])
(try-it))
(try-it)
1 Like
It is one of the pattern directives supported by syntax-parse and related forms: the are documented in this section of the manual.