Firmas invitadas |
Cálculo simbólico en C# 3.0
Publicado el 09/Ene/2007
|
1. IntroducciónEn nuestra columna anterior [3] introdujimos los árboles de expresiones de C# 3.0 y mostramos cómo éstos pueden ser utilizados para reflejar la estructura de expresiones (funciones) sobre uno o más parámetros (variables). Pensando un poco más sobre el tema, uno puede darse cuenta de que la presencia en las librerías de .NET de esta jerarquía de clases abre las puertas para realizar de una manera eficiente y más o menos cómoda la manipulación simbólica de expresiones, tarea que en general se reservaba antaño para lenguajes como LISP, en los que la representación uniforme de código y datos facilitaba en gran medida su transformación en ambas direcciones. En este artículo presentamos una posible implementación de una librería para el cálculo de derivadas de funciones matemáticas, ejemplo clásico de tarea de cálculo simbólico. Para una presentación básica del concepto de derivada, remitimos al lector a [4]; aquí es suficiente con que digamos que la derivada de una función f(x) (por ejemplo, x + sin(x)) es otra función (para el caso del ejemplo, 1 + cos(x)) que se calcula siguiendo un conjunto de reglas de manipulación simbólica como, por ejemplo, la regla de la suma: la derivada de una suma es igual a la suma de las derivadas de los sumandos.
2. Implementación de la libreríaLa tarea que tenemos por delante es la de implementar una librería que nos permita, dado un árbol de expresión (un objeto de la clase Expression<T>), obtener otro objeto de la misma clase que represente a la derivada de la función original. Llamaremos Derive() al método (sobrecargado) que se encargará de esta tarea; para implementarlo, aprovecharemos otra nueva característica de C# 3.0: los métodos extensores [2]. Gracias al uso de métodos extensores, podremos invocar al método Derive() sobre cualquier expresión utilizando la sintaxis natural de la orientación a objetos: // expr es de tipo Expression<Func<double, double>>
Console.WriteLine(expr.Derive());
en lugar de la más procedimental: // expr es de tipo Expression<Func<double, double>> // ExpressionExtensions es la clase en que está programado Derive() Console.WriteLine(ExpressionExtensions.Derive(expr)); El código fuente de la librería tiene la siguiente estructura: namespace Pokrsoft.Expressions { public static class ExpressionExtensions { public static Expression<T> Derive<T>(this Expression<T> e) { // … } public static Expression<T> Derive<T>(this Expression<T> e, string paramName) { // … } private static Expression Derive(this Expression e, string paramName) { // … } } public class ExpressionExtensionsException: Exception { public ExpressionExtensionsException(string msg) : base(msg, null) { } public ExpressionExtensionsException(string msg, Exception innerException) : base(msg, innerException) { } } }
La clase ExpressionExtensions ofrece dos variantes de Derive():
La primera de las variantes se implementa de la siguiente forma: public static Expression<T> Derive<T>(this Expression<T> e) { // check not null expression if (e == null) throw new ExpressionExtensionsException("Expression must be non-null"); // check just one param (variable) if (e.Parameters.Count != 1) throw new ExpressionExtensionsException("Incorrect number of parameters"); // check right node type (maybe not necessary) if (e.NodeType != ExpressionType.Lambda) throw new ExpressionExtensionsException("Functionality not supported"); // calc derivative return Expression.Lambda<T>(e.Body.Derive(e.Parameters[0].Name), e.Parameters); } Después de rechazar los casos incorrectos, el método llama a otro método extensor, en este caso de la clase no genérica Expression (que hemos presentado en [3]), y al que también hemos llamado Derive(), pasándole como parámetro el cuerpo de la expresión lambda, disponible a través de la propiedad Body de la instancia original. El método extensor para la clase Expression (que hemos declarado private para hacerlo invisible al exterior) es el “caballo de batalla” que implementa toda la funcionalidad de cálculo de derivadas de una manera recursiva. En él se expresan las reglas descritas en [4] para los distintos tipos de nodo posibles. A continuación se muestra un fragmento de la implementación: private static Expression Derive(this Expression e, string paramName) { switch (e.NodeType) { // constant rule case ExpressionType.Constant: return Expression.Constant(0.0); // parameter case ExpressionType.Parameter: if (((ParameterExpression) e).Name == paramName) return Expression.Constant(1.0); else return Expression.Constant(0.0); // sign change case ExpressionType.Negate: Expression op = ((UnaryExpression) e).Operand; return Expression.Negate(op.Derive(paramName)); // sum rule case ExpressionType.Add: { Expression dleft = ((BinaryExpression) e).Left.Derive(paramName); Expression dright = ((BinaryExpression) e).Right.Derive(paramName); return Expression.Add(dleft, dright); } // product rule case ExpressionType.Multiply: { Expression left = ((BinaryExpression) e).Left; Expression right = ((BinaryExpression) e).Right; Expression dleft = left.Derive(paramName); Expression dright = right.Derive(paramName); return Expression.Add( Expression.Multiply(left, dright), Expression.Multiply(dleft, right)); } // *** other node types here *** default: throw new ExpressionExtensionsException( "Not implemented expression type: " + e.NodeType.ToString()); } }
Después de agregar una referencia a la librería en cualquier proyecto LINQ, solo será necesario importar el espacio de nombres Pokrsoft.Expressions, y podremos calcular derivadas de funciones de la siguiente forma: Expression<Func<double, double>> circleAreaExpr = (radius) => Math.PI * radius * radius; Console.WriteLine(circleAreaExpr.Derive()); El resultado que obtendremos en pantalla (indentado para una mejor comprensión) será: radius => Add( Multiply( Multiply( 3,14159265358979, radius), 1), Multiply( Add( Multiply( 3,14159265358979, 1), Multiply( 0, radius)), radius)) que luego de las simplificaciones convenientes (vea el siguiente punto) se reduce a 2 * Math.PI * radius, el resultado correcto.
3. Trabajo pendienteUna implementación completa del método para el cálculo de derivadas no es una tarea sencilla, y la versión que se ofrece con el código fuente de este artículo no es completa; fundamentalmente queda trabajo pendiente en lo relativo a la programación de las derivadas de las diferentes funciones elementales (logarítmicas, trigonométricas, etc.) que pueden aparecer en una expresión. A modo de ejemplo, sí hemos provisto una implementación de la conocida regla de la potencia [4]: case ExpressionType.MethodCall: Expression e1 = null; MethodCallExpression me = (MethodCallExpression) e; MethodInfo mi = me.Method; // TEMPORARY // method should be static and its class - Math if (!mi.IsStatic || mi.DeclaringType.FullName != "System.Math") throw new ExpressionExtensionsException("Not implemented function: " + mi.DeclaringType + "/" + mi.Name); ReadOnlyCollection<Expression> parms = me.Parameters; switch (mi.Name) { case "Pow": // power rule e1 = Expression.Multiply( parms[1], Expression.Call(mi, null, new Expression[] { parms[0], Expression.Subtract( parms[1], Expression.Constant(1.0)) })); break; default: throw new ExpressionExtensionsException("Not implemented function: " + mi.Name); } // chain rule return Expression.Multiply(e1, parms[0].Derive(paramName));
Un pequeño inconveniente de la actual implementación de los árboles de expresiones es la ausencia de un tipo de nodo para la operación “elevar a potencia”; por esta razón, hemos tenido que asumir que esta operación se expresará mediante una llamada al método Math.Pow(). Otra de las tareas pendientes que podemos recomendar al lector como ejercicio es la implementación de un método extensor para la simplificación y factorización de expresiones. La programación de reglas tan simples como 0 + y = y ó 1 * y = y para cualquier y dado nos habría permitido obtener un resultado mucho más legible en el punto anterior. Por último, una tarea más compleja, pero que se antoja interesante, es la de dibujar gráficamente un árbol de expresión, utilizando los símbolos tradicionales de la notación matemática.
4. ConclusionesEn este artículo netamente práctico hemos mostrado una de las muchas posibilidades que se abren gracias a la disponibilidad de los árboles de expresiones en C# 3.0. El código fuente del ejemplo utilizado en el artículo está disponible para su descarga. Para poder compilarlo y ejecutarlo satisfactoriamente, se deberá instalar inicialmente la Presentación Preliminar de LINQ de Mayo de 2006, disponible en [1].
5. Referencias
|
Código de ejemplo (ZIP): |
Fichero con el código de ejemplo:
Octavio_Derivadas.zip - 3.79 KB
|