hy
hy copied to clipboard
Consider adding Common Lisp's Sharpsign-dot (#.) reader macro
Pushing the eval of certain forms back to readtime has various uses. A big one is to swap out code based on various conditions, like the #if
and related preprocessor directives from C.
(#.(if (< *version* 2) old-func new-func) arg)
This would probably fit well with #740.
Another is to substitute for literals that don't have a literal notation. Compare:
=> (int "1a" 32) ; base 32 number
int('1a', 32)
42
=> #.(int "1a" 32) ; expands directly into 42 at readtime, like a literal
42
42
=> (+ "A line that is much too long to fit on one line "
... "doesn't actually have to fit on one line.")
('A line that is much too long to fit on one line ' + "doesn't actually have to fi
t on one line.")
"A line that is much too long to fit on one line doesn't actually have to fit on
one line."
=> #.(+ "A line that is much too long to fit on one line "
... "doesn't actually have to fit on one line.")
"A line that is much too long to fit on one line doesn't actually have to fit on
one line."
"A line that is much too long to fit on one line doesn't actually have to fit on
one line."
Because evaluation happens at read time, it's safe to use in loops and such, without a performance hit for recomputing it every time.
I thought it would be a simple implementation in Hy:
#@((hy.macros.reader ".") (fn [expr] (eval expr)))
And this does seem to work for the above examples, but I'm running into HyModel
issues again:
=> #.(lambda [] nil)
[long stack trace including hy.errors.HyCompileError: Internal Compiler Bug \U0001f631\u2937 TypeError: Don't know how to wrap a <class 'function'> object to a HyObject
]
But this works fine in Common Lisp:
* #.(lambda () nil)
#<FUNCTION (LAMBDA ()) {1002E222DB}>
Is there a simple change in the macro definition that would fix this? Can we perhaps make everything a HyObject by default if it doesn't already have a model?
Is eval-when-compile
, which we now have, equivalent to what you asked for here?
Maybe. It's not documented, so I'm not sure. And many of my use cases would need a shorter alias.
I was wrong; eval-when-compile
doesn't expand to anything, so it doesn't do this. It's only for changing the state of the compiler, presumably to affect macro expansions.
The Emacs docs on eval-when-compile
says it's kind of similar.
Elsewhere, the Common Lisp
‘#.’
reader macro (but not when interpreting) is closer to whateval-when-compile
does.
Unlike Hy, in Emacs
The result of evaluation by the compiler becomes a constant which appears in the compiled program.
But since Hy's version doesn't expand to anything, it can't be used that way.
But how can we make a constant appear in the compiled program? As my first attempt demonstrated, only certain data types have a Hy model. To get an arbitrary object into the compiled file, it would have to be serialized somehow. Sounds like a job for pickle
.
As a first attempt,
(deftag "." [expr]
(import pickle)
`((. (__import__ "pickle")
loads)
~(pickle.dumps (eval expr))))
This does have some runtime overhead. It's not a big deal at the toplevel, but in a nested loop, that would be pretty bad--and that was a major motivation for doing #.
in the first place.
I think we can overcome this problem with memoization.
(import pickle)
;; you can memoize with functools in Python3...
(defn loadstatic [bytestr &optional [_memo {}]]
(try
(get _memo bytestr)
(except [KeyError]
(assoc _memo bytestr (pickle.loads bytestr))
(loadstatic bytestr))))
(deftag "." [expr]
`(loadstatic ~(pickle.dumps (eval expr))))
This way it would only get unpickled once. loadstatic
could be a core function, or we could have the compiler do it with a special form or a new HyStatic
Hy model type or something.
There's two more problems with this though.
First, if you pickle two equal mutable objects, then the memoization will make them point to the same instance. We can avoid this by adding a gensym to the lookup key, e.g. (loadstatic ~(pickle.dumps (, (gensym) (eval expr)))))
. This gives a different instance per expansion.
And secondly, pickle
is also limited in the data types it can serialize. It can't even do lambdas. But there are more capable third-party serializers, like dill. But that's another dependency. Maybe we could try importing that, and fall back to pickle
if it's not available.
I think you want eval-and-compile
, not eval-when-compile
.
In the linked pull request, I've opted to add do-mac
to core in place of the requested reader macro, but for posterity, here's how you could implement it and use in ordinary Hy code under the current reader-macro system:
(defreader .
(hy.eval (.parse-one-form &reader) (globals)))
#.`(setv ~(hy.models.Symbol (* "x" 5)) 15)
(print xxxxx) ; => 15