Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider adding Common Lisp's Sharpsign-dot (#.) reader macro #919

Closed
gilch opened this issue Aug 23, 2015 · 6 comments · Fixed by #2385
Closed

Consider adding Common Lisp's Sharpsign-dot (#.) reader macro #919

gilch opened this issue Aug 23, 2015 · 6 comments · Fixed by #2385
Labels

Comments

@gilch
Copy link
Member

gilch commented Aug 23, 2015

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?

@Kodiologist
Copy link
Member

Is eval-when-compile, which we now have, equivalent to what you asked for here?

@gilch
Copy link
Member Author

gilch commented Dec 14, 2016

Maybe. It's not documented, so I'm not sure. And many of my use cases would need a shorter alias.

@Kodiologist
Copy link
Member

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.

@gilch
Copy link
Member Author

gilch commented Sep 20, 2017

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 what eval-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.

@Kodiologist
Copy link
Member

I think you want eval-and-compile, not eval-when-compile.

@Kodiologist
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants