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.
#lang racket/base
(require (for-syntax racket/base
syntax/parse)
racket/date)
(provide at)
;; Time = TimeBase {+ TimeExt}*
;; TimeBase = now | today | Date-String
;; TimeExt = Real-Expr MaybeUnit
;; MaybeUnit = <nothing> | secs | mins
;; A TimeVal is a real number of seconds using the same epoch as
(current-seconds)
(begin-for-syntax
(define (at-keyword stx) (raise-syntax-error #f "illegal use of time keyword"
stx)))
(define-syntax now at-keyword)
(define-syntax today at-keyword)
(define-syntax tomorrow at-keyword)
(define-syntax secs at-keyword)
(define-syntax mins at-keyword)
(begin-for-syntax
(define-splicing-syntax-class Time
#:attributes (expr) ;; expression of TimeVal
#:literals (+)
(pattern (~seq base:TimeBase (~seq + ext:TimeExt) ...)
#:with expr #'(+ base.expr ext.expr ...)))
(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 tomorrow
#:with expr #'(tomorrow-fun))
(pattern s:str
#:with expr #'(parse-date-string s)))
(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)))
(define-splicing-syntax-class MaybeUnit
#:attributes (expr) ;; expression of Real
#:literals (secs mins)
(pattern (~seq secs)
#:with expr #'1)
(pattern (~seq mins)
#:with expr #'60)
(pattern (~seq)
#:with expr #'1))
)
;; today-fun : -> TimeVal
(define (today-fun)
(define today (current-date))
(find-seconds 0 0 0 (date-day today) (date-month today) (date-year today)))
;; tomorrow-fun : -> TimeVal
(define (tomorrow-fun)
(+ (today-fun) (* 60 60 24)))
;; parse-date-string : String -> TimeVal
(define (parse-date-string s)
(error 'unimplemented))
;; ----
;; (at Time) -> TimeVal
(define-syntax at
(syntax-parser
[(_ t:Time) #'t.expr]))
;; ----
;; Examples/Tests
(module+ test
(require rackunit syntax/macro-testing)
(define X 47)
(define MS 13500)
(at now)
(at now + 30)
(at today)
(at tomorrow + 20 mins)
(at now + X mins + (/ MS 1000) secs)
(at now + 30 mins)
(at now + 50 secs)
(check-exn #rx"illegal use of time keyword"
(lambda () (convert-syntax-error (at now + now))))
(check-exn #rx"at: contract violation"
;; include "at:" prefix; since "*" also says contract violation
(lambda () (convert-syntax-error (at today + 'apple mins))))
)
;; ----
;; Variation: Maybe TimeExt should allow deltas that aren't a fixed
;; number of seconds. Then make a TimeDelta type and change the main
;; `+` to `apply-time-deltas`.
;; Variation: Maybe TimeBase shouldn't have to come first. Then change
;; grammar and add a side condition that says at most one TimeBase. It
;; can be checked statically or dynamically, whichever you prefer.