Compartilhar via


Este artigo foi traduzido por máquina.

O trabalho programador

.NET com vários paradigmas, Parte 9: programação funcional

Ted Neward

Ted NewardQualquer momento uma série de artigos estiver perto de dois dígitos, uma das duas coisas está acontecendo: quer o autor é suficientemente pretensioso pensar que seus leitores estão realmente interessados no assunto que muitas vezes em uma linha, ou ele é muito boneheaded a pensar em vir acima com um novo tópico. Ou, eu suponho que, às vezes o assunto só merece muita cobertura. Não importa qual é o caso aqui, fique tranqüilo que agora estamos na reta final.

A peça anterior na edição de Junho (msdn.microsoft.com/magazine/hh205754), a idéia de fornecer variabilidade ao longo de um eixo baseado no nome veio sob o microscópio, usando as convenções de nomenclatura e programação dinâmica — ou seja, ligação por nome, que em.NET normalmente significa reflexão em algum nível — para resolver alguns problemas de design interessante. Maioria.Desenvolvedores líquidos, imagino, esperam que a maioria da programação dinâmica encontram será através da palavra-chave "dinâmico" que fornece C# 4. Como os desenvolvedores de Visual Basic velho-mão sabem, no entanto, c# veio somente pelo seu dinamismo recentemente, Considerando que os programadores de Visual Basic tem sabido — e usado, em muitos casos bastante sucesso — há décadas.

Mas isso não é o último dos paradigmas — um permanece mais para ser explorado, e, novamente, é aquele que tem sido escondido na vista lisa por alguns anos agora. E enquanto é certamente fácil (se um pouco atrevido) descrever design funcional como design ao longo do eixo algorítmico da variabilidade de uso comum, isso simultaneamente simplificava e obscurece suas capacidades.

Em uma única frase, programação funcional é sobre funções de tratamento como valores, assim como qualquer outro valor tipo de dados, ou seja, nós podemos passar funções em torno de como nós podemos valores de dados, bem como derivar novos valores desses valores. Ou, colocar com mais precisão, funções devem ser tratadas como cidadãos de primeira classe dentro da língua: eles podem ser criados, passados para métodos e retornados de métodos, assim como outros valores são. Mas essa explicação, novamente, não precisamente iluminar, então vamos começar com um simple estudo de caso.

Imagine o exercício do projeto é criar uma pequena calculadora de linha de comando: um usuário digita (ou canos) uma expressão matemática em, e a calculadora analisa-lo e imprime o resultado. Projetar isso é bastante simples, como mostrado na Figura 1.

Figura 1 uma 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 escrito, ele funciona — até que a calculadora recebe algo do que o cardeais quatro operadores. O que é pior, porém, é que uma quantidade significativa de código (em comparação com o tamanho total do programa) é código duplicado e continuará a ser código duplicado como podemos adicionar novas operações matemáticas para o sistema (tais como o módulo operador, % ou o operador de expoente, ^).

Pisando volta por um momento, é claro que a operação real — o que está sendo feito para os dois números — é o que varia aqui, e que seria bom ser capaz de reescrever esta em um formato mais genérico, como mostrado na Figura 2.

Figura 2 uma calculadora mais 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, nós simplesmente poderia recriar o bloco switch/case em operar, mas que realmente não ganhar muito. Idealmente, nós gostaríamos de algum tipo de operação de Cadeia de caracteres de pesquisa (que, na superfície, é uma forma de dinâmica programação novamente, vinculando o nome "+" para uma operação aditiva, por exemplo).

Dentro do mundo de padrões de design, este seria um caso para o padrão de estratégia, onde subclasses concretas implementam uma classe base ou interface, fornecendo a assinatura necessária e typechecking de tempo de compilação para segurança, algo nos moldes de:

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

Que funciona.. … sort of. É bastante detalhado, que exigem uma nova classe a ser criada para cada operação que queremos. Também não é muito orientada a objeto, porque nós realmente apenas necessidade uma instância dele, nunca, hospedado dentro de uma tabela de pesquisa para correspondência e execução:

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 alguma forma parece que isso poderia ser simplificado; e, como alguns leitores provavelmente já perceberam, este é um problema que já tenha sido resolvido uma vez antes, exceto no contexto de retornos de chamada do manipulador de eventos. Isso é exatamente o que o delegado construção foi criada para em 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);
}

E, claro, operações tem de ser inicializada corretamente com as operações que a calculadora reconhece, mas adicionar novos torna-se um pouco mais fácil:

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

Experientes programadores de c# 3 serão imediatamente reconhece que isso pode ser reduzido ainda mais, usando expressões lambda, que foram introduzidas nessa versão da linguagem. Visual Basic pode, no Visual Studio 2010, fazer algo semelhante:

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

Isto é onde param idéias mais c# e Visual Basic dos desenvolvedores sobre delegados e lambdas. Mas lambdas e delegados são muito mais interessantes, especialmente quando começamos a estender a idéia ainda mais. E esta idéia, de funções de passagem ao redor e usá-las de várias maneiras, é mais profunda.

Reduz, mapas e dobras — Oh meu!

Passar funções em torno não é algo que estamos acostumados a no mainstream.Desenvolvimento líquido, portanto, um exemplo mais concreto de como isso pode beneficiar design é necessário.

Assumir por um momento que temos uma coleção de objetos de pessoa, conforme mostrado na Figura 3.

Figura 3 uma coleção de objetos de pessoa

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 }
    };
  }
}

Agora, acontece que a gerência deseja celebrar algo (talvez eles todos feitos cota). O que eles querem fazer é dar a cada uma dessas pessoas uma cerveja, que é muito facilmente feita usando o loop foreach tradicionais, como mostrado na Figura 4.

Figura 4 o 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);
}

Existem alguns pequenos bugs aqui (principalmente em que seu código está entregando uma cerveja para meu filho de 11 anos de idade), mas o maior problema com este código? É intrinsecamente un-reusable. Tenta mais tarde todos dar outra cerveja requerem outro loop foreach, que viola o princípio de Don't Repeat Yourself (seco). Poderia, naturalmente, recolhemos o código emissão de cerveja em um método (resposta a semelhança processual clássico), da seguinte forma:

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 adicionei a verificação de over-21 anos; minha esposa, Charlotte, insistiu que eu incluí-lo antes deste artigo poderia ir para publicação.) Mas o que acontece se o desejo é encontrar quem é mais de 16 anos e dar-lhes um bilhete grátis filme R-rated, em vez disso? Ou para encontrar todos os que é mais de 39 anos e dar-lhes um "Santo vaca você está velho!" balão? Ou encontrar todos mais de 65 anos e dar a cada um de um notebook pequeno para escrever coisas que eles estão propensos a esquecer (como seu nome, idade, endereço …)? Ou encontre toda a gente com o último nome do que "Ford" e convidá-los para uma festa de Halloween?

Quanto mais desses exemplos podemos atirar fora, quanto mais ele se torna claro que cada um destes casos apresenta dois elementos de variabilidade: filtragem de objetos de pessoa e a ação a ser tomada com cada um desses objetos de pessoa. Dado o poder de delegados (e a ação <T> e predicado <T> tipos de introduzidos no.NET 2.0), podemos criar elementos comuns enquanto ainda expondo a variabilidade necessária, como mostrado na Figura 5.

Figura 5 filtragem pessoa objetos

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));
}

Uma operação mais comum é "transformar" (ou, para ser mais preciso sobre o assunto, "projeto") um objeto em outro tipo, tais como quando queremos extrair os últimos nomes da lista de objetos de pessoa em uma lista de seqüências de caracteres (ver Figura 6).

Figura 6 convertendo a partir de uma lista de objetos para uma lista de seqüências de caracteres

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, graças ao uso de genéricos nas declarações de filtro, Execute e transformação (mais comuns/variabilidade!), pode reutilizar executar para exibir cada um dos sobrenomes encontrados. Aviso, também, como o uso de expressões lambda fazer uma implicação interessante começam a vir claro — um que se torna ainda mais evidente quando escrevemos ainda outra operação funcional comum, reduzir, que "recolhe" uma coleção para baixo em um único valor, combinando todos os valores juntos de uma forma especificada. Por exemplo, poderíamos acrescentar até idade de todos para recuperar um valor de soma de todas as idades usando um loop foreach, da seguinte forma:

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

Ou podemos escrevê-lo usando um reduzir genérico, como mostrado na a Figura 7.

Figura 7 usando um genérico reduzir

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 operação de redução é conhecida como uma "dobra", by the way. (Para o programador funcional mais exigente, os dois termos são ligeiramente diferentes, mas a diferença não é crítica para a discussão principal.) E, sim, se você começou a suspeitar de que essas operações foram realmente nada mais do que o que fornece LINQ para objetos (LINQ para objetos recurso que tem tão pouco amor quando foi originalmente lançado), você seria in loco (ver Figura 8).

Figura 8 Dobre operações

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));
}

Para a empresa de trabalho.NET desenvolvedor, isso parece parvo. Não é como real programadores gastam tempo procurando maneiras de reutilizar código de idade-soma. Reais programadores escrevem código que itera uma coleção de objetos, concatenando cada um nome em uma representação XML dentro de uma Cadeia de caracteres, adequada para uso em uma solicitação de OData ou algo assim:

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

OPA. Acho que coisas LINQ para objetos podem ser útil depois de tudo.

Mais funcional?

Se você é um desenvolvedor orientado a objeto de formação clássica, isso parece ridículo mas elegante ao mesmo tempo. Ele pode ser um momento magnífico, porque esta abordagem literalmente vem em design de software em um quase diametralmente oposto longe de objetos: ao invés de incidindo sobre as "coisas" no sistema e comportamento de fazer algo que acompanha a cada uma dessas coisas, programação funcional olha para identificar os "verbos" no sistema e ver como eles podem operar em diferentes tipos de dados. Nenhuma abordagem é mais certa do que o outro — cada captura aspectos comuns e oferece variabilidade ao longo do eixo muito diferente, e como pode ser imaginado, há lugares onde cada um é elegante e simples, e onde cada um pode ser feio e desajeitado.

Lembre-se, em orientação a objetos clássicos, variabilidade vem a nível estrutural, oferecendo a capacidade de criar variabilidade positiva adicionando campos e métodos ou substituindo os métodos existentes (por meio de substituição), mas nada sobre a captura de comportamento algorítmico ad hoc. Na verdade, não foi até.NET tem métodos anônimos que este eixo de variabilidade/aspectos comuns tornou-se viável. Foi possível fazer algo assim no c# 1.0, mas cada lambda tinha de ser um método nomeado declarado em algum lugar, e cada método tinha que ser digitado em termos de System. Object (que significa baixar dentro desses métodos), como c# 1.0 não têm tipos com parâmetros.

Os profissionais de longa data de linguagens funcionais vão encolher no fato de que eu terminar o artigo aqui, porque existem inúmeras outras coisas uma linguagem funcional pode fazer além de apenas funções de passagem em torno de como valores — aplicação parcial de funções são um conceito enorme que fazer muita programação funcional incrivelmente apertadas e elegante, em linguagens que suportam diretamente — mas satisfazer necessidades editoriais, e eu estou empurrando meus comprimento limitações como ele é. Mesmo assim, vendo muito presente da abordagem funcional (e armado com as capacidades funcionais já presentes no LINQ) pode oferecer algumas dicas de design novo poderoso.

Mais importante ainda, desenvolvedores interessados em ver mais desta devem ter um longo e difícil olhar F #, que, de todos os.NET línguas, é o único que captura estes conceitos funcionais (aplicativo parcial e currying) como cidadãos de primeira classe dentro da língua. Os desenvolvedores de c# e Visual Basic podem fazer coisas semelhantes, mas requerem alguma ajuda de biblioteca (novos tipos e métodos para fazer o F # faz naturalmente). Felizmente, vários desses esforços estão em andamento, incluindo a "funcional" biblioteca de c#, disponível no CodePlex (functionalcsharp.codeplex.com).

Vários paradigmas

Goste ou não, línguas multiparadigm são de uso comum, e eles se parecem com eles estão aqui para ficar. Os idiomas Visual Studio 2010 apresentam algum grau de cada um dos vários paradigmas; C++, por exemplo, tem algumas instalações metaprogramação paramétricas que não são possíveis em código gerenciado, devido à maneira que o compilador C++ Opera, e apenas recentemente (no mais recente C + + 0x padrão) ganhou as expressões lambda. Mesmo muito caluniado ECMAScript/JavaScript/JScript Idioma pode fazer objetos, procedimentos, paradigmas metaprogramação, dinâmicos e funcionais; na verdade, grande parte da JQuery é construída sobre essas idéias.

Boa codificação.

Ted Neward é uma entidade com Neward & Associates, uma empresa independente, especializada na empresa.Sistemas de plataforma NET Framework e Java. Ele escreveu mais de 100 artigos, é um MVP c# e INETA orador e tem o autor ou co-autor de uma dúzia de livros, incluindo "Profissional F # 2.0" (Wrox, 2010). Ele consulta e mentores regularmente — contatá-lo em ted@tedneward.com se você estiver interessado em ter-lhe vir trabalhar com sua equipe, ou ler seu blog em blogs.tedneward.com.

Graças aos seguinte perito técnico para revisão deste artigo: Matthew podwysocki