[Grace-core] More thoughts on modules

Kim Bruce kim at cs.pomona.edu
Wed Jun 8 17:32:50 PDT 2011


I sent the following message on grace-core this morning, but it appears to be hung up waiting for approval because I attached my lecture slides and they took up too much space.  Here is the message again without the attachment.

Kim




On Jun 8, 2011, at 12:53 AM, Andrew P. Black wrote:

> 
> On 7 Jun 2011, at 17:14 , Kim Bruce wrote:
> 
>> More on modules, responding to Andrew's suggestion about using parameters to import modules:
>> 
>> Allowing for changes in names when importing can be tricky.  The idea in my module system is that one would import the interface (or definition) modules, which make no claim on implementation.  The advantage of this is that the interface modules provide all the information one needs to type-check the new code in the presence of the imported code.  If one were to write modules as taking other modules (or even pieces of other modules) as parameters, you would have to provide all of the kinds of information that is in the definition modules already,
> 
> I don't think so.  In fact, I think that the specification of a module's requirements is basically the same either way.  If there is already an existing interface module that specifies my requirements, I certainly imagine using it to specify my requirements without re-typing a lot of stuff.
> 
>> making it pretty clunky.  Personally I’d rather import from interface modules and then allow renaming when writing an implementation module for a given interface module.
> 
> I think that one key question is whether you anticipate allowing two implementations of the same module interface to co-exist in an application.  For example, I import a mail agent that uses an implementation of priority queue, and a scheduler that uses an implementation of priority queue.  Can I parameterize these two different module with different priority queue factories?

Yes, as long as the types are compatible (structurally or otherwise).  They could easily be imported from one or more modules and used interchangeably if the resulting objects have the same type.
> 
>> Let’s take a very simple example (using interface as the keyword):
>> 
>> Interface Module DMod {
>> 
>>  type T {...}  // alternatively can also hide some methods, giving a partial revelation:
>>                       // (I used different names, T and U, to make it clearer this time)
>> 
>>   type U extends {
>>         m:....
>>         ...   // list only features you want publicly available outside of module
>>   }
>> 
>>  const o: U    // list type, but not implementation
>> 
>>  const makeT: S -> T  // closure, not a method, as it’s not sitting in an object.
>> }
>> 
>> (As Andrew points out, rather than exporting makeT as a closure, we could instead wrap it in an object and export it.  I have a marginal preference for exporting closures, but either works.  I originally had a variable also being exported, but instead we could export an object like o with getter and setter methods for the variable.)
>> 
>> Implementation Module implements DMod {
>> 
>>   import EMod  // options would exist to import either all or some of it,
>>                           // could also specify whether need to use qualified names or not
>> 
>> 
>> 
>>  type T = EMod.OT
>> 
>>  type U = EMod.OU
>> 
>>  const o = EMod.oo
>> 
>>>> }
>> 
>> This implementation module simply does renaming to make the imported module match up with the names in the definition module.  Now we can link this in as the implementation module corresponding to DMod.
> 
> As written, your imports statement looks like parameterization.  So another key question is whether you intend that the parameterization by EMod is early bound or late bound.  It looks to me from this example that it is early bound, that is, if I wanted to use a different implementation of whatever interface EMod implement, I would have to copy and edit the source code, rather than just "calling" your module with a different parameter.  Is that right?

The intention is that the definition modules would be early bound (compile time), but the implementation modules would be late bound (e.g., at link time).  I see modules as static, so don't see the need to bind at run time, but am happy to listen to arguments in favor of that.
> 
>> 
>> This seems to work pretty simply if we have structural types, as T is now just another name for EMod.OT, so all type specifications in EMod could have their OT’s replaced by T’s without difficulty.  If we had nominal types then we’d have a way to define types so that we ended up with the new name being an alias for the old rather than giving a “new” type (and hence different type), which would not be freely substitutable for the old.
>> 
>> Now when we import DMod, we would end up getting those items from the EMod implementation, with their names changed.
> 
> What you write is rather like one of Gilad's initialization sections, where he extracts stuff from platform and bind it to local names.

Yup.  I like it this way, as it makes it easier to late-bind these without changing any existing code.  However, it could be done the other way.
>> 
>> If someone would like to work instead with parameters to modules, I’d be interested in seeing how one would write an example like this so that there is enough information that we could do static type-checking of the importing module (before we fill in the formal parameters with the actual parameters).
> 
> I would write something like this.
> 
> Implementation Module Andrew implements DMod { EMod: EInterface ->
> 
>  type T = EMod.OT
> 
>  type U = EMod.OU
> 
>  const o = EMod.oo
> 
>> }
> 
> This is under the assumption that EInterface is defined as an interface module; it wasn't in your code, but I imagine that you would need it too.  For the above to type-check EMod would have to contain type OT and OU and an object oo.

This appears to me to be pretty much a notational variant of what I wrote.  How (and when) would you instantiate it?  Unless something pops up that is surprising, I have no strong preferences as to which notation is better.
> 
> 
>> Please note that all of the above on modules is written from the perspective of statically typed languages.  A quick read of Andrew’s paper seems to indicate that similar effects could be obtained by having objects provide (at least) two different encapsulation properties.  One inside the implementation module that would provide calling privileges to all methods of the object.  A different one would be provided for each interface, specifying that only the methods listed in the interface module would be callable from objects that don’t live (or are generated) in the implementation module.  Andrew, does this make sense?
> 
> I think that's more or less right.  But note that encapsulation from Modula-3 modules is very much more limited than what we describe in our paper, which is object encapsulation.  It lets you do things like have full access to a collection from one objector (its "owner", say), but read-only access from other objects.   Neither of these clients is the implementor of the collection.

I think this is where the multiple interfaces for an implementation module in my presentation kick in.  Depending on what privileges you want for that object, you would import and/or use one or the other interface module for the same implementation.  I can't tell if your scenario provides more flexibility than the one I outlined.
> 
> We have the notion of encapsulation policies; a policy says, for each method, whether you can (a) call it or (b) override it.  We divide message sends into three groups: self-sends, super-sends, and general (object) sends.  There are two things to figure-out: which messages are valid, and how to do message-lookup.
> 
> As far as deciding whether a message is valid, with object-sends, the receiver is treated as a black-box.  (It's encapsulated.  Fancy that!)  So it's the encapsulation policy in the reference to the target that determines validity: if the method is callable according to the policy, then it's callable.  However, for self-sends and super-sends, its the encapsulation policy associated with the inheritance hierarchy that matters.  There is also an interaction between encapsulation an method lookup, as there is with Java private.
> 
> A while back, when you proposed modules, I asked you what problem you were trying to solve with them.  I tink that modules can solve namespace problems, and provide a mechanism for deployment.  They don't really address encapsulation problems in a very useful way; for that one needs something like encapsulation policies.

I'm not sure they don't provide something close to what you outline, though probably in a slightly less expressive/flexible way.
> 
> I'm not convinced that for a teaching language, encapsulation is that much of an issue.   But namespaces and deployment certainly are.  This is why I hadn't been pushing encapsulation policies (although I cold see them appearing high up in the lattice of language levels), but was interested to hear about modules.  I do think that we should try and keep this separation of purposes.

I agree that namespaces and deployment are the highest priorities for a novice language.  An advantage of including partial revelations is that it can be used to limit access to particular methods when objects (or their types) are imported.  This could (if we wanted to be more radical than we have so far) lead us to omit protection annotations in class definitions, using interface modules to hide access rather than "private" or "protected".  (I won't push this, but I find it intriguing.)


	Kim


More information about the Grace-core mailing list