lunes, junio 28, 2010

Cómo evaluar expresiones lógico-matemáticas en tiempo de ejecución (y usar la magia de TFormula)

C++ es un lenguaje poco dinámico. En otros lenguajes se dispone del propio lenguaje en tiempo de ejecución y, usando Eval o análogos, se puede evaluar cualquier expresión válida del lenguaje. Esto que puede ser una ventaja y puede ahorrar líneas de código puede ser también peligroso si se usa con poca cabeza. Como casi todo :)

Bueno, de todos modos lo que yo necesitaba no era eval como tal, sino evaluación de condiciones en tiempo de ejecución: poder decirle al programa qué condición debe evaluar en función de una serie de variables que dependen también de una configuración. Todo demasiado dinámico para que resultase sencillo de programar en C++. Había por lo menos tres alternativas, aparte de una cuarta que es la que he elegido :)

  • Programar una especie de ejecutor de reglas. Para el que tiene un martillo todo son clavos y si sólo te centras en el código la solución pasa por ahí. Mi intuición me dice que hiciese lo que hiciese me iba a quedar corto o introduciría bugs gordos potenciales. Mejor aprovechar algo existente...
  • Dejar la parte dinámica para un lenguaje dinámico. Hacer librerías que puedan ser llamadas desde dicho lenguaje. La descarté por varias razones, entre ellas el esfuerzo "librerizar" el código y la posible pérdida de rendimiento: la condición ha de evaluarse en le bucle principal y el rendimiento es un requisito clave.
  • Embeber un lenguaje dinámico dentro de programa. Introduciría dependencias nuevas. Sonaba seductor :), pero seguramente demasiado arriesgado

Prácticamente todas las soluciones son matar moscas a cañonazos y la que he elegido creo que es la más elegante de todas: usar una librería que ya estaba usando, cuya funcionalidad no conocía del todo (y que no está especialmente documentada...)

Así que esta entrada servirá además para mostrar una funcionalidad interesante de ROOT que podría ser usada por separado. Más o menos. El hecho es que ROOT, una librería de gestión y análisis de datos (en grandes cantidades) ya posee un evaluador de formulas que se llama TFormula. Es capaz de evaluar tanto expresiones matemáticas como lógicas o a nivel de bit. Cubre sobradamente mis exigencias. Al informarme más sobre su uso, vi que tenía inconvenientes: el nombre de las variables es fijo (x, y, z ,t) y no creía conveniente configurar las formulas basadas en esos nombres de variables o reemplazar a mano el nombre de las variables por las "por defecto". Entonces, buscando un poquito más encontré que en un sublibrería de ROOT, RooFit, existía una no muy documentada RooFormulaVar, que hace básicamente lo que yo quería, evaluar condiciones (y de paso formulas matemáticas) en base a una cadena de definición. El uso es relativamente sencillo una vez que se descubre cómo, porque no hay una documentación directa de cómo hacerlo. Un uso fácil sería:

RooRealVar a("a","a",3.);
RooRealVar b("b","b",7.);
RooFormulaVar plus(“aplusb”,”a+b”,RooArgSet(a,b));
//10
cout << plus.getVal() << endl;


Si se quiere usar con un número variable de argumentos, se puede usar otro constructor y da más juego, es un poquito más dinámico. Este ejemplo además usa operaciones a nivel de bit:

TClonesArray tca("RooRealVar",2)
RooRealVar *var1 = new (tca[1]) RooRealVar("var1","var2",0);
RooRealVar *var2 = new (tca[0]) RooRealVar("var2","var2",0);

RooFormulaVar opor("var1orvar2","var1&var2",RooArgSet(tca));

var1->setVal(2.);
var2->setVal(7.);
//2
cout << opor.getVal() << endl;

var1->setVal(5.);
var2->setVal(1.);
//1
cout << opor.getVal() << endl;

var1->setVal(8.);
var2->setVal(1.);
//0
cout << opor.getVal() << endl;

Por último y más importante para mí, evaluar condiciones lógicas, por ejemplo:

TClonesArray tca("RooRealVar",2)
RooRealVar *var1 = new (tca[1]) RooRealVar("var1","var2",0);
RooRealVar *var2 = new (tca[0]) RooRealVar("var2","var2",0);

RooFormulaVar cond("var1littvar2big","var1<1 || var2>10",RooArgSet(tca));


var1->setVal(0.);
var2->setVal(17.);

//true
cout << cond.getVal() << endl;

var1->setVal(3.);
var2->setVal(11.);

//true
cout << cond.getVal() << endl;

var1->setVal(8.);
var2->setVal(1.);
//false
cout << cond.getVal() << endl;

Espero que sirva a alguien y que esa persona se pueda ahorrar tiempo y código :)