CHAPTER 17
As the stated goal of Julian language, it is designed to facilitate the transition from a compiled programming environment to an interpreted one. The features we have learned so far are mainly meant to fulfill this purpose: the requirement for class definition before using a compound object, inheritance and interfaces, member visibility control, polymorphism, and the entire type system. For anyone coming from a Java/C# background, being able to immediately start writing in Julian with these familiar semantic features is a blessing - at least at the beginning.
When people get used to scripting, however, having to write their code in such a fashion gradually become counter-productive. A lot of these features exist in compiled language exactly because they can be enforced by the compiler. But in JSE every check has to be postponed to runtime. For example, if we only get to check the existence of a member at the runtime, what's point of even requiring the declaration of the said member? Similarly, if the type compatibility can only be enforced when an actual argument is passed along, then why to bother declaring the parameter's type anyway?
The second problem is the sheer verbosity for writing declaration-first code. Even for writing a tiny class with a single field, one has to declare a class, the field and its constructor, in which an argument is copied to the field. Most of these code is boilerplate that can't be more boring. And without a compile-time check their very necessity is worth questioning.
Fortunately, as you may have already noticed the use of word transition in the design manifesto, Julian does allow people to write their code in a fashion that is more aligned with classic scripting, therefore enabling them to voluntarily switch to a new flavor from a compiler-driven background. At the core of this scripting capability is Dynamic, a very special class that is exempted from class member check and, with a little support of language syntax, allows users to write code in a manner reminiscent of JavaScript.
At first sight, Dynamic is just another subclass of Object. It has two constructors. The default one is used in most occasions, which, as we will see later, can be invoked via a reduced syntax.
Dynamic by itself doesn't contain a lot of pre-defined members. It merely has those which it either inherits from its parent class (Object), or has to implement for the declared interfaces, such as IIterable. The way to use this class is to add whatever new members you would like at any time after the instantiation. To accentuate the dynamic nature of these members in contrast to those appearing in the class definition, in Julian we refer to them as property.
Dynamic dinner = new Dynamic();
// Add properties
dinner.appetizer = "shrimp";
dinner.mainCourse = "steak";
dinner.price = 70;
dinner.togo = true;
// Use properties
Console.println(dinner.appetizer + " and " + dinner.mainCourse + " ($" + dinner.price + ", " + (dinner.togo ? "takeaway" : "hall eating") + ")"); // shrimp and steak ($70, takeaway)
You havn't defined these members in a class anywhere, have you? In fact, at the time you are about to assign a value to a new property, the property doesn't exist yet, nonetheless the engine never complains.
If you try to retrieve a property that doesn't exist, you end up with null
. This means you cannot use null
to differentiate the case where a property doesn't exist, and the case where a property does exist with its value set to null
. The null-returning behavior is configurable. We will talk about the configurability in a few paragraphs.
Overwriting a property is allowed by default. Even more, you can overwrite with a value of some type that is farthest away from the current one. Again, the engine remains silent.
// (continuing from the previous example)
// Update the property with different type.
dinner.togo = "Home Address";
The arbitrary property access is a syntax sugar. In fact, Dynamic implements System.Util.IIndexable, and all property access operations are forwarded to one of the two metbods on IIndexable: the setter at(), and the getter at(). This also means there are three ways to access to a property:
// (continuing from the previous example)
dinner.price; // Special treatment for accessing to properties on a dynamic object
dinner["price"]; // Supported by the language for any type that implements IIndexable
dinner.at("price"); // Call the underlying method directly
Unsurprisingly, we can also add properties with Function type.
void steam(var gradient) {
print("steaming " + gradient.toString() + " ..."); // print() not defined in this example
}
Dynamic kitchen = new Dynamic();
// Add properties
kitchen.fry = (gradient) => { print("frying " + gradient.toString() + " ..."); }; // Assign a lambda
kitchen.steam = steam; // Assign a global function
// Call property functions
kitchen.steam("kale"); // steaming kale ...
kitchen.fry("fish"); // frying fish ...
Not very interesting. How about referring to the cook in the kitchen?
Dynamic kitchen = new Dynamic();
// Add properties
kitchen.cook = "Jeffery";
var fryFunc = (gradient) => { print(this.cook + " is frying " + gradient.toString() + " ..."); };
// Bind the lambda
fryFunc = Function.bind(fryFunc, kitchen);
kitchen.fry = fryFunc; // Assign a lambda
// Call property function
kitchen.fry("fish"); // Jeffery is frying fish ...
In the example above we have to write a few more lines. In particular, the function needs to bind with kitchen
so that the this
keyword appearing inside the lambda body can point to the Dynamic object.
Note that you can bind almost any kind of function to a Dynamic. Some of them, such as a global function, do not have a natural binding with this
, so if they get called without being bound first you will be hit with a runtime error. In the case of instance member method, it already has a bound this
. Rebinding such method with a value of other types would usually result in type check error, with the sole exception of Dynamic, which is always allowed to be bound to an instance method. It's just when being bound in this way, it's likely the member access happening inside the method's body may fail silently. Of course, we expect that the programmers know what they are doing.
class KitchenTech {
string operator;
void fry(var gradient) {
print(this.operator + " is frying " + gradient.toString() + " ...");
}
}
KitchenTech tech = new KitchenTech("Robot");
Dynamic kitchen = new Dynamic();
// Add properties
kitchen.cook = "Jeffery";
// Bind the instance member method
fryFunc = Function.bind(tech.fry, kitchen);
kitchen.fry = fryFunc; // Assign a lambda
// Call property function
// Since it's rebound to kitchen, expression "this.operator" will try to find it from the Dynamic object, which doesn't have such property. So it evaluates to null, which, when concatenated to a string, becomes "(null)"
kitchen.fry("fish"); // (null) is frying fish ...
Creating an object first, then adding the properties one by one looks infinitely lame. Dynamic therefore implements IMapInitializable, so that it can be instantiated with properties directly appended to the constructor invocation.
IMapInitializable defines a method, initByMap(), which takes an array of System.Util.Entry. A class that implements this interface is allowed to have a map-like initializer tailing the constructor call. Here is an example:
Dynamic dinner = new Dynamic() {
"appetizer" = "shrimp",
"mainCourse" = "steak",
"price" = 70,
"togo" = true
};
This now looks quite good, although it can be better. But let's first dive into the details a bit: for all other classes that implement IMapInitializable, what appears to the left side of =
in each entry must be one of the following:
And in all of these cases, the left side will be evaluated as an expression to produce a key. In the case of Dynamic and its subclasses, however, only these are allowed:
Now the curious part: if it's an identifier, it will be re-interpreted as a string value. The string coercion also occurs for char literal.
string appetizer = "cheese";
Dynamic dinner = new Dynamic() {
appetizer = "shrimp",
mainCourse = "steak",
price = 70,
togo = true
};
// appetizer is added as the property name
Console.println(dinner.appetizer); // shrimp
// not cheese - the initializer didn't try to evaluate an identifier, instead it only treated it as a string.
Console.println(dinner.cheese); // (null)
As a side note, the rule for re-interpreting identifier as string literal is universal to all classes implementing IMapInitializable, not just Dynamic. If you have to add a property whose name must be determined in the runtime, you can employ the trick of parentheses.
string appetizer = "cheese";
Dynamic dinner = new Dynamic() {
(appetizer) = "shrimp",
...
};
// the result of expression, appetizer
, is added as the property name
Console.println(dinner.cheese); // shrimp
We still have one major typing burden to alleviate - the incredibly clumsy maneuver around the binding operation. To the rescue, Dynamic supports automatic binding if you provide a lambda literal to the right side of the entry.
Dynamic kitchen = new Dynamic() {
cook = "Jeffery",
fry = gradient => { print(this.cook + " is frying " + gradient.toString() + " ..."); }
};
kitchen.fry("fish"); // Jeffery is frying fish ...
Note such binding only happens when you are providing a straightforward lambda. No other indirection is allowed; no further nesting is recognized. For example, if the right side is enclosed in parentheses, you don't get bound; if the right side is try to produce a lambda from another one, only the outer one will get bound, etc.
Dynamic kitchen = new Dynamic() {
cook = "Jeffery",
fry = (gradient => { print(this.cook + " is frying " + gradient.toString() + " ..."); }), // WON'T BIND!
grill = () => { // WILL BIND (but not that useful)
return () => print(this.cook + " is grilling ..."); // WON'T BIND
}
};
kitchen.fry("fish"); // Jeffery is frying fish ...
To allow automatic binding to arbitrary function object, other than doing it manually as shown above, we can resort to the alternative constructor. See Behavior customization.
Since Dynamic is so special, Julian allows you to instantiate it without specifying the type name, as long as you provide a map initializer. To further minimize typing (pun intended), let's also change the variable's type to Any.
var kitchen = new {
cook = "Jeffery",
appetizer = "shrimp",
deliver = gradient => this.cook + " prepares " + this.appetizer + "."
};
There you go, JavaScript programmers (don't forget new
though)!
Dynamic has a second constructor which allows us to tweak the behavior of the created object. This constructor itself takes a Dynamic as the argument, so the properties to pass along can be arbitrary. Nonetheless as of 0.1.34 only the following boolean properties are recognized:
A little inconvenient is that when calling this constructor you may no longer use the minimal syntax.
var kitchen = new Dynamic(new { // You can still use the minimal syntax to create the config object though
autobind: true,
sealed: true
}) {
appetizer = "shrimp",
deliver = kitchenTech.deliver // An instance method still gets bound.
};
kitchen.appetizer = "octopus"; // OK. The property exists.
kitchen.dessert = "browning"; // ERROR: no property can be added afterwards.
If a provided property has the recognized name but is not of the boolean type, it will be ignored. All properties are case-sensitive as well.
Another powerful feature of Dynamic is that it integrates seamlessly with the class system. This means, first of all, you can declare a subclass of it.
class Kitchen : Dynamic {
private int id;
Kitchen(int id){
this.id = id;
}
// Override a method from Dynamic
string toString() {
return "KITCHEN-" + this.id + ": " + this.appetizer + "/" + this.mainCourse + "/" + this.dessert;
}
// Add a new method
void report() {
...
}
}
var kitchen = new Kitchen(100) {
appetizer = "shrimp",
deliver = kitchenTech.deliver // An instance method still gets bound.
};
kitchen.appetizer = "octopus"; // Update a property
kitchen.dessert = "browning"; // Add a property
Console.println(kitchen); // KITCHEN-100: octopus/(null)/browning
Of particular interest from the example above is toString(), in which we tried to refer to some properties we do not know for sure if they would exist. Also note while we refer to them using explicit this
keyword as a means of addressing, it is not required, since in an instance-scoped method body the name lookup will automatically fall back to the instance members, and in this case, properties, if it's not found among the local variables. However, we STRONGLY recommend that you always use this
, not only for its clarity, but also because for some other functions such as a static method, binding it to this
doesn't alter its name lookup behavior, so it won't fall back to members on this
if it can't find it elsewhere. In this example, the bound static method misses a this
when trying to call getResource()
. This would not be a problem if it were an instance method, but in the case of static context, it won't resolve arbitrary name against a this
reference, thus failing at an unknown-symbol error.
class Helper {
static void install() {
string name = this.name;
var res = getResource(); // FAIL. Cannot find getResource.
Console.println(id + " is being installed with " + res.total + " ...");
}
}
var kitchen = new Dynamic({ autobind = true }) {
name = "Pastavia",
getResource = () => new Resource(), // Resource is not implemented in this example
install = Helper.install // A static method gets bound.
};
kitchen.install();
While we have shown how to declare a subclass, as you may have guessed it, we can do literally anything with a customized Dynamic class definition as we usually do with other regular classes: implementing interfaces, calling super methods, forwarding to another constructor, overloading methods by different signatures, etc.
Next to inheritance is extension.
interface IWorkshop {
string name();
int personnel();
}
static class WorkshopExtension {
static string describe(IWorkshop this) {
retrun this.name() + " (" + this.personnel() + ")";
}
}
class Kitchen : IWorkshop, Dynamic {
...
}
Kitchen kitchen = new Kitchen("happy dim-sum", 10) { ... }
kitchen.describe(); // happy dim-sum (10)
Regarding both class definition and extension, there is an important rule to remember that is pertinent to name resolution. If we try to access something on a Dynamic object, e.g. kitchen.describe()
, will it try it as a member first, or a property first, or somehow together?
The golden rule is simple to memorize: if there is any class member, be it declared as a class member or added as an extension member, it will be resolved as such, even if there is a property with same name. In other words, names from the defined types hide those added dynamically.
interface IWorkshop {
string name();
}
static class WorkshopExtension {
static string describe(IWorkshop this) {
return "NAME="" + this.name() + """;
}
}
class Kitchen : IWorkshop, Dynamic {
...
}
Kitchen kitchen = new Kitchen("happy dim-sum") {
name = "summer feast",
describe = () => "this is " + this["name"]
}
kitchen.name(); // happy dim-sum
kitchen.describe(); // NAME="happy dim-sum"
kitchen["name"]; // summer feast
kitchen["describe"](); // this summer feast
The implication of this rule is that you wouldn't want to use names which are already defined in your class or inherited from a parent class. So even if you are just using the plain Dynamic class, do not add these properties as their names are already taken: toString
, getType
, hashCode
, equals
, at
, size
, getIterator
and initByMap
. Of course, there is no one stopping you from adding properties with these names. But you won't be able to access to them without using the more explicit indexer syntax or calling at(key)
directly.
At the conclusion of this chapter, we would advise that, to live a longer and more prosper life as a Julian programmer, please refrain from overriding the methods inherited from Dynamic, with the exception of toString()
. Dynamic's ability to navigate freely in an otherwise strong-typed environment lies in its implementation of several key methods: at(var)
, at(var, var)
, and initByMap()
in particular. Overriding these members would therefore bear the potential consequences of compromising the intended functionality of Dynamic. For example, if you override the setter (at(var, var)
), in which you forgot to call super.at(key, value);
, you will end up with a Dynamic that is as useless as a plain Object, since whatever property you try to add to that class will be silently lost.