Be a Supporter!

Fuinctors

  • 322 Views
  • 0 Replies
New Topic Respond to this Topic
kiwi-kiwi
kiwi-kiwi
  • Member since: Mar. 6, 2009
  • Offline.
Forum Stats
Member
Level 09
Programmer
Fuinctors 2012-02-08 16:57:30 Reply

Picking up where we left off, the next logical step in building a parser would be to somehow define the interactions between data types. Now because this is 2012 and the olden days where procedural programming was the de facto standard are way behind us, weâEUTMd like for this script parser to support these minimum requirements:

Operators for every possible combination of Invariants ( +, âEU" etc)
Function scope
Objects
Duck Typing

And we want to be able to do this in a reasonable amount of time, so of course hardcoding behavior on a per Invariant basis being a massive overkill from this point of view falls from the start. What we actually need here is a way to generate an abstractization of this behavior on which we can build when time comes.

Luckily hereâEUTMs where we can take a lesson from the C++ architecture. You see, in C++ object orientedness is somewhat of an illusion propagated by the compiler, every member function you declare in a class is ultimately transformed into a procedural style function to which compiler passes the this pointer, which is why you can abuse the compiler and do stuff like the following:

//Foo.h

class Foo{

private:

 Foo();

public:

 int Bar();

};

 //Foo.cpp

#include"Foo.h"

int Foo::Bar(){

 return8;

} 

//main.cpp

#include "Foo.h" #include<cstdio> 

int main()

{

 Foo*foo=NULL;

 foo->Bar();

 return0;

}

without fear of a crash. Beyond the fact that this serves to prove that C++ can sometimes resemble the goatse of object oriented programming, we can take a mental note on implementing the architecture we want. So we need a way to store functions in a invariant object and pass the this pointer at runtime when we call a function that we previously added to an invariant.

The next thing C++ can teach is that we can think of operators as functions, which is indeed particularly interesting because having decided that we want objects, we can now store operators as one of the functions the object has and call it when we encounter it in our parser. This also has the advantage that we can name it to match the operator it represents so that when the time comes to interpret the parse tree, when you encounter an operator node all you have to do is call the method that matches the operatorsâEUTM string.

This is where Functors ( short for Function Objects ) come in. You know how we defined an interface for the Invariant, something that all invariants should have so that we could let them worry about managing the state of the underlying data so that we can use types interchangeably in our script. Well this is what weâEUTMre going to do with Functors:

class Functor{

public:

 InvUnknown* (*Func)(InvUnknown* args);

 Functor(InvUnknown* (*function)(InvUnknown* args)){

 Func = function;

 }

 virtual InvUnknown* apply(InvUnknown* args){return Func(args);}

};

So basically we took a function pointer and wrapped it in an object that has the apply method so that we can call this function at our convenience.

Now here is where it gets interesting because as you can see the Functor apply method takes a InvUnkown pointer as a argument and returns a InvUnknown pointer itself. Well because the Invariant was designed to abstract data and since we can pass any type of invariant to the function, this means that not only can we abuse the List invariant to pass as many arguments as we want to the function (not to mention return as many values as we want from the function), but this way we get the Duck Typing as well.

And last but not least the function scope can be implemented by adding a map to the Functor object so that we can store temporary objects.

Up until now all is good in theory, but a real world example wouldnâEUTMt hurt, so allow me to demonstrate this by implementing a push function for a list.

First of all we need to update the InvUnknown class to take advantage of these functors:

class InvUnknown{

public:

virtual bool inline is(inttype){returninv_type==type;}

virtual const string toString() = 0;

virtual void nativeCall(InvUnknown*param) = 0;

virtual InvUnknown* call(string& name,InvUnknown* params)

{

 Functor* method=methods[name];

 if(!method)printf("No method with name %s found",name.c_str());

   assert(params->is(LIST) && "Function parameters must be passed as LIST invariants");

 params->nativeCall(this);

 return method->apply(params);

}

protected:

 InvUnknown(int type){inv_type=type;} 

 map<string,Functor*>methods;

 void inline addMethod(string&name,InvUnknown* (*function)(InvUnknown*)){

 Functor* method = new Functor(function); 

 methods.insert(pair<string,Functor*>(name,method));

 }

private:

 int inv_type;

};

Now we have the means to store and access Functors from our Invariant. Notice the native call though. In my opinion this is a hack that is unfortunately needed because upcasting in a base class is a definite no go and because the push Functor needs to take a list of values as a parameter, so the list has to be full when we call push.

Of course another solution would be to overload the apply method to take a vector<InvUnknown*> instead and it would basically be the same thing seeing as our list is in fact a wrapper for a vector<InvUnknown*>, but that wouldnâEUTMt be fair because that way you create an opening for bad practices. If the API you have to use in your code is the same as the one you expose to the script, this ensures that you are actually using what you create and you eliminate furstrations as you use it. Having a separate API that gives you more flexibility and or power than whatever you expose to the script usually shows that something isnâEUTMt planned right.

Onto the list definition:

class List:public Invariant<vector<InvUnknown*>,LIST>

{

 public:

 List();

 virtual const string toString();

 void inline nativeCall(InvUnknown* elem){data.insert(data.begin(),elem);}

 static InvUnknown* push(InvUnknown* args); } 

And the implementation

List::List(){

stringpush_name="push";

addMethod(push_name,&push);

}

const string List::toString()

{

 stringstream buffer;

 string retval;

 buffer << "[";

 vector<InvUnknown*>::iterator paramIt = data.begin();

 for(;paramIt != data.end();++paramIt)

 {

 if((*paramIt) == this) buffer << "this";

 else buffer << (*paramIt)->toString();

 if(paramIt != data.end()-1) buffer << ",";

 }

 buffer << "]";

 buffer >> retval;

 return retval;

}

InvUnknown*List::push(InvUnknown*params)

{

assert(params->is(LIST) && " ALL function parameters need to be passed as LIST objects");

List* paramList = (List*)params;

vector<InvUnknown*>data = paramList->getData();

assert(data[0]->is(LIST) && "List function push called on non list");

List*parent=(List*)data[0];

for(unsigned int paramIt = 1;paramIt < data.size();++paramIt)
 parent->nativeCall(data[paramIt]);
 return parent;  
}

And last but not least the actual usage of the push function:

InvUnknown* l1 = new List();

InvUnknown* l3 = new List();

InvUnknown* l2 = new Primitive();

InvUnknown* args = new List();

string push_name = "push";

args->nativeCall(l3);

args->nativeCall(l2);

l3->call(push_name,args);

args->nativeCall(l1);

args->nativeCall(l1);

args->nativeCall(l2);

args->nativeCall(l2);

l1->call(push_name,args);

delete l1;

delete l2;

delete args;

Which produces the following output:

[this,0x227d858]
[[this,0x227d858],0x227d858,[this,0x227d858],this,this,0x227d858,0x227d858]

Which means that I can add Primitives (Invariant) and Lists and I can just as easily add just about any other Invariant I fancy, success.
Next time we talk a bit about scope.