Cómo evaluar expresiones matemáticas en VB .Net

Por José G. Alvarez

Contenido

 Introducción
 La clase Evaluador
 El método PrecompilarAssembly
 El método Evaluar
 Implementado Evaluador
 Conclusión
 Referencias

Introducción

Generalmente denominamos funciones, a las expresiones matemáticas o no, posibles de ser evaluadas. Sin embargo, para simplificar este artículo, las llamaremos sencillamente "expresiones", para no confundirlas con el concepto de función en los lenguajes de programación.

En algunas ocasiones nos encontramos con la imposibilidad de evaluar directamente una expresión en VB .Net. Y es que desde los inicios del Basic, nunca ha existido una función propia del lenguaje que evalúe una expresión.

Recuerdo con nostalgia cuando, con GWBasic, aprendí un artilugio que consistía en que nuestro código creara un archivo secuencial (con extensión .bas) que contuviera el código de una función, y dentro de esta función escribiéramos la expresión que queríamos evaluar. Claro, esto era posible pues GW era un lenguaje interpretado y no existía un compilador que generara un ejecutable.

En VB el truco consistía en usar una referencia a un objeto Microsoft Script Control y usar su método Eval. Como se muestra en el ejemplo siguiente:

Dim oSC As New ScriptControl
Dim expMath As String
Dim expToEval As String
 
oSC.Language = "VBScript"
 
expMath = "X ^ 2 + 2 * X + 1"
expToEval = Replace(expMath, "X", 5)
 
MsgBox oSC.Eval(expToEval)

Esta técnica, aunque resulta muy útil y en algunos casos soluciona nuestros problemas, está limitada a que sólo podemos usar objetos y/o funciones definidas por VBScript dentro del parámetro Expression del método Eval.

Todo esto ha servido para que reflexione sobre cuán viejo soy, aunque éstas no son las técnicas de reflexión que se usan en VS .Net. De éstas vamos a hablar más adelante.

Si bien en VB .Net podemos hacer referencia a un objeto Microsoft Script Control, tendríamos las mismas desventajas que con versiones anteriores de VB. ¿Cómo podemos entonces evaluar una expresión en VB .Net, sin desaprovechar las bondades de .Net? Si recordamos el artilugio que se usaba con GWBasic, lo desempolvamos y lo implementamos bajo la plataforma .Net, podremos no solo evaluar una expresión, si no crear y ejecutar cualquier código de forma dinámica. Sin embargo, por ahora nos conformaremos con crear una clase que evalúe una expresión matemática.

Crearemos entonces una clase Evaluador. Esta creará un assembly con el código que deseamos ejecutar, en este caso un método o función con la expresión a evaluar. Luego, usando la reflexión invocará este método y obtendremos los resultados. Suena sencillo... ¡Realmente lo es!

La clase Evaluador

La clase Evaluador contendrá un objeto privado (oEnsamblado) de tipo assembly (del espacio de nombres System.Reflection) y dos métodos públicos: PrecompilarAssembly y Evaluar. He decidido separar ambos métodos por cuestiones de rendimiento. Si combináramos los dos métodos y fuésemos a dibujar la grafica de una expresión, el rendimiento final sería simplemente inaceptable. La compilación de un assembly requiere una cantidad de proceso considerable.

El objeto oEnsamblado será la representación del assembly que compilaremos. Este assembly, como mencionamos anteriormente, será una clase (EvalClase) que expondrá un único método (Eval) que contendrá la expresión a evaluar y retornará el resultado:

Imports System.Text
Imports System.CodeDom.Compiler
Imports System.Collections.Specialized
 
   Public Class Evaluador
   Private oEnsamblado As System.Reflection.Assembly
 
   Public Sub PrecompilarAssembly ...
 
 
   End Sub
 
 
   Public Function Evaluar ...
 
 
   End Sub

El método PrecompilarAssembly

El método PrecompilarAssembly, como su nombre lo indica, precompila el assembly de la clase que contiene un método con la expresión a evaluar. Vamos a visualizar el código a generar y los posibles escenarios dependientes de la expresiones a evaluar.

Supongamos que deseamos evaluar la expresión matemática y(x) = x + 3. El código necesario sería algo así como lo que se muestra a continuación:

Public Class EvalClase
   Public Shared Function Eval(ByVal X As Double) as Object
      Return X + 3
   End Function
End Class

Por otro lado, supongamos que queremos evaluar z(x , y) = Log(x) + y:

Imports System.Math
 
Public Class EvalClase
   Public Shared Function Eval(ByVal X As Double, ByVal Y As Double) as Object
      Return Log10(X) + Y
   End Function
End Class

Vemos que existen tres factores cambiantes: la expresión, los parámetros del método Eval y los namespaces. Por lo tanto nuestro método Precompilarfuncion recibirá tres parámetros, correspondientes a esos elementos. Veamos entonces el método PrecompilarAssembly:

Public Function PrecompilarAssembly(ByVal Funcion As String, _
ByVal ParametrosList As StringCollection, ByVal NameSpaceList As StringCollection) As Boolean
 
Dim mStrings As String
   Dim mParametros As String
   'Definimos un objeto de tipo StringBuilder que contendra el código a compilar
   Dim CodigoFuente As New StringBuilder()
 
'Agregamos los Imports necesarios a nuestro codigo fuente 
   For Each mStrings In NameSpaceList
      CodigoFuente.Append("Imports " & mStrings & vbCr)
   Next
 
'Preparamos un string con los parametros que usará el metodo Eval 
   'de de la clase EvalClase 
   For Each mStrings In ParametrosList
      mParametros &= ", " & mStrings
   Next
 
mParametros = Trim(mParametros)
   If mParametros.Length > 0 Then
      mParametros = Trim(Mid(mParametros, 2))
   End If
 
'Terminamos de construir la clase a compilar
    CodigoFuente.Append("Public Class EvalClase" & vbCr)
    CodigoFuente.Append(" Public Shared Function Eval(" & _
       mParametros & ") as Object" & vbCr)
    CodigoFuente.Append(" Return " & Funcion & vbCr)
    CodigoFuente.Append(" End Function " & vbCr)
    CodigoFuente.Append("End Class " & vbCr)
 
'Creamos una instancia de la clase VBCodeProvider 
    'que usaremos para obtener una referencia a una interfaz ICodeCompiler
    Dim oCProvider As New VBCodeProvider()
    Dim oCompiler As ICodeCompiler = oCProvider.CreateCompiler
 
'Usamos la clase CompilerParameters para pasar parámetros al compilador
    'En particular, definimos que el assembly sea compilado en memoria.
    Dim oCParam As New CompilerParameters()
    oCParam.GenerateInMemory = True
 
'Creamos un objeto CompilerResult que obtendrá los resultados de la compilación
    Dim oCResult As CompilerResults
    oCResult = oCompiler.CompileAssemblyFromSource(oCParam, CodigoFuente.ToString)
 
'Comprobamos que no existan errores de compilación.
    Dim oCError As CompilerError
    If oCResult.Errors.Count > 0 Then
       'Si existen errores los mostramos.
       'Si bien, podriamos implementar un mejor método para visualizar 
       'los errores de compilación, este nos servirá por los momentos.
       For Each oCError In oCResult.Errors
          MsgBox(oCError.ErrorText.ToString)
       Next
       Return False
    Else
       'Como el ensamblado se generó en memoria, debemos obtener 
       'una referencia al ensamblado generado, para esto usamos 
       'la propiedad CompiledAssembly
       oEnsamblado = oCResult.CompiledAssembly
       Return True
    End If
End Sub

El método PrecompilarAssembly devolverá True o False dependiendo de si la compilación tuvo éxito o no. El assembly compilado en memoria, estará referenciado por el objeto oEnsamblado.

El método Evaluar

Ahora necesitamos invocar el método Eval de nuestro assembly recientemente generado. Para ello, haremos uso de las técnicas de reflexión:

Public Function Evaluar(ByVal ParamArray Parametros() As Object) As Object
    If oEnsamblado Is Nothing Then
       Return Nothing
    Else
       'Instanciamos la clase EvalClase de nuestro assembly 
       'creando un tipo a partir de ella.
       Dim oClass As Type = oEnsamblado.GetType("EvalClase")
 
'Usamos GetMethod para accesar al método Eval, e invocamos este con los parametros necesarios.
       Return oClass.GetMethod("Eval").Invoke(Nothing, Parametros)
    End If
End Function

Implementado Evaluador

Veamos ahora cómo usar nuestra clase Evaluador. En el ejemplo se evaluara la expresión z(x , y) = Log(x) + y; con x = 100 , y=3.

Nota: recuerda que en notación matemática, Log(x) simboliza el logaritmo de x en base 10; luego, debemos usar la función Log10.

'Creamos una nueva instancia de la clase Evaluador
Dim mEval As New Evaluador.Evaluador()
 
'Creamnos una variable tipo string y le asignamos la expresión que queremos evaluar
Dim mExpresion As String = "Log10(X) + Y"
 
'Creamos un objeto StringCollection y agregamos los parámetros de entrada que usará el método eval
Dim mParameters As New StringCollection()
mParameters.Add("ByVal X as Double")
mParameters.Add("ByVal Y as Double")
 
'En este caso, la función Log10() Pertenece al espacio de nombres System.Math. 
'se hace necesario entonces, crear un objeto StringCollection y agregar 
'el namespace System.Math.
Dim mNameSpaces As New StringCollection()
mNameSpaces.Add("System.Math")
 
'Invocamos el método PrecompilarFunción y verificamos si se genero correctamente el assembly.
If If mEval.PrecompilarFuncion(mExpresion, mParameters, mNameSpaces) Then
    'Si el assembly se generó correctamente, creamos un array con los valores de los parametros a evaluar
    Dim mParam() = {100, 3}
    'invocamos el método Evaluar y mostramos el resultado
    MsgBox(mEval.Evaluar(mParam))
Else
    MsgBox("No se ha generado el Assembly")
End If

Conclusión

Hemos utilizado las técnicas de compilar mediante código para crear un assembly en memoria y luego, mediante la reflexión, invocar sus métodos. Se me ocurren en este momento no menos de 10 mejoras que se pueden hacer a la clase Evaluador. Sin embargo, la he construido con un propósito específico: crear una clase Graficador que implementará la clase Evaluador para graficar funciones matemáticas y para este fin, está más que bien como está ahora.

Referencias

CÓMO: Compilar código mediante programación utilizando el compilador de Visual Basic .NET

Generar código fuente y compilar un programa a partir de un gráfico CodeDOM

José G. Alvarez trabaja como Desarrollador especializado en tecnologías de desarrollo Microsoft; puedes contactarle por e-mail a jgalvarezr@hotmail.com.

Mostrar: