Dynamic Languages: Constructs vergelijken
Deze pagina vergelijkt verschillende dynamische talen in een poging om een overzicht te maken tussen de alsmaar groeiende lijst. De meest gebruikte features van zulke talen worden hieronder opgelijst.
Het verschil tussen MOPs en Prototypal inheritance
❗ Javascript heeft géén Meta Object Protocol (MOP) dat de taal dynamisch maakt, maar bouwt verder op prototypes. Dat wil zeggen dat het klassieke inheritance systeem niet bestaat in Javascript, maar wel nagebootst kan worden door objecten te laten afleiden van objecten. De vergelijkingstabellen hieronder tonen hoe Javascript zich kan gedragen als een klassieke OO taal, dat wil niet automatisch zeggen dat dit de beste manier is om JS te gebruiken!
De volgende dynamische talen beschikken wel over een MOP:
- Groovy
- Python
- Ruby
- Smalltalk dialecten
- LISP dialecten (bvb. Runtime MOPs via
CLOS
) - Perl 6
- OpenC++ (bvb. Compiletime MOPs door C++ te parsen en analyseren)
Ruby VS Javascript metaprogramming:
The most interesting thing about the Javascript example is that it is exactly the same as the example of adding a dynamic method to a class. There is no difference because Javascript functions are closures. The ubiquity of closures in Javascript is extremely powerful and, makes metaprogramming very easy.
Vergelijkingstabellen
Dynamica van de taal: Metaprogramming
Methods toevoegen
Op klasse niveau
JavaScript
String.prototype.isTrue = function() { return this == "true"; }
Groovy
String.metaClass.isTrue = { delegate == "true"}
Javascript bevat enkele common pitfalls hierrond, zie JS Inheritance.
Groovy staat ook toe om constructors toe te voegen op deze manier.
Methods “lenen” gaat ook met de &. operator in Groovy om de pointer naar een method vast te krijgen. In JS gewoon de “key” (methodnaam) als referentie gebruiken.
Op instance niveau
JavaScript
"someString".isTrue = function() {
return this == "true";
}
Groovy
"someString".metaClass.isTrue = {
delegate == "true"
}
Voor beide talen op exact dezelfde manier dus, op een instance zelf.((Groovy 1.6+ heeft dit standaard, anders uw metaClass nog correct aanpassen! Zie http://groovy.codehaus.org/Per-Instance+MetaClass ))
Als Groovy problemen geeft, kan het zijn doordat ExpandoMetaClass
niet geconfigureerd staat voor inheritance:
ExpandoMetaClass.enableGlobally()
Methods catchen
Met “catchen” wordt “decoreren” bedoeld. van alle calls op van method namen die niet bestaan en doe ermee wat je wilt.
JavaScript
niet mogelijk
Groovy
SomeClass.metaClass.methodMissing = { name, arguments ->
switch(name) {
case "bla": return "blaing eh"
break;
}
}
assert "blaing eh" == new SomeClass().bla()
Groovy kan ook alle methods opvangen, ook de bestaande, door de MOP hook invokeMethod
, get/setProperty
, propertyMissing
etc te overriden - op Object
of metaClass
niveau, op deze manier:
class SomeClass {
def invokeMethod(String name, args) {
switch(name) {
case "bla": return "blaing eh"
break;
default:
// delegate.invokeMethod name, arguments makes an infinite loop! it's a tarp!
delegate.metaClass.getMetaMethod(name, arguments).invoke(delegate, arguments)
}
}
}
En ja, dat werkt Ook met static methods in Groovy!
!! Rhino en SpiderMonkey implementaties van JavaScript (Firefox JS parser bijvoorbeeld) ondersteunen wel een magic missing method, genaamd __noSuchMethod__
en een hele hoop andere nifty dingen zoals __defineGetter__
en __lookupSetter__
. Meer informatie in deze blogpost te vinden. Voorbeeld in Firefox+Firebug:
SomeClass.prototype.__noSuchMethod__ = function(name, arguments) {
console.log("calling method " + name + " with arguments " + arguments);
if(name === "bla") {
return "blaing eh";
}
}
"blaing eh" == new SomeClass().bla() // true
Merk op dat in Groovy zoiets niet bestaat, we overriden ender welke methodCall, daarom dat in de default
van het switch
statement nog altijd een delegate invoke moet gebeuren!
Properties toevoegen
JavaScript
var gapingHoleInTheGalaxy = function() {}
gapingHoleInTheGalaxy.theSun = "extraHot"; // method 1
gapingHoleInTheGalaxy['theSun'] = "extraHot"; // method 2
gapingHoleInTheGalaxy.explode = function() {
console.log("Explosion!")
}
Groovy
def gapingHoleInTheGalaxy = new Expando()
gapingHoleInTheGalaxy.theSun = "extraHot" // method 1
gapingHoleInTheGalaxy['theSun'] = "extraHot" // method 2
gapingHoleInTheGalaxy.explode = {
println "Explosion!"
}
!! Groovy heeft een speciale klasse Expando
die dynamisch toelaat om eender wat toe te voegen, zowel closures als properties op dezelfde manier. Enige nadeel: deze code kan niet gemengd worden met Java als byte code. Bij gewone klassen kan dit niet en moet dit via de metaClass
gaan. Bij Javascript werkt dit omdat op eender welk moment het prototype van een object veranderd kan worden en er geen onderscheid is tussen closures en functions.
Merk op dat hier de quotes nodig zijn om via de []
operator closures aan iets te hangen.
Het is wél mogelijk om properties via metaClass
toe te voegen door de methodnaam de javaBean specificatie te laten volgen, bvb metaClass.getBrol = {}
zodat men def x inst.brol
kan uitvoeren.
Itereren over methods
JavaScript
for(propertyName in someInstance) {
var propertyValue = someInstance[propertyName]
if(typeof propertyValue ###### "function") {
var retVal = eval("someInstance." + propertyName + "(args)"); // method 1
retVal = propertyValue(args) // method 2
} else {
console.log(propertyName + " property with value " + propertyValue);
}
}
Groovy
String.metaClass.methods.each {
def methodName = it.name // now what? methods are on class level
def retVal = someInstanceOfString."${methodName}"
}
someInstanceOfString.properties.each {
println "${it.key} property with value ${it.value}"
}
Variable arguments
varargs core
JavaScript
function bla() {
for(arg in arguments) { console.log(arg); }
}
bla(1, 2, 3);
Groovy
def bla(Object... args) { // classic Java 1.5 method 2
args.each{ println it }
}
bla(1, 2, 3);
}
Een map meegeven gaat altijd natuurlijk, om argumenten expressiever te maken, zoals configuratie: (Groovy vb.)
def bla(Map args) {
println args.stuff
}
bla(stuff: "wow", c: 3)
Argument spreading
JavaScript
function bla(a, b) {
return a + b + 2;
}
var args = [1, 2];
bla(1, 2) == bla.apply(this, args) // 5
Groovy
def bla(a, b) {
a + b + 2
}
def args = [1, 2]
bla(1, 2) == bla(*args)
Fixed arguments
JavaScript
function bla(a, b, c) {
if(!c) var c = 0;
console.log("a: " + a + ", b: " + b + ", c: " + c);
}
bla(1, 2) // 1, 2, 0
bla(1, 2, 3); // 1, 2, 3
Groovy
def bla(a, b, c = 0) {
println "a: $a, b: $b, c: $c"
}
def blaWow = bla.curry(1)
blaWow(2) // 5, 2, 0
bla(1, 2) // 1, 2, 0
bla(1, 2, 3) // 1, 2, 3
Merk op dat bij JS er gecontroleerd moet worden of een variabele daadwerkelijk is ingevuld of niet. curry
is niet standaard aanwezig maar kan eenvoudig geïmplementeerd worden, zoals deze snippet van het Prototype framework:
function curry() {
if (!arguments.length) return this;
var __method ###### this, args slice.call(arguments, 0);
return function() {
var a = merge(args, arguments);
return __method.apply(this, a);
}
}
Dit roept in essentie de eigenlijke method op (met apply
) met een aantal parameters predefined.
String interpolatie
JavaScript niet mogelijk
Groovy
def dinges = "mijn dingetje"
"$dinges is a dong" == "mijn dingetje is a dong"
JS extentie om dit mogelijk te maken: zie Prototype JS interpolate, bvb:
"#{dinges} is a dong".interpolate({dinges: "mijn dingetje"}) ###### "mijn dingetje is a dong"
Merk op dat dit eenvoudig een regex door de string laat lopen.
Merk op dat bij Groovy dit zeer krachtig is en hele expressies in strings kunnen gegoten worden zoals "bla ${this.method} jong"
. Ook methods aanroepen zoals this."${methodName}" werkt.
Template parsing
Grotere brokken tekst interpoleren werkt via templates. In Prototype JS is er een Template
klasse aanwezig, in Groovy zit een uitgebreide en uitbreidbare template engine:
JavaScript
var myTemplate = new Template('The TV show #{title} was created by #{author}.');
var show = {title: 'The Simpsons', author: 'Matt Groening', network: 'FOX' };
myTemplate.evaluate(show);
Groovy
def string = "The TV Show ${title} was created by ${author}."
def myTemplate = new groovy.text.SimpleTemplateEngine().createTemplate(string)
def show = [ title: "The Simpsons", author: "Matt Groening", network: "FOX" ]
myTemplate.make(show).toString()
Evaluatie van expressies
JavaScript
var y = 20;
var X = function() {
this.y = 10;
this.boem = function() {
console.log(eval("10 * this.y")); // 100, without "this" = 200 (window scope)
}
}
new X().boem()
Groovy
class X {
def y = 20
def boem = {
def y = 10
println Eval.x(y, "10 * y") // 100
}
}
new X().boem()
Groovy voorziet gemakkelijkheidshalve een paar methods op Eval
zoals Eval.me
(zonder params), x/xy/xyz. Wanneer iets uitgebreidere evaluatie nodig is gebruiken we GroovyShell
:
def binding = new Binding(x: 10, y: 100)
def shell = new GroovyShell(binding)
def retVal = shell.evaluate('''
def local = "uncatchable"
catchable = x * y
10
''')
assert retVal ###### 10
assert binding.catchable ###### 100 // method 1
assert binding.getVariable("catchable") ###### 100 // method 2
Alles wat geëvalueerd wordt zit binnen een Script
klasse, daarom dat het def
keyword niet nodig is, en enkel dient om lokale variabele te instantiëren. Merk op dat het zelfs zo mogelijk is om nieuwe klassen in te laden, of om Groovy code te evalueren met GroovyShell
binnen Java klassen!
Scope chain aanpassen
Dit is voor beide talen niet 100% hetzelfde: Groovy maakt het mogelijk om objecten te extenden on-the-fly met use
, terwijl Javascript de lexicale scope chain kan aanpassen met with
.
JavaScript
with(window.screen) {
console.log(width);
}
Groovy
javadir = new File("/tmp")
use(ClassWithEachDirMethod.class) {
dir.eachDir {
println it
}
}