24 mar 2009 kl. 19.51 skrev Marc Feeley:
I think an important part of making a module system work well is not to have a (load) function that "loads" a module, but insted have an import/use construct that declaratively states that this module needs this other module to work. It is then up to the module system when and how to load the modules. This makes it easier to analyze dependencies and, imho, yields clearer code.
Yes I completely agree. However, at the lowest level the system must perform some operation which makes the module usable to the program. In other words, an "import" or "use" construct must rely on lower level operations to do its work. I want to brainstorm on what should be provided at the lowest level. Then this lower level could be used to implement a higher level module system on top.
Conceptually, I agree, but a load-once procedure is, depending on what you want it to do, not very difficult to do, or relies heavily on high- level constructs in the system, for instance dependencies, so I don't see how it can be implemented as a reusable low-level component in any useful way.
It should be possible to build an abstraction that seems to work by filename, with some automatic trickery behind the scenes for other linking strategies.
I find filenames to be too brittle of an identifier for modules. It shouldn't matter where a module is located.
I think we are talking about two different issues here: Firstly, how a potential load-once function would keep track of loaded modules internally. For this purpose I don't think filenames are a brittle solution at all. Somehow it must boil down to files and where they are, and these files probably won't move too much in the timeframe of the program execution.
From my point of view this is completely separated from the issue of how any import/use construct would look like for the end user.
On the other hand, I'm rather convinced that filenames are the opposite of a brittle solution, even when it comes to the user interface, but that's another discussion.
- For interactive development there is a need to force the
loading of modules, even if they have been loaded already. How should this be expressed by the user?
In the system I have written this is done simply by re-importing the module from the REPL. The module system can see (with a hash and/or with timestamps) that the module is updated and reloads it. It has simply never been a problem.
You mean because the "last modification" time of the source is newer than what was recorded when the module was loaded? What happens if module A requires B, and B has changed but A hasn't. Will your module system reload B if you ask to load A again?
First of all, this issue rarely arises, because interactive development is fastest accomplished by using the plain REPL and not reloading whole modules.
The current version of my module system would reload only B in the above situation. I explain why I don't think this is wrong below. Another correct behavior that would make sense would be to do nothing - when developing interactively I will always reload the module I have actually edited, no?
You could of course reload both A and B in that situation, that is, reload A's whole dependency graph. I haven't found that very practical in practise however, because that wipes all globals. Yes, it is bad practise to rely on globals but sometimes for hacks it's convenient. Reloading the whole graph renders those techniques pretty useless. (I know Christian Jaeger has written a cool hack for this, but it is just that - a hack) Moreover, as I describe below, reloading A's whole dependency graph doesn't even solve the problem.
Moreover, often the initialisation of a module depends on the initialisation of the module it depends on. In my example, module B might create a unique key for a record type (i.e. generative records) which is then used by A's initialisation to instantiate some records of that type. So in this case it is necessary to reload A even if it hasn't changed. I believe the safest approach is to reload the whole dependence graph of the module to be reloaded.
The problem you mention isn't actually that B will have to be reloaded when A is requested, even if A itself is unchanged. As I see it, the problem in your example is that A must be reloaded, when B is requested. Otherwise there might for instance be a function in A that creates the old version of the B's record type (this can happen if A has a closure from B that generates records).
One solution to this is to reload dependencies upwards in the hierarchy when a module is reloaded, but that doesn't seem very practical, especially when working with utilities that many modules use. But most of all, this approach doesn't make sense in combination with the REPL. What would happen if I re-evaluate the record definition in the REPL? Should A automatically be reloaded? Should that happen for every expression that is evaluated in B's environment?
The approach I have taken here is to simply state that for any module M, no module that depends on it should have to be reloaded when M is reloaded. This is means that you must not implement records in the way you described. (Which for that particular example I think is good; records in memory don't necessarily become incompatible with the new version just because the module is reloaded)
This assumption implies that it's possible to reload any module at any time, without worrying about dependencies in any direction. It is also the only approach that I'm aware of that makes it possible to really leverage the power of the REPL in the context of modules, so I think it's a good trade-off.
Those are interesting and difficult issues. PLT Scheme's module system has addressed those questions rather thoroughly, so it's probably a good idea to check how they are doing it.
/Per