-
Notifications
You must be signed in to change notification settings - Fork 371
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
Get rid of autoimports via new namespaces #1407
Comments
600 builtins is too many. Perl has around 200 (depending on how you count), which is also too many (many of them are no longer very useful, and I suspect they predated the implementation of modules in Perl). Arguably, Hy already has too many. There's no shame in moving things to |
Steps 1 and 2 sound great, though I personally don't think using I got totally lost at step 3 though... |
The most straightforward solution to auto-importing woes is to replace the current auto-importing magic with Things like dynamically scoped variables and protection against shadowing of core function names in macro expansions would be nice. But it's hard for me to guess in advance whether your plan would work and what the side-effects would be. |
@kirbyfan64, I was assuming knowledge of Clojure. It might help if you play with Clojure's syntax quote. user=> `x
user/x Notice the prompt. That indicates that the current namespace is If you need to insert a symbol without the prefix (like user=> `~'x
x The current namespace lives in user=> clojure.core/*ns*
#<Namespace user> We can change it with the user=> (in-ns 'foo)
#<Namespace foo>
foo=> `x
foo/x And now symbols get a prefix for the current namespace. foo=> (def x "Foo!")
#'foo/x
foo=> x
"Foo!"
foo=> (in-ns 'user)
#<Namespace user>
user=> x
CompilerException java.lang.RuntimeException: Unable to resolve symbol: x in this context, compiling:(NO_SOURCE_PATH:0:0)
user=> foo/x
"Foo!" Here we defined a user=> (refer 'foo)
nil
user=> x
"Foo!"
user=> (eval `x)
"Foo!"
user=> `x
foo/x See also, the discussion in #911. Does that help? |
I did think of that possibility, but it doesn't work well. Importing everything at the top doesn't help with #1367. And it creates a new problem: module docstrings have to be the first statement, but now that doesn't work in Hy. (I guess you could explicitly I'd also like to make Hy's core more discoverable. With Furthermore, all It's more trouble than what we're currently doing.
I am also worried about some of this. Some of these steps could be done in a different order, or in slightly different ways. If we did the lookup magic from step 5 first, we can hook anything we please into the I'd also like Hy to support mypy for static typing, if possible. But I'm worried that the abbreviated special variables from steps 5 and 6 would confuse it. Those accessed through Another option would be to use symbol macros, but restrict them to names containing a Another option would be to autoimport |
Yes, Hy will still need to correctly position future statements, module docstring, and core imports. |
Some more concerns. We can shadow special forms. => (setv + 42)
File "<input>", line 1, column 7
(setv + 42)
^^
HyTypeError: b"Can't assign to a builtin: `+'"
=> ((fn [+] (+ + +)) 21)
from hy.core.shadow import +
(lambda +: (+ + +))(21)
42 You wouldn't expect this to work, but it does. Maybe it shouldn't, but then, how are the shadow functions supposed to work? Maybe shadowing special forms should be allowed in general instead of partially disallowed like now. But then, how should a Hy syntax-quote expand Let's look at Clojure--you actually can assign to special form symbols. user=> (def do 42)
#'user/do
user=> `do
do
user=> (do do)
42
user=> `(do do)
(do do) and, as you can see, it doesn't expand them in syntax quotes. But, like a macro, it takes priority over a function with the same name, like how shadows work in Hy now. This is probably the right way to handle it. You could still explicitly use the prefixed form, when that's your intent. But it should be explicitly documented, because Hy has a lot of special forms compared to most Lisps. |
Presumably, it's not supposed to, and whoever wrote the "Can't assign to a builtin" feature forgot about function parameters etc. as ways to change those names. We could conceivably ban assignments to |
Considering how Clojure works, no ban anywhere makes more sense. And it would be easier to implement too. Special form names would take priority in the function position, even when they're shadowed, like Clojure. |
I worry that other implementations don't have it. I'd like to support IronPython3 and Jython3 when (if) they get released. They both appear to have active repositories. I'm also thinking about supporting Stackless, but PyPy3 might make that obsolete. It doesn't run on Windows yet though. I suppose we should check out how those implementations do it. Maybe we could special case them somehow if they don't use Another concern is the issue of creating hidden dependencies. I'm not sure how big a deal this is, but normally, you want all of your imports at the top of the file, so you know what it needs to run properly just from looking at the head, instead of searching through possibly thousands of lines. Except for One "advantage" of using something like |
Autoimport per se is gone as of #2141, except for the |
Autoimports are causing us headaches. #1367, #791.
Macros are also brittle due to lack of namespacing and hygene. #277. Even some compiler builtins have this problem--
It seems like that should work. I didn't even use any macros. Can you spot the error in the Python expansion?
One might be tempted to scoff and say, "You should never use a builtin name as a local!". That might work for Python, which has a fairly small number of builtins, but for Clojure, which has ~600 symbols in core, that's an unreasonable cognitive burden on the programmer. Hy isn't quite there yet, but it's still got a much bigger core than Python's builtins. The
name
builtin is already a problem. #525 Maybe restricting the names of special forms is okay, but we really need to be able to shadow the other core names with a local.Clojure's namespacing may hold the answer. If the compiler had used
__import__("builtins").list
instead of justlist
in its quasiquote expansion, the above would have worked properly. Clojure automatically namespaces symbols in its syntax-quote forms, so they work more like that.An inline import is not a big deal in Python, since modules are cached. It's basically just an extra dict lookup--a fast action that you do all the time in Python code.
Using
__import__
directly is frowned upon in Python--__import__
is kind of an implementation detail (but we could say the same of the AST itself). The recommendation is to use the import statement, like we're doing now. But that's harder to use than an expression, and requires an extra gensym, and doesn't play nice with__future__
. So if you need a more portable import expression, you're supposed to use the one from importlib. But how do we get to that without an autoimport? Is__import__("importlib").import_module("foo").bar
any better than__import__("foo").bar
? Chicken/egg.The solution to a chicken/egg problem is bootstrapping.
step 1 new builtin
As a start, I propose adding a
_#
object to Python's builtins module upon import of Hy. The_
marks it "private" and the#
makes it unlikely to interfere with any other Python use of the module, even by other libraries, since#
is not allowed in Python identifiers. This object's class will overridegetattribute
to make a dot access a module. Now you can refer to (e.g.)list
as_#.builtins.list
in macros, and it will work properly from any module, even in a context wherelist
is shadowed.step 2 no more autoimports
We can get rid of all autoimports in the compiler once this is available and we rewrite the compiler and macros to use these namespaced symbols instead of builtins or anything from core we're autoimporting now. This is a a bit less tedious than rewriting them to (e.g.)
__import__("builtins").list
or(. ~g!builtins list)
after adding(import [builtins :as g!builtins])
to every macro. Unlike some complicated import expression,_#.foo.bar
is just aHySymbol
, and can be treated like one by macros.step 3 upgrade quasiquote to syntax-quote
But it could be automated further like Clojure does in its syntax quote. This way, the compiler would insert the namespace prefix for you. You don't have to write it, but it comes out in the expansion anyway.
There could be some namespace macro that alters the compiler state, maybe by setting a
_#.hy.core.ns
object, which would map abbreviations to their expansions, e.g.{"list": HySymbol("_#.builtins.list"), ...}
. We could make thedef
form add the mapping to the current namespace. #911. This way you don't have to build the mapping either. You just set the namespace, anddef
builds it for you as you go.setv
wouldn't alter the namespace, so adding the abbreviation is optional. There would also be a way to include all the mappings from one namespace in another. You'd often includebuiltins
andhy.core
in a custom namespace, for example.step 4 special variables
We could also use the namespace system to help implement special variables. hylang/hyrule#51. We'd update the
def
form to make special variables instead of the normal kind. A dot lookup would go through a special__getattr__
method that dereferences a dynamic variable, instead of returning the Var object itself. This means that you'd have to spell out the prefix when using them outside of a syntax quote.We can start using special variables to configure things like other Lisps do, like changing how things print at the repl.
step 5 module-level namespacing
Making the compiler add the prefix to an abbreviation outside of a syntax quote seems like a bad idea. There'd be no way to shadow it with a local. That's not how special variables work in Common Lisp or Clojure.
Since we want to be able to shadow abbreviations, they need to act like globals, not symbol macros. For most things, it's enough to simply add it to the module dict. (e.g.
(import [foo [*]])
But that's not good enough for a dynamic variable, which needs the dereference magic that lives in the dot access. At the repl, you could replace the__builtins__
module with an object that does the dereference magic, since if value not found in theglobals()
dict, Python will look for it in__builtins__
. But this doesn't work in modules, which are executed all at once on import, instead of incrementally. By the time you try to set__builtins__
it's already fixed.The way around this is to
exec
the code or bytecode on module load using a customglobals
object that has the magic in its__getitem__
. This way, an ordinary global lookup can dereference the special variable, even without a dot, even outside a syntax quote. And it can still be shadowed by a local.step 6 finally, a sensible let alternative
Make an
_#.auto
object that overrides__setattr__
to create a Var object when you attempt to bind one with that prefix that doesn't exist yet.The
binding
form implemented in step 4 requires a prefixed name. In step 5 it would also accept an abbreviation from the currently active namespace. Now, if there's no prefix, and no abbreviation,_#.auto
is assumed instead. Now you can usebindings
like a dynamic-scopedlet
. Thesebindings
forms seem to do most of we want fromlet
short of lexical closures. The binding get released at the end of the last form. You can nest them. You can use short names, likex
andy
. You can pass them to functions, if the call is made before the binding ends, and they'll get a lexical argument.conclusion
That's a big plan with a lot of steps. Some of the details are tentative. I'm not going to put all of this in one PR. That's too much. But you might not like the changes until they're finished, so you need to know where I'm going with this.
I'd also like to get general approval of this plan before I start putting in the actual work. Does this seem like a good direction? Any parts that aren't clear? Is there anything you'd want changed?
The text was updated successfully, but these errors were encountered: