Este artículo proviene de un motor de traducción automática.

El trabajo programador

.NET multiparadigmático, parte 9: programación funcional

Ted Neward

Ted NewardCualquier momento una serie de artículos se obtiene cerca a dos dígitos, una de dos cosas sucede: ya sea el autor es bastante pretencioso pensar que sus lectores están realmente interesados en este tema que muchas veces en una fila, o es demasiado boneheaded a pensar para llegar a un nuevo tema. O bien, supongo, a veces el tema sólo merece mucho cobertura. Independientemente de que es el caso aquí, estar a seguros de que estamos ahora en la recta final.

En la pieza anterior en el número de junio (msdn.microsoft.com/magazine/hh205754), la idea de proporcionar variabilidad a lo largo de un eje de nombre surgió bajo el microscopio, utilizando las convenciones de nomenclatura y programación dinámica: vinculante, es decir, por su nombre, que en.NET normalmente significa reflexión a cierto nivel, para resolver algunos problemas de diseño interesante. Mayoría.Los desarrolladores netos, imagino, esperan que la mayor parte de la programación dinámica que se encuentran a través de la palabra clave "dinámica" que proporciona C# 4. Como los desarrolladores de Visual Basic de mano viejo saben, sin embargo, C# sólo vino por su dinamismo recientemente, mientras que los programadores de Visual Basic lo han conocido — y, en muchos casos utilizan con bastante éxito — durante décadas.

Pero ese no es el último de los paradigmas: uno más sigue a explorar, y, una vez más, es uno que ha estado ocultando a la vista normal hace unos años. Y es ciertamente fácil (si un poco impertinente) describir el diseño funcional como diseño del eje algorítmico de variabilidad de características comunes, al mismo tiempo esto tal y oculta sus capacidades.

En una única frase, programación funcional es acerca de las funciones de tratamiento como valores, al igual que cualquier otro valor tipo de datos, lo que significa que podemos pasar funciones todo igual que nosotros los valores de datos, así como obtener nuevos valores de esos valores. O, dicho con más precisión, funciones deberían ser tratadas como ciudadanos de primera clase en el idioma: pueden crear, pasa a los métodos y regresó de métodos, así como otros valores son. Pero esa explicación, una vez más, no precisamente ilumine, así que vamos a comenzar con un simple caso de estudio.

Imaginar es el ejercicio de diseño crear una pequeña calculadora de línea de comandos: un usuario tipos (o tubos) una expresión matemática en y la calculadora lo analiza e imprime el resultado. Este diseño es bastante sencillo, como se muestra en figura 1.

Figura 1 una calculadora Simple

class Program
{
  static void Main(string[] args)
  {
    if (args.Length < 3)
        throw new Exception("Must have at least three command-line arguments");

    int lhs = Int32.Parse(args[0]);
    string op = args[1];
    int rhs = Int32.Parse(args[2]);
    switch (op)
    {
      case "+": Console.WriteLine("{0}", lhs + rhs); break;
      case "-": Console.WriteLine("{0}", lhs - rhs); break;
      case "*": Console.WriteLine("{0}", lhs * rhs); break;
      case "/": Console.WriteLine("{0}", lhs / rhs); break;
      default:
        throw new Exception(String.Format("Unrecognized operator: {0}", op));
    }
  }
}

Como está escrito, funciona, hasta que la calculadora recibe algo más que el cardenales cuatro operadores. Lo que es peor, sin embargo, es que una cantidad importante de código (en comparación con el tamaño total del programa) es código duplicado y seguirá siendo el código duplicado como agregamos nuevas operaciones matemáticas al sistema (como el módulo operador, % o el operador exponencial, ^).

Paso atrás por un momento, ha claro que la operación real — lo que se está haciendo a los dos números: es lo que varía aquí, y sería bueno poder escribir esto en un formato más genérico, como se muestra en figura 2.

Figura 2 una calculadora más genérica

class Program
  {
    static void Main(string[] args)
    {
      if (args.Length < 3)
          throw new Exception("Must have at least three command-line arguments");

      int lhs = Int32.Parse(args[0]);
      string op = args[1];
      int rhs = Int32.Parse(args[2]);
      Console.WriteLine("{0}", Operate(lhs, op, rhs));
    }
    static int Operate(int lhs, string op, int rhs)
    {
      // ...
}
  }

Obviamente, nosotros podríamos recrear simplemente el bloque switch/case en funcionamiento, pero realmente no ganar mucho. Idealmente, nos gustaría que algún tipo de operación de cadena de búsqueda (que, en la superficie, es una forma de dinámica programación una vez más, el nombre de enlace "+" para una operación aditiva, por ejemplo).

Dentro del mundo de patrones de diseño, esto sería un caso para el patrón de estrategia, donde subclases concretas implementan una clase base o interfaz, de la firma necesaria y typechecking de tiempo de compilación para seguridad, algo a lo largo de las líneas de:

interface ICalcOp
{
  int Execute(int lhs, int rhs);
}
class AddOp : ICalcOp { int Execute(int lhs, int rhs) { return lhs + rhs; } }

… Que funciona una especie de. Es bastante detallado, que requieren una nueva clase para cada operación que queremos. También no es muy orientado a objetos, ya realmente sólo necesidad de una instancia de ella, nunca, alojado dentro de una tabla de búsqueda para la asociación y ejecución:

private static Dictionary<string, ICalcOp> Operations;
static int Operate(int lhs, string op, int rhs)
{
  ICalcOp oper = Operations[op];
  return oper.Execute(lhs, rhs);
}

De alguna manera se siente como esto se podría simplificar; y, como probablemente ya han dado cuenta de algunos lectores, esto es un problema que ya se ha resuelto una vez antes, salvo en el contexto de las devoluciones de llamada de controlador de eventos. Esto es exactamente lo que el delegado construcción fue creado para en C#:

delegate int CalcOp(int lhs, int rhs);
static Dictionary<string, CalcOp> Operations = 
  new Dictionary<string, CalcOp>();
static int Operate(int lhs, string op, int rhs)
{
  CalcOp oper = Operations[op];
  return oper(lhs, rhs);
}

Y, por supuesto, las operaciones debe inicializarse correctamente con las operaciones que reconoce la calculadora, pero agregar nuevos se hace un poco más fácil:

static Program()
{
  Operations["+"] = delegate(int lhs, int rhs) { return lhs + rhs; }
}

Programadores de C# 3 Savvy inmediatamente reconoce que esto puede acortar aún más, utilizando expresiones lambda, que se introdujeron en la versión del lenguaje. Visual Basic puede, en Visual Studio 2010, hacer algo similar:

static Program()
{
  Operations["+"] = (int lhs, int rhs) => lhs + rhs;
}

Esto es donde detienen ideas mayoría C# y Visual Basic desarrolladores acerca de delegados y lambdas. Pero lambdas y delegados son mucho más interesantes, sobre todo cuando empezamos a ampliar aún más la idea. Y esta idea, de paso funciones alrededor y su uso en diversas formas, es más profunda.

Reduce, mapas y se repliega: Oh mi!

Pasando funciones alrededor no es algo que estamos acostumbrados en la corriente principal.Desarrollo de neto, por lo que es necesario un ejemplo más concreto de cómo esto puede beneficiar a diseño.

Supongamos por un momento que tenemos una colección de objetos de la persona, como se muestra en figura 3.

Figura 3 una colección de objetos de la persona

class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public int Age { get; set; }
}

class Program
{
  static void Main(string[] args)
  {
    List<Person> people = new List<Person>()
    {
      new Person() { FirstName = "Ted", LastName = "Neward", Age = 40 },
      new Person() { FirstName = "Charlotte", LastName = "Neward", Age = 39 },
      new Person() { FirstName = "Michael", LastName = "Neward", Age = 17 },
      new Person() { FirstName = "Matthew", LastName = "Neward", Age = 11 },
      new Person() { FirstName = "Neal", LastName = "Ford", Age = 43 },
      new Person() { FirstName = "Candy", LastName = "Ford", Age = 39 }
    };
  }
}

Ahora, resulta que la administración quiere celebrar algo (tal vez que todos los hecho de cuota). Lo que quieren hacer es dar a cada una de estas personas una cerveza, que muy fácilmente se consigue mediante el bucle foreach tradicionales, como se muestra en figura 4.

Figura 4 la tradicional foreach Loop

static void Main(string[] args)
{
  List<Person> people = new List<Person>()
  {
    new Person() { FirstName = "Ted", LastName = "Neward", Age = 40 },
    new Person() { FirstName = "Charlotte", LastName = "Neward", Age = 39 },
    new Person() { FirstName = "Michael", LastName = "Neward", Age = 17 },
    new Person() { FirstName = "Matthew", LastName = "Neward", Age = 11 },
    new Person() { FirstName = "Neal", LastName = "Ford", Age = 43 },
    new Person() { FirstName = "Candy", LastName = "Ford", Age = 39 }
  };
  foreach (var p in people)
    Console.WriteLine("Have a beer, {0}!", p.FirstName);
}

¿Hay algunos errores menores aquí (en su mayoría en que su código es entregar una cerveza a mi hijo de 11 años de edad), pero el mayor problema con este código? Es intrínsecamente un-reusable. Los intentos de dar a todos más tarde otra cerveza requieren otro bucle foreach, que viola el principio de Don't repita usted mismo (seco). Por supuesto, reunimos podríamos, el código de expedición de cerveza a un método (respuesta clásica uniformidad procesal), de este modo:

static void GiveBeer(List<Person> people)
{
  foreach (var p in people)
    if (p.Age >= 21)
        Console.WriteLine("Have a beer, {0}!", p.FirstName);
}

(Observe que agregué la verificación de más de 21 años; mi esposa, Charlotte, insistió que incluirlo antes este artículo podría ir a la publicación.) Pero ¿qué pasa si el deseo es encontrar a todo el mundo que es más de 16 años y darles un boleto de cine gratis R-rated, en su lugar? ¿O para buscar en todo el mundo que es más de 39 años y darles un "Santo vaca le está viejo!" globo? ¿O encontrar a todo el mundo más de 65 años y dar a cada uno de ellos una pequeña libreta para anotar las cosas que son probables olvidar (como su nombre, edad, dirección...)? ¿O encontrar a todo el mundo con el apellido que "Ford" y les invita a una fiesta de Halloween?

Más de estos ejemplos nos toss, más se pone de manifiesto que cada uno de estos casos presenta dos elementos de variabilidad: filtrado de los objetos de la persona y la acción a tomar con cada uno de los objetos de la persona. Dado el poder de delegados (y la acción <T> y predicado <T> en los tipos.NET 2.0), podemos crear factores comunes mientras aún exponiendo la variabilidad necesaria, como se muestra en figura 5.

Figura 5 filtrado de objetos de la persona

static List<T> Filter<T>(List<T> src, Predicate<T> criteria)
{
  List<T> results = new List<T>();
  foreach (var it in src)
    if (criteria(it))
      results.Add(it);
  return results;
}
static void Execute<T>(List<T> src, Action<T> action)
{
  foreach (var it in src)
    action(it);
}
static void GiveBeer(List<Person> people)
{
  var drinkers = Filter(people, (Person p) => p.Age >= 21);
  Execute(drinkers, 
      (Person p) => Console.WriteLine("Have a beer, {0}!", p.FirstName));
}

Una operación más común es "transformar" (o ser más exacta acerca de él, "proyecto") un objeto en otro tipo, como cuando queremos extraer los últimos nombres de la lista de objetos de la persona en una lista de cadenas (véase figura 6).

Figura 6 la conversión de una lista de objetos a una lista de cadenas

public delegate T2 TransformProc<T1,T2>(T1 obj);
static List<T2> Transform<T1, T2>(List<T1> src, 
  TransformProc<T1, T2> transformer)
{
  List<T2> results = new List<T2>();
  foreach (var it in src)
    results.Add(transformer(it));
  return results;
}
static void Main(string[] args)
{
  List<Person> people = // ...
List<string> lastnames = Transform(people, (Person p) => p.LastName);
  Execute(lastnames, (s) => Console.WriteLine("Hey, we found a {0}!", s);
}

Observe que, gracias a la utilización de medicamentos genéricos en las declaraciones de filtro, Execute y transformación (más comunes/variabilidad!), podemos reutilizar Execute para mostrar cada uno de los últimos nombres encontrados. Aviso, también cómo el uso de las expresiones lambda hacer una implicación interesante comienzan a llegar claros: uno que se hace aún más evidente cuando escribimos otra operación funcional común, reducir, que "cae" una colección abajo en un único valor mediante la combinación de todos los valores juntos de una manera especificada. Por ejemplo, podríamos añadir hasta la edad de todo el mundo para recuperar un valor de suma-de-todas las edades mediante un bucle foreach, así:

int seed = 0;
foreach (var p in people)
  seed = seed + p.Age;
Console.WriteLine("Total sum of everybody's ages is {0}", seed);

O podemos escribir utilizando un genérico reducir, como se muestra en figura 7.

Figura 7 reducir el uso de un genérico

public delegate T2 Reducer<T1,T2>(T2 accumulator, T1 obj);
static T2 Reduce<T1,T2>(T2 seed, List<T1> src, Reducer<T1,T2> reducer)
{
  foreach (var it in src)
    seed = reducer(seed, it);
  return seed;
}
static void Main(string[] args)
{
  List<Person> people = // ...
Console.WriteLine("Total sum of everybody's ages is {0}", 
    Reduce(0, people, (int current, Person p) => current + p.Age));
}

Esta operación de reducción es conocida como un "redil", por cierto. (Para el programador funcional más exigente, los dos términos son ligeramente diferentes, pero la diferencia no es crítica para el debate principal.) Y, sí, si había comenzado a sospechar que estas operaciones realmente no eran más que lo que proporciona LINQ para objetos (la función de LINQ para objetos que tiene tan poco amor cuando fue lanzado), sería inabarcable (véase figura 8).

Figura 8 doblar las operaciones

static void Main(string[] args)
{
  List<Person> people = new List<Person>()
  {
    new Person() { FirstName = "Ted", LastName = "Neward", Age = 40 },
    new Person() { FirstName = "Charlotte", LastName = "Neward", Age = 39 },
    new Person() { FirstName = "Michael", LastName = "Neward", Age = 17 },
    new Person() { FirstName = "Matthew", LastName = "Neward", Age = 11 },
    new Person() { FirstName = "Neal", LastName = "Ford", Age = 43 },
    new Person() { FirstName = "Candy", LastName = "Ford", Age = 39 }
  };
  // Filter and hand out beer:
  foreach (var p in people.Where((Person p) => p.Age >= 21))
    Console.WriteLine("Have a beer, {0}!", p.FirstName);

  // Print out each last name:
  foreach (var s in people.Select((Person p) => p.LastName))
    Console.WriteLine("Hey, we found a {0}!", s);

  // Get the sum of ages:
  Console.WriteLine("Total sum of everybody's ages is {0}", 
    people.Aggregate(0, (int current, Person p) => current + p.Age));
}

La empresa de trabajo.Desarrollador NET, esto parece una tontería. No es como real programadores pasan tiempo buscando formas de reutilizar código de suma de la edad. Reales programadores escriben código que recorre en iteración una colección de objetos, concatenación de cada uno nombre en una representación XML dentro de una cadena, adecuada para su uso en una solicitud de OData o algo:

Console.WriteLine("XML: {0}", people.Aggregate("<people>", 
  (string current, Person p) => 
    current + "<person>" + p.FirstName + "</person>") 
  + "</people>");

Hurras. Supongo que esas cosas LINQ a objeto podrían ser útil después de todo.

¿Más funcional?

Si eres un desarrollador clásica orientada a objetos, esto parece ridículo pero elegante a la vez. Puede ser un momento alucinante, porque este enfoque llega literalmente en diseño de software en un casi diametralmente opuesto de forma de los objetos: en lugar de centrarse en las "cosas" en el sistema y hacer comportamiento algo que depende de cada una de esas cosas, programación funcional busca identificar los "verbos" en el sistema y ver cómo pueden funcionar en diferentes tipos de datos. Ni enfoque es más adecuado que el otro, cada uno captura uniformidad y ofrece variabilidad a lo largo de los ejes muy diferentes, y como puede imaginarse, hay lugares donde cada uno es simple y elegante, y donde cada uno puede ser feo y torpe.

Recuerde, en orientación a objetos clásicos, variabilidad viene a nivel estructural, ofreciendo la posibilidad de crear variabilidad positiva por agregar campos y métodos o reemplazar los métodos existentes (a través de reemplazo), pero nada acerca de la captura de comportamiento algorítmico ad hoc. De hecho, no fue hasta.NET con métodos anónimos que este eje de compatibilidad/variabilidad se hizo factible. Es posible hacer algo como esto en C# 1.0, pero cada lambda tenía que ser un método con nombre declarado en algún lugar y cada método debe escribirse en términos de System.Object (lo que significa la conversión dentro de esos métodos), porque C# 1.0 no tipos parametrizados.

Por mucho tiempo profesionales de idiomas funcionales se cringe por el hecho de que termino el artículo aquí, porque hay muchas otras cosas puede hacer un lenguaje funcional además sólo pase funciones alrededor como valores — aplicación parcial de las funciones son un concepto enorme que hacen gran parte de la programación funcional increíblemente estrecha y elegante, en idiomas que apoyan directamente, pero deberán satisfacer las necesidades de la editoriales, y yo estoy empujando mis limitaciones de longitud como es. Aún así, ver sólo este mucho del enfoque funcional (y armado con las capacidades funcionales ya presentes en LINQ) puede ofrecer algunas potente nueva perspectiva de diseño.

Más importante aún, los desarrolladores interesados en ver más de esto deberían echar un vistazo largo y duro en F #, que, de todos los.NET idiomas, es el único que captura estos conceptos funcionales (aplicación parcial y currificar) como ciudadanos de primera clase dentro de la lengua. Los desarrolladores de C# y Visual Basic pueden hacer cosas similares, pero requieren una ayuda de biblioteca (nuevos tipos y métodos para hacer lo que F # hace naturalmente). Afortunadamente, varios de esos esfuerzos están en marcha, incluyendo la "funcional C#" biblioteca, disponible en CodePlex (functionalcsharp.codeplex.com).

Varios paradigmas

Guste o no, multiparadigm idiomas son de uso común, y parecen que están aquí para quedarse. Los lenguajes de Visual Studio 2010 presentan algún grado de cada uno de los paradigmas distintos; C++, por ejemplo, dispone de unas instalaciones de metaprogramación paramétricas que no son posibles en código administrado, debido a la forma en que opera el compilador de C++, y recientemente (en el más reciente C ++ 0 x estándar) ganó expresiones lambda. Incluso el tan denostada ECMAScript/JavaScript/lenguaje JScript puede hacer objetos, procedimientos, paradigmas metaprogramación, dinámicos y funcionales; de hecho, gran parte de JQuery está construido sobre estas ideas.

¡Feliz codificación!

Ted Neward es un principal con Neward & Associates, una firma independiente especializada en la empresa.Sistemas de plataforma NET Framework y Java. Ha escrito más de 100 artículos, es un orador C# MVP y INETA y ha autor o coautor de una docena de libros, incluyendo "Profesional F # 2.0" (Wrox, 2010). Se consulta y mentores regularmente: llegar a él en ted@tedneward.com si está interesado en lo que trabajan con su equipo, o lee su blog en blogs.tedneward.com.

Gracias a los siguiente experto técnico para revisar este artículo: Podwysocki Mateo