| This describes my plan for modules in PyArc. It's already implemented, and appears to work correctly, but PyArc itself is not finished, and cannot parse/eval all of arc.arc yet. Henceforth, I shall refer to Arc 3.1 in MzScheme as "MzArc" and my Arc interpreter written in Python as "PyArc". I had some issues with other module systems I saw. One was complexity. There's nothing wrong with a complex module system per se, but I would rather keep Arc lightweight and simple if I can. I would rather have a simple module system at the core, and then build a more complex system on top of that. For me, the primary concern was namespace collisions. I wanted to be able to import a library somebody else made without worrying about it breaking stuff in my program. In other words, I wanted to be able to call library functions they've created, without letting them see or tamper with my stuff, unless I explicitely allow them to. To accomplish that, I made a few changes. They shouldn't cause existing code to break, but obviously programs that use the extensions won't work in MzArc. First, eval and load can take a second optional parameter. This is the evaluation environment. If it is not specified, the code is evaluated in the current environment, which is what MzArc does right now. What is an environment? Anything that supports get/set on key/value pairs. That means (table) can be used as an environment, and so can an alist. Custom types can be used as well, provided that the interpreter knows what to do with them. All that is required is that (foo 'x) returns something, and (= (foo 'x) 'y) sets something. Lookups are performed in the environment you specify, and (assign) is scoped there as well. This means you can evaluate code in a safe, clean environment, isolated from everything else: (eval '(+ 10 5) (table)) -> error: `+` is undefined
The above throws an error because `+` is not defined in the environment. PyArc provides a new built-in function, called `new-namespace`, which returns a table that contains the built-in functions, including those defined in arc.arc. So if you want the previous example to work, but don't want it to have access to your globals, you can use this: (eval '(+ 10 5) (new-namespace)) -> 15
You could also pick-and-choose specific variables from the current namespace. For instance, the following allows the code to only use the + function: (eval '(+ 10 5) (obj + +)) -> 15
That is all that is required to create a simple module system. How do you use it, then? Let's suppose you had the files foo.arc and bar.arc and you wanted to import them into your program, but without icky namespace issues. You could use this: (= foo (load "foo.arc" (new-namespace)))
(= bar (load "bar.arc" (new-namespace)))
Now, all the global variables, functions, macros, and ssyntax are available in the tables foo and bar. However, foo.arc and bar.arc are not allowed to access anything in your namespace. Let's suppose that foo.arc defines a function called my-let, you could use it like so: (foo!my-let ...)
What if you don't want to type foo! all the time? You can pull it into your namespace: (= my-let foo!my-let)
(my-let ...)
Okay, but what about macros? They're scoped to the module, just like everything else. Assuming bar.arc contains a macro called my-do, you can pull it into your namespace just like with my-let: (= my-do bar!my-do)
Now, whenever my-do appears after that assignment, it will be expanded, like as if you had written it yourself. The same goes for ssyntax. If a file defines ssyntax, it will only affect that file. But other files can use it as well, if they choose to import it.All the above is pretty verbose, though, so I also defined a macro called import: (import foo "foo.arc"
bar "bar.arc")
The above is the same as calling load with (new-namespace) and assigning to a symbol, but it's much more concise. It also supports an alternate syntax, that lets you import only certain variables: (import (my-let) "foo.arc"
(my-do) "bar.arc")
When the first argument is a list, it specifies which variables to import. That means the above is equivalent to: (with (foo (load "foo.arc" (new-namespace))
bar (load "bar.arc" (new-namespace)))
(= my-let foo!my-let)
(= my-do bar!my-do))
If you know Python, then the following are roughly equivalent: (load "foo.arc") -> from foo import *
(import foo "foo.arc") -> import foo
(import bar "foo.arc") -> import foo as bar
(import (my-let) "foo.arc") -> from foo import my-let
In this module system, it is the person who imports the module who decides what the module's name is. This has at least a couple benefits: first off, it means that you can import Arc code that knows nothing about modules. In some module systems, you're required to use something like (module ...) in your program, but that means that you can't import files that don't use that convention.Second, it creates a problem when you want to import two or more modules, but they define the same name. Consider if foo.arc and bar.arc had both used (module "something"), that would cause a name collision! But because the caller is the one who defines the names, they can easily resolve the issue. I considered the two points above to be important, and so they guided me as I designed this module system. If you want, it should be possible to build a more complex system on top of this simple core, but I personally don't have a need for that (yet). Also, despite being simple, the system defined above allows for some interesting things. For instance, suppose you wanted to eval foo.arc and bar.arc in the same environment, but not in your namespace: (= env (new-namespace))
(load "foo.arc" env)
(load "bar.arc" env)
Now the table env contains the globals for both foo.arc and bar.arc, but neither have access to your globals (unless you let them). How do you let a module have access to your globals? Simple, you just assign them to the environment: (= env (new-namespace))
(= env!my-func my-func)
(= env!my-eval my-eval)
(= env!my-defn my-defn)
(load "foo.arc" env)
(load "bar.arc" env)
Now foo.arc and bar.arc have access to the built-in globals, my-func, my-eval, and my-defn, but nothing else. Thus, modules interact in a predictable way, controllable by you. Or, how about importing the same file twice, but in two separate namespaces? (import foo1 "foo.arc"
foo2 "foo.arc")
Even though they are the same file, the namespaces foo1 and foo2 are separate from each other. So, as you can see, the caller has control over how to mix and match modules, rather than the one who wrote the module.Random side note: see why I dislike using ! for property access? Using . would be so much nicer. |