Emulate Object Oriented in Scilab - part 2
Abstract
The current version of the Scilab language is procedural. The goal of this page is to explore ways to emulate OO in the Scilab language.
Contents
Introduction
The goal of this page is to show how to make a OO-like system in Scilab, based on a C++ hash map.
See in attachment tbxoo-v0.2.zip for a .zip which contains the full toolbox, validated on Windows.
On the use of hash maps
I make the hypothesis that I have a C++ class, "MyClass", which has 3 members : type, a, b and a "print" method. The goal is to be able to write in the Scilab console :
vu1 = myclass_new("Normale",1.0,0.5); myclass_print(vu1); myclass_destroy(vu1);
The method is based on the creation of an intermediate layer, base on a C++ hash map, which allow to associate an integer (the "token") with the C++ object. The map is managed by the file myclass_map.cpp, which contains :
typedef map<int , MyClass *> MyClass_map_type; MyClass_map_type MyClass_map; int MyClassCounter = 0;
The constructor is associated with MyClass_map_new, which takes as input arguments the creation parameters, and returns an integer :
int MyClass_map_new (char * type, double a, double b) { MyClass * rv; int token; string name(type); rv = new MyClass(name, a, b); token = MyClassCounter; MyClassCounter = MyClassCounter + 1; MyClass_map[token] = rv; return token; }
The destructor takes as input argument the integer token and free the C++ object :
void MyClass_map_free ( int token ) { MyClass_map_type::iterator it; MyClass * rv = MyClass_map[token]; free(rv); it = MyClass_map.find (token); MyClass_map.erase(it); }
The print method takes the token as input argument and triggers the corresponding method of the object :
void MyClass_map_Print ( int token ) { MyClass * rv = MyClass_map[token]; return rv->Print(); }
One can easily implement the static methods (class methods) "size" and "tokens" which returns respectively the number of current objects and a Scilab array which contains the current tokens. The following script shows how to use these methods from the Scilab language :
nbvu = myclass_size(); // nbvu is 0 vulist = myclass_tokens(); // vulist is [] vu1 = myclass_new("Normale",1.0,0.5); vu2 = myclass_new("Uniforme",1.0,2.5); nbvu = myclass_size(); // nbvu is 2 vulist = myclass_tokens(); // vulist is [0 1] myclass_destroy(vu1); myclass_destroy(vu2); nbvu = myclass_size(); // nbvu is 0 vulist = myclass_tokens(); // vulist is []
There is no difficulty to create the gateways.
The following is a list of advantages / drawbacks.
Advantages
- it is possible to manage C++ objects in the Scilab language.
- the system does not require a lot of memory. Just a single integer is sufficient to store the index of the object in the hash map.
- the system is simple.
the automatic completion gives rich informations. With the current method, type "myclass" + <TAB> in the console and you get the complete list of methods which are associated with
the class "myclass".
- it will be possible to generate most gateways with SWIG. This is because each gateway is, most of the time, very simple with respect to the management of input / output parameters. Indeed, each function has a specific role so that the input arguments are most of the fixed in number and in type.
Drawbacks
- the existing class must be encapsulated with a hash map access. This is tedious and repetitive, therefore sources of bugs.
- there is no verification of type. Suppose that two objects are implemented with this method, say "a" for "myclass" and "b" for "mybetterclass". Now, if you pass the token "b" to a method of the class "myclass", it is very probable that the integer a will index an existing position in the hash map, i.e. the method will have an unexpected effect, but will not generate an error. Respectively, it may index a position which does not exist. That may lead to bugs difficult to analyse.
One gateway for several methods, designated by strings
Another possibility is to manage a single entry point (i.e. a single gateway), say "myclass" for example, which takes as first argument a string which represents the action to perform : "new", "destroy", "print". This method is used for example in grand.
vu1 = myclass("new","Normale",1.0,0.5); myclass("print",vu1); myclass("destroy",vu1);
Advantages
The method has the advantage of requiring only one gateway, which seems to be simpler. There is only one file to manage.
As we are going to see, this is the only advantage, against many drawbacks.
Drawbacks
This method has the following drawbacks.
- The gateway is much longer and difficult to extend.
- The automatic completion in the console does not give good information. For example, the automatic completion in the console does not give any information about the action provided by the grand function. Instead, the "tbx_*" commands are completed in the console so that the development of a script is much faster.
- The associated help is much more complicated to write and to read. For example, the help page for the "grand" function is very long, with some parts which are common to all "actions", and some parts which are specific of one or several "actions". This leads to a complicated help page, both complicated to write and to read. Instead, the several pages associated with the "tbx_*" commands are much simpler to write, to maintain and to read.
- To add a new method (i.e. to add a new "action"), one must modify the existing source code, the existing unit test, the existing help. This might leads to bugs.
- The gateway does not reflect exactly the Object Oriented model. If such a gateway was updated into a OO method, that would require to do a re-engineering of the unique gateway, which is probably complicated. Indeed, the fact that there is a single gateway may lead to factorization of some parts of the source code inside the gateway.
- It will be difficult to generate the gateway with SWIG. This is because each "action" correspond to a different set of input / output arguments. Since the SWIG description file is based on the definition of these arguments, generating the gateway will not be possible easily. Indeed, the unique gateway does not have a specific role : it must manage all "actions" and, therefore, must accept a variable number of input / output arguments with various data types. This forbids the use of SWIG, which can handle more easily simple cases where the number of input/output arguments is fixed, with fixed data types.
In short, this way of developping the gateway does not "scale" well with the number of methods.
Indeed, the gateway must contain the following sequence of if/then/else :
if ( action == "new" ) { // Source code for "new" } elseif if ( action == "print" ) { // Source code for "print" } elseif if ( action == "destroy" ) { // Source code for "destroy" }
In practice, this leads to very long gateways, which are much more difficult to maintain. The source code in the gateway is typically of length n * 100, where n is the number of methods (i.e. "actions"). For example, the grand gateway contains 1000 source code lines. This is much more complicated to maintain than 10 gateways with 100 lines by gateway.
In practice, this also leads to some confusion in the design of the gateway. Indeed, the same gateway serves different purposes. In the end, the exact role of the gateway is complicated to understand. For example, grand allows to generate random numbers. But it also allows to initialize the seed for some of the random number generators. All generators do not require the same number of seeds, which leads to a complicated management of the input arguments.
The unit tests of such gateways are also more difficult to write. This is because updating the source code for one method may lead to a modification of the behaviour of another method, because the source code is the same. This might lead to increasing testing times.
Instead of doing this, a much simpler approach is to create one function by method (i.e. one function by "action"). In the following session, we use a class "myclass" which is associated to 3 methods.
vu1 = myclass_new("Normale",1.0,0.5); myclass_print(vu1); myclass_destroy(vu1);
This way of developing leads to separate gateways, separate helps and separate unit tests. It is much simpler to develop, maintain and understand and is much closer to an 00 methodology.
Conclusion
In practice, the method based on hash maps performs quite well. But some improvments may be necessary / useful, for example :
- check that the iterator exists before applying the method. Generate a method if not.
- implement a collective destroy, which destroys all existing objects.