I could not resist to read it entirely, just to see what kind of Algo I could have found there ... but "surprise", it was not about algos, more about (T)Pascal itself.
Since at the of the day Pascal is somehow the basis for all we can write today, I thought it would have been a nice experiment to try to reproduce the record concept via JavaScript.
What Is a Record
Nothing different from a predefined structure of types, similar to a classic literal object in JS except the structure defines type hints.
// simple Pascal function example
function personToString(self:person);
begin
writeln(self.name, ' is ', self.age, ' years old')
end;
// basic record example
type person = record
name:string[20];
age:integer
end;
// record usage
var me:person;
begin
me.name := "Andrea";
me.age := 32;
personToString(me)
end.
Please note I don't even know if above program will ever run so take it just as an example.
Record VS Prototype
As we can see the record is not that different from a "class" description via prototype, except rather than values we can specify the type of these values.This approach may be handy even in JavaScript since there is no type hint but sometimes we may desire to make some behavior consistent and hopefully safer.
A first JavaScript record example
Using some ES5 feature, we could be able to write something like this:
function record(description) {
// (C) WebReflection - Mit Style License
var
shadow = {},
self = {}
;
Object.keys(description).forEach(function (key) {
var
type = description[key],
check
;
if (!type) {
throw new TypeError("unable to check a type for property: " + key);
}
switch (typeof type) {
case "string":
check = function (value) {
return typeof value == type;
};
break;
case "function":
type = type.prototype;
default:
check = function (value) {
return type.isPrototypeOf(value);
};
break;
}
this(self, key, {
get: function () {
return shadow[key];
},
set: function (value) {
if (!check(value)) {
throw new TypeError("type violation for property: " + key);
}
shadow[key] = value;
},
configurable: false
});
}, Object.defineProperty);
return self;
}
Here a basic usage example for above function:
var person = record({
name: "string",
age: "number",
toString: Function
});
person.name = "Andrea";
person.age = 32;
person.toString = function () {
return this.name + " is " + this.age + " years old";
};
alert(person);
// Andrea is 32 years old
Following the same concept we may decide to add arbitrary types and relatives checks such "integer", "real", or whatever we need.
Here there is a version compatible with almost all browsers but IE:
function record(description) {
// (C) WebReflection - Mit Style License
function getter(key) {
return function () {
return shadow[key];
};
}
function setter(key) {
var
type = description[key],
check
;
if (!type) {
throw new TypeError("unable to check a type for property: " + key);
}
switch (typeof type) {
case "string":
check = function (value) {
return typeof value == type;
};
break;
case "function":
type = type.prototype;
default:
check = function (value) {
return type.isPrototypeOf(value);
};
break;
}
return function (value) {
if (!check(value)) {
throw new TypeError("type violation for property: " + key);
}
shadow[key] = value;
};
}
var
shadow = {},
self = {}
;
for (var key in description) {
if (description.hasOwnProperty(key)) {
self.__defineGetter__(key, getter(key));
self.__defineSetter__(key, setter(key));
}
}
return self;
}
Record And Prototype Problem
Unfortunately we cannot use this strategy to create constructors prototypes since the shared object (singleton) will use a single shadow for every instance.
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = record({
name: "string",
age: "number",
toString: Function
});
Person.prototype.toString = function () {
return this.name + " is " + this.age + " years old";
};
var me = new Person("Andrea", 32);
var other = new Person("unknown", 29);
alert(me); // unknown is 29 years old
To avoid this problem we should use return this["@" + key]; in the getter and this["@" + key] = value; in the setter, setting if possible the property as enumerable:false if we have an ES5 friendly environment.
This should make the trick but still there is something not that friendly for JS developers.
A Proper JSRecord
A more flexible approach would let us define at the same time both type and default value, so that prototype definition and type hint could be done in one single call.
How could we write something similar? Here my last revision (done while I was writing this post):
function JSRecord(description) {
// (C) WebReflection - Mit Style License
var
self = {},
cDONTwe = {configurable: true, writable: false, enumerable: false},
hasOwnProperty = self.hasOwnProperty,
defineProperty = Object.defineProperty
;
Object.keys(description).forEach(function (key) {
var
current = description[key],
type = hasOwnProperty.call(current, "type") ? current.type : current,
enumerable = hasOwnProperty.call(current, "enumerable") ? current.enumerable : true,
$key = "@" + key,
check
;
if (!type) {
throw new TypeError("unable to check a type for property: " + key);
}
switch (typeof type) {
case "string":
check = function (value) {
return typeof value == type;
};
break;
case "function":
type = type.prototype;
default:
check = function (value) {
return type.isPrototypeOf(value);
};
break;
}
defineProperty(self, key, {
get: function () {
return this[$key];
},
set: function (value) {
if (!check(value)) {
throw new TypeError("type violation for property: " + key);
}
cDONTwe.value = value;
defineProperty(this, $key, cDONTwe);
},
configurable: false,
enumerable: enumerable
});
if (hasOwnProperty.call(current, "value")) {
self[key] = current.value;
}
});
return self;
}
How things changed:
// we can have defaults
function Person(name, age) {
this.name = name;
if (age) this.age = age;
}
// we can define a type hint
// or we can define both hint
// and value plus enumerable
Person.prototype = JSRecord({
name: "string",
age: {
type: "number",
value: 0
},
toString: {
enumerable: false,
type: Function,
value: function () {
return this.name + " is " + this.age + " years old";
}
}
});
// we can test everything now
var me = new Person("Andrea", 32);
var other = new Person("unknown");
alert(me); // Andrea is 32 years old
alert(other); // unknown is 0 years old
alert(me instanceof Person); // true
for (var key in me) alert(key);
// name, age
What do you think? Here my partial list of pros and cons:
Pros
- easy type hint implementation
- safer objects, properties and methods
- usable as record or prototype without shared vars problems
Cons
- requires ES5 compatible environment
- getters and setters are slower than direct access due function overload
- type hint is slower due checks for each set
You're just inventing classes, I hope you're aware of it...
ReplyDeleteClasses exists because we like to think in categories, we, late descendants of Aristoteles' antic culture.
(Not that Kama Sutra, being written before Aristoteles was born, and far away, wouldn't use a taxonony)
Nowadays I'm thinking that the notion of 'class' is hardwired into the western culture, and we, javascripters, probably shouldn't try to avoid that much as we like (yes, it does make us hip and different, but what if we just dance around a lack of feature instead of having something...)
BTW, if old books, try to read Structured Programming from Dahl-Dijsktra-Hoare. Classes aren't the successors or the opposite of Structured programming: they were part of its concepts from the very beginning.
i think it is an interesting abstracion for the validation problem: it defines a whitelist and ensure the values are compliant. can be used every time you want to be shure that data passed inside your routine are clean, can be used as an alternative to assertions to go toward design by contract...
ReplyDeletethanks, one missing thing is the arguments and the returns property for methods, once I change the code to validate those too, if specified, the final thing to do will be a sort of production/development switch in order to ensure performances when necesary. Stay tuned
ReplyDeleteAadaam I am not inventing classes, I am implementing type hint usable through prototype following a good old Pascal notation as model ;)
ReplyDeletefunction Person(name, age) {
ReplyDeletethis.name = name;
if (age) this.age = age;
}
// we can define a type hint
// or we can define both hint
// and value plus enumerable
Person.prototype = JSRecord({
name: "string",
age: {
type: "number",
value: 0
},
toString: {
enumerable: false,
type: Function,
value: function () {
return this.name + " is " + this.age + " years old";
}
}
});
written in semi-java-like syntax:
public class Person {
public function Person(name, age){
this.name = name;
if (age) this.age = age;
}
public string name {get, set}; //C#-like
public number age = 0;
private function toString() { //enumerable = false => private
return this.name + " is " + this.age + " years old";
}
}
Let me quote wikipedia (much simpler than the official explanation in the UML standard: "a class is a construct that is used as a blueprint (or template) to create objects of that class".
The fact that you call it Record does not make it less class-like :)
(In order to have a class-like structure, you don't have to have class-based inheritance.)
many changes/improvements in my next experiment, have a look ;)
ReplyDeleteCalling it a record doesn't make it less class-like, but it *being* a record makes it less class-like.
ReplyDeleteA record = a structure = essentially a block of variables. A structure TYPE is similar to a class in some ways, but quite different in others.
The main difference is that if you make a TYPE RECORD =... in Pascal, then you can declare multiple actual records using that type, but each has only data. An actual class (using ObjectPascal) will have data, but methods too. (And the methods will not be duplicated). Of course you could use dynamically allocated records and use procedures instead of class methods to process them - and this isn't a lot different than what happens under the hood in C++, etc. (Look up Virtual Method Table).