Package extension and toplevel member refinement

Posted by    |      

Despite having spent several years designing frameworks and specifications based around the notion of dependency injection, I've never especially considered myself a big fan of the whole thing. Dependency injection strikes me as more of a fashion within one particular programming community than as some enduring pattern that will be reproduced in future languages by future framework designers.

Indeed, popular patterns sometimes point to some kind of inflexibility at the language level. So I've often asked myself precisely what missing language-level feature makes this particular pattern so appealing. The only answer I've ever really come up with is that dependency injection is a work around for the fact that instantiation, unlike method invocation, is not a polymorphic operation in most (all?) object-oriented languages. Instantiation operations hardcode the concrete class being instantiated, requiring the use of some kind of factory method in code that should be abstracted from the concrete type. Dependency injection frameworks exist to tame the profusion of factory methods.

In Ceylon, member class refinement makes instantiation a polymorphic operation at the language level, but it only works for member types. I think it's a very useful and interesting feature of the language, but it's hard to see it replacing dependency injection. Of course, in theory, you could define all your classes as members of some container class, but I just don't see that idiom catching on!

So an idea I have for a far-future version of the language is to allow refinement of toplevel declarations. This would all work out along the same lines as member class refinement. You could imagine a package which declares a default or formal toplevel class, for example:

//in package my.stream
shared formal class Buffer(Stream stream) {
    shared formal Byte read();
}

(We would also allow formal and default toplevel methods and attributes, of course.)

Notice again the difference between formal and abstract in Ceylon: a formal class may be instantiated. An abstract class may not be instantiated. A formal toplevel member makes the containing package an abstract package. An abstract toplevel class does not. By now you should be seeing why we truly do need two different annotations here.

Anyway, the point here is that other code in our package would be able to create and use a Buffer, without depending upon any concrete subclass.

//also in package my.stream
shared void open(Stream stream) {
    Buffer buffer = Buffer(stream);
    ...
    Byte b = buffer.read();
    ...
}

Now, before we actually make use of this package we must fill in the definition of the formal member. I could imagine that one way to do this might be to use the import statement:

//in package your.backend
import my.stream { 

    actual class Buffer(Stream stream) 
            extends super.Buffer(stream) {
        shared actual Byte read() {
            ...
        }
    }

    Stream, open

}

But I imagine a more common scenario would be reuse of a package via package extension. I'm not quite sure what the syntax for this would look like, probably something in the module descriptor declaring that one package extends a second package. Then an actual toplevel member would fill in the definition of Buffer:

//in package my.stream.fileio
actual class Buffer(Stream stream) 
        extends super.Buffer(stream) {
    shared actual Byte read() {
        ...
    }
}

I'm not completely certain, but I expect that one additional facility is needed: the ability to rewire inter-package references in the module descriptor. For example, in the package your.backend, you might import my.stream, but then the module descriptor would declare that references to the abstract package my.stream are actually fulfilled by the package my.stream.fileio.

Well, all this is really just speculation for now, and I haven't even really put any thought into how you could map all this to the JVM. But I think you can sort of see how this could be an alternative to dependency injection. What I especially like about it is how it makes the Ceylon language even more regular, while adding useful capabilities.

I have a gnawing feeling that this idea should be somehow related to how modules work in ML. But after a couple of attempts, I still don't feel like I really totally get ML's module system. It's something I definitely need to put more time into.


Back to top