Funciones locales en C# 7.0

“And if my shadow is all that survives
I’m still alive …”
Meat Loaf, “Alive” (2006)

Aprovecho la suerte de que este blog sigue aún activo (¡muchas gracias por ello a mis buenos amigos de Plain Concepts!) para dar señales de vida después de hace bastante tiempo.

Revisando varios recursos online relacionados con las novedades incorporadas a C# 7.0 (incluyendo una excelente entrega de la serie Plain TV, que sigo regularmente), me pareció sumamente curioso que prácticamente ninguno de los vídeos consultados utilizaran, al presentar las funciones locales (local functions, véase descripción en MSDN), lo que para mí es el ejemplo por excelencia de uso de las mismas. Después de darle vueltas al tema por un buen rato, he llegado a la conclusión de que probablemente todo es consecuencia de varias décadas de enseñanza de la programación utilizando lenguajes que no ofrecen (u ofrecían) la posibilidad de anidar funciones (léase Java, C++ y el propio C#). Allá por los años 80, la Universidad de La Habana y muchos otros centros docentes utilizaban como vehículo de iniciación el lenguaje Pascal, que siempre ofreció esa posibilidad; no creo que ninguno de mis antiguos alumnos se haya “escapado” de haber visto algún ejemplo de utilización de esta característica para acotar el ámbito (visibilidad) de un  procedimiento/función.

El patrón de utilización de funciones anidadas al que me refiero se presenta comúnmente al programar algoritmos basados en la estrategia “divide y vencerás”, y lo ejemplificaré aquí a través de una implementación del método QuickSort1 para ordenar un array de enteros en su sitio (in place). Al aplicar esta estrategia, aparecen funciones auxiliares cuyo ámbito natural es la rutina como parte de la cual han sido concebidos; muy frecuentemente estas funciones son recursivas. En el caso de QuickSort, cuya implementación muestro más abajo, esta posibilidad de anidamiento se presenta a varios niveles:

  • Al nivel más externo, la necesidad de ofrecer una implementación “limpia” nos exige exponer un método que solo requiera como parámetro el array a ordenar. Pero la naturaleza misma del algoritmo, como se describe a continuación, nos exige disponer de otra versión que reciba los índices inferior y superior sobre los que se va a actuar en el marco de una llamada cualquiera. Una función anidada es la solución ideal, dado que esta última versión no necesita ser expuesta al exterior (ni  siquiera otros métodos de la clase).
  • La idea central del algoritmo consiste en colocar de manera económica uno de los valores en el array en su posición final, con todos los valores menores que él a su izquierda y los mayores a su derecha (un proceso que se conoce como partición), para luego aplicar recursivamente la ordenación a los dos sub-arrays situados, respectivamente, a la izquierda y la derecha de ese elemento pivote. En nuestra implementación, la versión “interna” de QuickSort hace a su vez uso de una función local a ella, Partition, para hacer la partición del fragmento de array recibido.
  • Esta idea de descomposición funcional “hacia adentro” podría extenderse tanto como fuera necesario; a riesgo de lucir pedante, en el ejemplo he creado dentro de Partition una función Swap para intercambiar los valores de dos elementos del array, dados sus índices.

Observe el lector que el uso de funciones locales no solo produce un encapsulamiento más correcto del código, sino que además permite capturar en la función anidada los parámetros y variables locales de la función que la contiene, lo que se traduce tanto en economía de expresión como en mejoras en el rendimiento.

namespace Geeks
{
   public static class Utilities
   {
      public static void QuickSort(this int[] a)
      {
         void QuickSort(int first, int last)
         {
            int Partition()
            {
               void Swap(int p, int q)
               {
                  int tmp = a[p];
                  a[p] = a[q];
                  a[q] = tmp;
               }
 
               int pivot = a[first], index = first;
               Swap(index, last);
               for (int i = first; i < last; i++) 
                  if (a[i] < pivot) Swap(index++, i);
               Swap(index, last);
               return index;
            }

            if (first < last)
            {
               int pivotIndex = Partition();
               QuickSort(first, pivotIndex - 1);
               QuickSort(pivotIndex + 1, last);
            }
         }

         QuickSort(0, a.Length - 1);
      }
   }

   class Program
   {
      static void Main(string[] args)
      {
         var x = new int[] { 75824 };
         x.QuickSort();
      }
   }
}

1 Una descripción independiente del lenguaje de QuickSort puede verse aquí, y su implementación utilizando anidamiento aquí.


Referencia musical: Por allá por la segunda mitad de la década de los 70, cuando aún era un adolescente, un artista con el estrambótico nombre de Meat Loaf llamó poderosamente mi atención gracias a un peculiar estilo operístico, que combinaba el pop/rock tradicional con elementos de rock sinfónico. Con el tiempo, el disco “Bat Out of Hell” (1977) ha llegado a convertirse en uno de los diez más vendidos de todos los tiempos, y ha dado lugar a dos secuelas (también bastante exitosas) en 1993 y 2006. El tema “Alive” pertenece al tercer disco de la trilogía.

Octavio Hernandez

Desarrollador y consultor en tecnologías .NET. Microsoft C# MVP entre 2004 y 2010.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *