Given the quality and depth of prior comments in this thread, I'm a
little reluctant to chime in.  I suppose, however, that people on this
list tend to be forgiving and maybe my much less elegant solution will
be helpful, at least as inspiration.  This solution is not originally
intended for the problem you're asking about, but it can be used for
your problem.

Effectively, you want to carry state forward in time from one function
to another.  I had a somewhat related problem in that I wanted to
schedule tasks for the future.  Obviously, I needed a job queue.
Fortunately, Racket has one:
https://docs.racket-lang.org/job-queue/index.html  (Hat tip to Jay
McCarthy)

I wanted to be able to schedule them for the future.  One way to do
that would be to make up a job like this:

(thunk
   (sleep 60)
   (do stuff))

...and then hand it to the worker queue.  It would run in its own
thread so as not to block the main thread, so scheduling issue solved.
The problem with this is that the jobs are not durable; if the Racket
process crashed then all the jobs would go up in a puff of magic smoke
with no way to recover them.

Ah, durability, you say?  Isn't that what the 'D' in DB stands for?
(Answer: no, it's not.)  Okay, database sounds good.  So, I created a
'tasks' table that would store details about the job that I wanted to
run and when to run them.  Periodically a thread would check the
table, grab a job that was eligible, and run it. It would have been
nice to be able to shove straight Racket code in there, but that would
have required 'evil' -- I mean, 'eval'.  Instead, I stuffed in a JSON
blob that looked (in part) like this:

{  "task" : "sync-file-list-for-team", "args" : [ "team name"] }

Then I did the ugly part:  I created a global variable.

Said variable was a hash table (yes, yes, John, I know:  structs are
better than hashes for most things.  You  convinced me in the general
case, but for this I really did need something free-form) that looked
like so:

(hash   "sync-file-list" (lambda (args-list) ...stuff...))

You can recover the function by doing:

(define task  (...database query that retrieves the JSON object and
parses it into a hash...))

; 'task' is something like (hash "task" "sync-file-list "args" (list
"team name"))

(define func (function-from task)) ; take the value of the "task" key,
look it up in the global jumptable, retrieve the lambda stored there
(define args (args-from task))  ; get the list from the task hash
(func args) ; run it


Anything that wants to schedule a task can now do this:

(require "lib/workers.rkt")

(define/contract (sync-file-list-for-team lst)
  (-> list? any)
    ...do stuff...)

(register-functions     sync-file-list-for-team)

(add-task
    db-connection
    #:task sync-file-list-for-team
    #:args (list "team name")
    #:delay 90 ; will not run for *at least* 90 seconds.  No guarantee
about when it *will* run
    #:priority 10  ; default is 0, bigger is higher priority
)

Or, if you'd prefer to be able to pass fancier state:

(register-functions (list "extra-args-for-a-func" (thunk (list 'x 'y
(thunk 7)))))
(define (a-func lst)
    (define extra-args ((function-from (hash "task" "extra-args-for-a-thunk"))))
    ; extra-args is now (list 'x 'y (thunk 7))
    ...)

The tasks are now durable, since they're in the DB.  If the Racket
process crashes then the jumptable will go away, but as long as your
register-functions calls are at top level then the jumptable will be
rebuilt when the module is next loaded.


Like I said, this isn't exactly the problem you were asking about --
there's no macros involved, for one thing -- and it's way less elegant
than the other solutions that people have been offering.  Despite
that, I've found it useful and so I thought I'd share.

On Sat, Oct 14, 2017 at 7:37 PM, 'John Clements' via Racket Users
<racket-users@googlegroups.com> wrote:
> Lovely! I was thinking along these lines, but you hit it out of the park. 
> Sounds like you must be avoiding some really important task!
>
> John
>
>> On Oct 14, 2017, at 11:31, Ryan Culpepper <ry...@ccs.neu.edu> wrote:
>>
>> On 10/14/2017 05:01 AM, George Neuner wrote:
>>> On 10/14/2017 3:00 AM, Jack Firth wrote:
>>>>
>>>>    So is there a way ... from normal code ... to get at the locals of
>>>>    functions higher in the call chain?  Or at least the immediate
>>>>    caller?
>>>>    Some reflective capability that I haven't yet discovered?
>>>>
>>>>
>>>> I'm not sure if there's a way to do that, but I'm wondering if what you 
>>>> want to do can be achieved more simply with plain functions and a very 
>>>> small macro wrapper. In particular, I suspect putting too much logic in 
>>>> the macro is what led you to eval which is the root of all evil. From what 
>>>> I can tell there shouldn't be any need at all for eval or any sort of 
>>>> dynamic runtime compilation to do things like what you're describing. 
>>>> Could you give a few more details about your use case? Ideally with some 
>>>> example code illustrating the problem?
>>> Basically, this is a sort of Unix at-like function for flexible scheduling. 
>>>  It takes an expression containing dates, times, certain keywords and 
>>> arbitrary numeric expressions, and it produces seconds since the epoch.  
>>> Code is attached - hope it survives posting to the list.  It should be 
>>> runnable as is.
>>> What led me to eval originally was wanting to reference arbitrary 
>>> functions/variables from the runtime environment.  I'd like to be able to 
>>> say
>>> things like:
>>>    (let [(y 42)] (schedule (at now + y mins) ...))
>>> and similar involving top-level defined functions [which already works with 
>>> eval].
>>> I know that eval is not needed if I generate inline code rather than having 
>>> the macro invoke a normal function.  But that is complicated by having to 
>>> deal with free form expressions: they can have internal references between 
>>> things which are not necessarily adjacent.  It is doable, but at some 
>>> expense and not very cleanly.
>>> I started out going the "compile" route - just generating inline code.  But 
>>> as more functionality was added, that became unwieldy. So I switched to a 
>>> runtime function. Right now the assoc list code is overly complex [so 
>>> please ignore it] - it is there as a debugging tool until I get everything 
>>> working exactly right.
>>
>> Your example above shows that you want the `at` macro to accept a mixture of 
>> Racket expressions and syntax that you interpret. If you want to accept 
>> Racket expressions, you must leave them as syntax objects. Once you flatten 
>> them with `syntax->datum`, there is *no way* to *correctly* recover their 
>> meaning.
>>
>> Here's my attempt to adapt the HtDP design recipe to a scaled-down version 
>> of this problem.
>>
>> First, let's think about the syntax (grammar) that `at` should accept. Let's 
>> call it a Time. What are some reasonable Times?
>>
>>  now
>>  now + 30     ;; means 30 seconds
>>  today
>>  tomorrow + 20 mins
>>  now + x mins + (/ ms 1000) secs
>>  "2017-10-14 1:17:25" + 30 mins
>>
>> But there are also some terms that are nonsense as Times:
>>
>>  12
>>  today + tomorrow
>>  now mins
>>
>> Let's say (as a place to start) that a Time consists of an absolute 
>> reference point (like now or today) and some number of offsets. Here's a 
>> grammar:
>>
>>  ;; Time = TimeBase {+ TimeExt}*
>>  ;; TimeBase = now | today | String[Date]
>>  ;; TimeExt = Expr[Real] MaybeUnit
>>  ;; MaybeUnit = <nothing> | secs | mins
>>
>> Let's call the *meaning* of a Time a TimeVal, and let's represent it as a 
>> real number of seconds, using the same epoch as (current-seconds).
>>
>>  ;; A TimeVal is a real number of seconds
>>  ;; using the same epoch as (current-seconds)
>>
>> The meaning of TimeBase is also a TimeVal. The meanings of TimeExt and 
>> MaybeUnit are both Real (they are duration and durations scales; they have 
>> no epoch).
>>
>> Now to translate that plan to Racket.
>>
>> First, turn keywords like `now` into "illegal use" macro definitions:
>>
>>  (begin-for-syntax
>>    (define (at-keyword stx)
>>      (raise-syntax-error #f "illegal use of `at` keyword" stx)))
>>  (define-syntax now at-keyword)
>>  (define-syntax today at-keyword)
>>  (define-syntax secs at-keyword)
>>  (define-syntax mins at-keyword)
>>
>> Then define syntax classes for the nonterminals in the grammar. I'll also 
>> define an `expr` attribute to compute the meaning of each term. For example, 
>> the syntax class for TimeBase is
>>
>>  (begin-for-syntax
>>    (define-syntax-class TimeBase
>>      #:attributes (expr) ;; expression of TimeVal
>>      #:literals (now today)
>>      (pattern now
>>               #:with expr #'(current-seconds))
>>      (pattern today
>>               #:with expr #'(today-fun))
>>      (pattern s:str
>>               #:with expr #'(parse-date-string s))))
>>
>> To avoid complicating the syntax class and also to avoid making the 
>> expansion bigger than necessary, factor run-time behavior out into helper 
>> functions, like `today-fun`:
>>
>>  ;; today-fun : -> TimeVal
>>  (define (today-fun)
>>    (define today (current-date))
>>    (find-seconds 0 0 0 (date-day today) (date-month today) (date-year 
>> today)))
>>
>> Here's the syntax class for TimeExt. It uses `expr/c` to make sure the given 
>> Racket expression actually produces a number.
>>
>>  (begin-for-syntax
>>    (define-splicing-syntax-class TimeExt
>>      #:attributes (expr) ;; expression of Real
>>      (pattern (~seq (~var r (expr/c #'real?)) u:MaybeUnit)
>>               #:with expr #'(* r.c u.expr))))
>>
>> Finally, `at` just wraps the work done by the syntax classes up as a macro:
>>
>>  ;; (at Time) -> TimeVal
>>  (define-syntax at
>>    (syntax-parser
>>      [(_ t:Time) #'t.expr]))
>>
>> See the attached file for some of the missing pieces, like testing. It 
>> should be possible to scale this approach up to the Time grammar you want.
>>
>> ----
>>
>> The other question you should ask is whether the benefit of the macro is 
>> worth the cost to write it. Is `(at today + 30 mins + x secs)` that much 
>> better than `(+ (today) (mins 30) x)`, which you could support with just a 
>> few function definitions?
>>
>> Ryan
>>
>> --
>> You received this message because you are subscribed to the Google Groups 
>> "Racket Users" group.
>> To unsubscribe from this group and stop receiving emails from it, send an 
>> email to racket-users+unsubscr...@googlegroups.com.
>> For more options, visit https://groups.google.com/d/optout.
>> <at.rkt>
>
>
>
> --
> You received this message because you are subscribed to the Google Groups 
> "Racket Users" group.
> To unsubscribe from this group and stop receiving emails from it, send an 
> email to racket-users+unsubscr...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

-- 
You received this message because you are subscribed to the Google Groups 
"Racket Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to racket-users+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to