At my day job I write web-related software in C#. I like C#, despite some of its problems. In fact, it is my favorite language for general development on Windows machines.
One thing that I particularly like about my C# coding projects is Unity. Unity is a dependency injection framework which can provide your software a number of benefits: improved decoupling, dependency injection, dependency identification, ready-made factory and singleton patterning, recursive object resolution, avoidance of global variables, easy system configuration, etc. It is a really great tool and where I’ve used Unity I’ve found the quality of the resulting code to be much higher than without it.
For some time, I’ve wished we had something similar for Parrot, but it wasn’t until yesterday evening when I started putting down some exploratory code to see if it would even be possible to do. It turns out that it does make sense, if we change a few ideas around. This morning, I pushed an iniatial set of classes to github for the new Parrot-Container project.
First, let me talk about what Unity does and what dependency injection is. Then I’ll show some example code from Parrot-Container and talk about how it can be used and what my development plans are for it.
What we can do with a Unity Container object is register types and objects under a certain type key. Later when I ask Unity for an object of a specific type, it can provide it to me from the catalog of registrations. If I register a type, it will create me a fresh new object of that type. If I register an instance, it will return me the pre-existing instance. What’s great is that I can register a concrete type with a key that is an interface or an abstract type, and rely on unity to deliver me an object of the configured subclass. Here’s some example code:
Actually, through the magic of type generics in C#, we can write the code much more cleanly:
This is a nice feature, but not entirely powerful. Where Unity really starts to become valuable is in handling constructor parameters.
Notice how the class Bar
requires an IFoo
in its constructor? Notice how
we didn’t specify one to pass when resolving the new bar
variable? Unity
reads the constructor signature and automatically resolves instances of all
variable types asked for. Anywhere in my program, I can ask for a fresh new
IBar
object without having to know anything about what the dependencies are
of the particular concrete class which is being constructed. I don’t need to
know what the dependencies of Bar
are when I just want any old IBar
object. I ask for an IBar
, and Unity automatically injects the necessary
dependecies for me.
In Parrot, I would really like to be able to do something similar. I would
like to be able to ask for objects and be able to ignore, in most cases, the
messy details about how to instantiate and initialize them. This is especially
important since there really is no standard way of initializing a new Parrot
object with invariant data. If I have a regular Parrot type, like a built-in
PMC type or a user-defined PIR Class PMC, I can pass in a single PMC
initializer when I create it. For built-in types this logic is handled by
VTABLE_init_pmc
. For user-defined classes it would happen through
VTABLE_instantiate
. If we assume for the sake of argument that most
initializer PMCs like this would be arrays or hashes of named quantities, we
do have a relatively flexible way of creating new objects.
However, if I’m using a P6protoobject things are a little bit messier. In
the P6protoobject implementation in the Parrot repo, there is a .new()
method which can be used to create a new object of the specified type, but
that .new()
method doesn’t take any parameters, and doesn’t call either of
the constructor-ish methods described in the Perl6 spec: CREATE and BUILD.
Even if we override the instantiate
vtable in our custom class, there’s no
way through the normal P6protoobject interface to pass an initializer value to
it. There are ways around this, of course. We could update the .new()
method
in P6protoobject to allow initializers, or to call a BUILD method if it
exists. We could override that method with custom behavior in each of our own
projects, although that’s a big messy pain in the ass. We could simply
instantiate every NQP object using a two-stage system:
my $foo := My::Foo.new();
$foo.initialize(args);
Kakapo got around this issue by overriding P6protoobject.new()
with a
custom implementation which called a custom _init_obj()
method. That’s not
in the Perl6 spec, although it would have been trivial to rename the method
which was called to “BUILD” for compliance. But even with that overridden
constructor logic, it would only work on P6protoobjects, not on ordinary
Parrot Class PMCs, PMCProxy PMCs, or even some other kind of exotic metaobject
that the user was using. Either we would need to take substantial effort to
wrap all the non-P6ish types with P6metaclass wrappers, or we would need to
differentiate between types in the application code. A large part of Kakapo
involved wrapping built-in types and adding all sorts of add-on logic,
improved constructor logic, extension methods, etc. An equally large part of
Kakapo also involved handling dependencies between classes, so that we were
setting up all this scaffolding in specific orders so we didn’t run into
conflicts.
Parrot-Container aims to change all of that. We register objects by “type”, and can resolve objects using a specified sequence of initialization steps. This way the user of an object doesn’t need to be aware of the initialization steps and dependencies of the objects it creates. Parrot-Container’s Container class does all that for you. I put the word “type” in quotes above because I really don’t key on types, but instead key on stringified slugs. The key can be a P6protoobject, a P6metaclass, a Parrot Class PMC, a String, a string array PMC, a NameSpace PMC, a Key PMC, or anything else that can be stringified. Because we are stringifying, it basically is a slug-based lookup system, and we would be able to register types and instances by name instead of by type.
When I register a type, I can register it with a list of initialization
actions. Those actions can involve passing in an initialization PMC to
VTABLE_init_pmc
or VTABLE_instantiate
, or calling a method with a
specified list of parameters, or calling a sub which takes the new object and
a list of parameters, or calling a completely custom factory method to
generate the new object. Without further adieu, here is an example of code
that works right now in Parrot-Container:
INIT { pir::load_bytecode("parrot_container.pbc"); }
my $c := ParrotContainer::build(ParrotContainer::Container)
$c.register_type("String", :meth_inits([
ParrotContainer::build(ParrotContainer::Initializer::Sub,
sub ($obj) {
pir::set__vPS($obj, "FooBarBaz");
}, []
),
ParrotContainer::build(ParrotContainer::Initializer::Method,
"replace", [
ParrotContainer::build(ParrotContainer::InitializerArg::Instance, "B", :position(0)),
ParrotContainer::build(ParrotContainer::InitializerArg::Instance, "C", :position(1))
]
)
]));
my $bar := $c.resolve("String");
pir::say($bar); # "FooCarCaz"
The ParrotContainer::build()
function is a convenience wrapper that
instantiates a new object from the type (P6protoobject or anything else that
represents a type) and automatically invokes a BUILD method with the provided
arguments, if any BUILD method exists in that class. By using
ParrotContainer::build()
faithfully, we can use “BUILD” constructors with
any type we create, whether it be P6object-based or not.
Since we’re using dynamically-typed languages on Parrot, we can’t really read
constructor parameter lists and extract a list of required types from that.
For this reason we need to do a little bit more work up-front when we register
types with the container to specify injection parameters. If we want the
Container to automatically resolve parameters recursively, we can use a
ParrotContainer::InitializerArg::Resolve
type instead. This will ask the
container to resolve the parameter by tag name dynamically.
In the sequence above, we register the lowly String type with two
initialization routines. The first is a nested sub which sets the value to
"FooBarBaz"
. The second routine calls the .replace()
method with two
positional arguments to replace all instances of “B” with “C”. When we resolve
the “String” type, we get a fresh String PMC with the value “FooCarCaz”, which
we expect. Here’s another example:
$c.register_type("Complex", :init_pmc("1+2i"));
my $complex := $c.resolve("Complex");
pir::say($complex); # "1+2i"
We can manually specify an initialization PMC to use, which is passed to the
VTABLE_init_pmc
. In the case of Complex, it takes a String argument and
parses out a complex value from it.
There is one other feature that I’ve built into ParrotContainer, and which I’ve also been putting together in a separate lighter-weight class as well: Prototype-based object building. In a prototype system, we register a prototype object with the container, and when we resolve that prototype’s type, we get a clone of it. This is a necessary feature for JavaScript, and Parrot-Container provides it out of the box:
my $proto := new__PPP("Complex", "1+2i");
$c.register_prototype("Complex", $proto);
my $complex := $c.resolve("Complex");
pir::say($complex); # "1+2i"
$complex := "4+5i";
pir::say($complex); # "4+5i"
pir::say($proto); # "1+2i"
With prototype we can also specify a number of initializer actions, which
could be used to resolve child properties if deep cloning or other custom
per-instance modification is required. Notice that, like in JavaScript, if we
change the prototype after it’s been registered, future clones of it will
have the new properties (although existing clones will not). I’m also writing
a much smaller PrototypeManager
class which will handle prototype-based
activities like this without all the features and overhead of the Container
class.
That’s a basic overview of the new Parrot-Container project and what it is currently capable of. I have a lot of development I want to be doing on it in the future to add new features and abilities. I already have a number of planned uses for it, and I would be interested to hear what other people think about it as well.