Firmas invitadas
 

Cálculo simbólico en C# 3.0

 

Publicado el 09/Ene/2007
Actualizado el 09/Ene/2007

Autor: Octavio Hernández
[email protected]
PoKRsoft, S.L.
 
English version English version

En este artículo netamente práctico se muestran las posibilidades para la manipulación simbólica que se abren gracias a la disponibilidad de los árboles de expresiones en C# 3.0.


 

1. Introducción

En 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ía

La 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():

  • Una con un único parámetro, la expresión a derivar. Esta variante asume que la expresión tiene una única variable. Observe la presencia del modificador this delante del tipo del parámetro del método; esto identifica al método como un método extensor de ese tipo.
  • Una segunda variante con un segundo parámetro, el nombre de la variable por la que derivar. Con esta variante se implementan las derivadas parciales; básicamente, si una función tiene varias variables, se puede calcular la derivada usando cualquiera de ellas como “la variable” y considerando las demás como valores constantes.

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 pendiente

Una 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. Conclusiones

En 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

  1. Recursos relacionados con C# 3.0 y LINQ: http://msdn.microsoft.com/csharp/future/default.aspx.
  2. Hernández, Octavio “Lo que nos traerá Orcas: novedades en C# 3.0”, publicado en dotNetManía Nº 24, marzo de 2006.
  3. Hernández, Octavio “Las expresiones lambda en C# 3.0”, publicado en  http://www.elguille.info/NET/futuro/firmas_octavio_ArbolesExpresiones.htm, enero de 2007.
  4. “Derivative”, en Wolfram MathWorld, http://mathworld.wolfram.com/Derivative.html

 

 


Código de ejemplo (ZIP):

 

Fichero con el código de ejemplo: Octavio_Derivadas.zip - 3.79 KB

(MD5 checksum: 048B3E07938B3D73DA9944B57729B48E)

 


Ir al índice principal de el Guille

Valid XHTML 1.0 Transitional