Promise: IoC Type/Class mapping
Before I can get into mapping, I need to changed the way I defined getting an instance in Type and Class definition:
Getting an instance in Promise, revisited
When I talked about Object.new
, I eluded to it being a call on the Type, not the Class and the IoC layer taking over, but I was still trapped in the constructor metaphor so ubiquitous in Object Oriented programming. .new
is really not appropriate, since we don't know if what we are accessing is truly new. You never call a constructor, there is no access to such a beast, instead it can best be thought of an instance accessor or instance resolution. To avoid confusing things further with a loaded term like new, I've modified the syntax to this:
We just use the Type name followed by empty parentheses, or in the case that we want to pass a JSON initializer to the resolution process we can use:
As before, this is a call against the implicit Type Object
, not the Class Object
. And, also as before, creating your own constructor intercept is still a Class Method, but now one without a named slot. The syntax looks like this (using the previous post's example):
The important thing to remember is that the call is against the Type, but the override is against the Class. As such we have access to the constructor super
, really the only place in the language where this is possible. Being a constructor overload does mean, that a call to Song{ ... }
will not necessarily result in a call to the Song class constructor intercept, either because of type mapping or lifespan managment, but i'm getting ahead of myself.
How an instance is resolved
Confused yet? The Type/Class overlapping namespace approach does seem needlessly confusing when you start to dig into the details, but I feel it's a worthwhile compromise, since for the 99% use case it's an invisible distinction. Hopefully, once I work through everything, you shouldn't even worry about there being a difference between Type and Class -- things should just work, or my design is flawed.
In the spirit of poking into the guts of the design and explaining how this all should work, I'll stop hinting at the resolution process and instead dig into the actual usage of the context system.
The Default Case
This call uses the implicit mapping of the User
type to class and creates a new User
class instance. If there is no intercept for the User()
Class Method, the deserializer construction path is used and if there exists a field called _name
, it would be initialized with "bob".
Type to Class mapping
// this happens implicitly
$#[User].Use<User>; // first one is the Type, the second is the Class
// Injecting a MockUser instance when someone asks for a User type
$#[User].Use<MockUser>;
Promise uses the $
followed by a symbol convention for environment variables popularized by perl and adopted by php and Ruby. In perl, $
actually is the general scalar variable prefix and there just exist certain system populated globals. In Promise, like Ruby, $
is used for special variables only, such as the current environment, regex captures, etc. $#
is the IoC registry. Using the array accessor with the Type name accesses the registry value for that Type, which we call the method Use<>
on it.
The Use<>
call betrays that Promise support a Generics system, which is pretty much a requirement the moment you introduce a Type system. Otherwise you can't create classes that can operate on a variety of other typed instances without the caller having to cast instances coming out to what they expect. Fortunately Generics only come into play when you have chosen typed instances, otherwise you just treat them as dynamic duck-typed instances that you can call whatever you want on.
Type to lambda mapping
The above mapping is a straight resolution from a Type of a class. But sometimes, you don't want a one-to-one mapping, but rather want a way to dynamically execute some code to make runtime decisions about construction. For this, you can use the lambda signature of .Use
:
The above is a simple example of how a dynamic type can be built at runtime to take the place of a typed instance. Of course any methods promised by User not implemented on that instance will result in a MethodMissing
runtime exception on access.
The $_
environment variable is the implict capture of the lambda's signature as a JSON construct. This allows our mapping to access whatever initializer was passed in at resolution access.
The above example looks like it's the same as the $#[User].Use<MockUser>
example, but it has the subtle difference that MockUser
in this scenario is the Type, not the Class. If MockUser
were mapped as well, the resolved instance would be of another class.
Doing more than static mapping
But you don't have to create a new instance in the .Use
lambda, you could do something like this:
This will create a singleton for AddressBook
, but it's a bad pattern, being a process-wide global. The example only serves to illustrate that .Use
can take any lambda.
So far, mapping just looks like a look-up table from Type to Class, and worse, one that is statically defined across the executing process. Next time I will show how the IoC container isn't just a globally defined Class mapper. Using nested execution context, context specific mappings and lifespan mappings, you can easily created factories, singletons and shared services, including repositories, and have those definitions change depending on where in your code they are accessed.
More about Promise
This is a post in an ongoing series of posts about designing a language. It may stay theoretical, it may become a prototype in implementation or it might become a full language. You can get a list of all posts about Promise, via the Promise category link at the top.