Pontos de dados

Dados, conheçam meu novo amigo, F#

Julie Lerman

Baixar o código de exemplo

Julie LermanNos últimos anos, tive certa exposição à programação funcional. Parte dela é implícita: a codificação com lambdas em LINQ é programação funcional. Parte é explícita: usar o Entity Framework e o LINQ para a SQL tipo CompiledQuery me obrigou a usar uma lógica funcional do .NET. Isso sempre foi um pouco complicado, porque eu fazia isso muito raramente. Também estive exposta à programação funcional pelo entusiasmo contagiante de Rachel Reese, MVP da Microsoft, que não só faz parte do meu grupo de usuários locais (VTdotNET), mas também administra o grupo VTFun aqui em Vermont, que foca em muitos aspectos e linguagens de programação funcional. Na primeira vez que fui a uma reunião do VTFun, ela estava cheia de matemáticos e cientistas do espaço. Não estou brincando. Um dos frequentadores é um teórico de matéria condensada na Universidade de Vermont. É muita emoção! Eu fiquei um pouco perdida com as conversas de alto nível, mas foi divertido realmente se sentir a caloura da sala. Grande parte do que foi dito estava além da minha compreensão, exceto uma única instrução que me chamou a atenção: “As linguagens funcionais não precisamos de nenhuma porcaria de loops foreach”. Espere aí! Eu queria saber o que isso significava. Por que eles não precisam de loops foreach?

O que eu ouvia normalmente é que a programação funcional é ótima para operações matemáticas. Não sendo um gênio da matemática, interpretei isso como "para demonstrações que usam a sequência de Fibonacci para testar o comportamento assíncrono" e não prestei muito mais atenção. Esse é o problema em ouvir apenas uma curta parte do argumento sobre programação funcional.

Mas eu finalmente comecei a ouvir uma caracterização mais precisa: que a programação funcional é ótima para a ciência de dados. Isso certamente atrai um nerd dos dados. F#, a linguagem funcional do Microsoft .NET Framework, traz todos os tipos de recursos de ciência dados para os desenvolvedores do .NET Ela tem bibliotecas inteiras dedicadas à criação de gráficos, manipulação de tempo e operações definidas. Possui APIs com lógica dedicada a matrizes 2D, 3D e 4D. Ela compreende unidades de medida e é capaz de restringir e validar com base em unidades específicas.

A F# também permite cenários de codificação interessantes no Visual Studio. Em vez de construir sua lógica em arquivos de código e depois depurar, você pode escrever e executar o código linha por linha em uma janela interativa e mover o código bem-sucedido para um arquivo de classe. Você pode ter uma boa noção do F# como linguagem lendo o artigo de Tomas Petricek, “Understanding the World with F#” (bit.ly/1cx3cGx). Adicionando F# no conjunto de ferramentas .NET, o Visual Studio torna-se uma ferramenta poderosa para a criação de aplicativos que executam a lógica da ciência de dados.

Neste artigo, vou me concentrar em um aspecto da F# e da programação funcional que passei a entender desde que ouvi comentários sobre não precisar de loops foreach. As linguagens funcionais são realmente boas para trabalhar com conjuntos de dados. Em uma linguagem de procedimento, quando você trabalha com conjuntos, você deve iterar explicitamente por eles para executar a lógica. Uma linguagem funcional, por outro lado, compreende conjuntos em um nível diferente, então, você só precisa pedir para executar uma função em um conjunto, em vez de percorrer o conjunto e executar uma função em cada item. E essa função pode ser definida com muita lógica, incluindo matemática, se necessário.

LINQ oferece atalhos para isso, até mesmo um método ForEach para o qual você pode passar uma função. Mas, no segundo plano, sua linguagem (talvez C# ou Visual Basic) simplesmente converte isso em loop.

Uma linguagem funcional como F# tem a capacidade de executar a função set em um nível inferior, e é muito mais rápida graças ao seu processamento paralelo fácil. Adicione a isso outros benefícios importantes, como uma ótima capacidade de processamento matemático e um sistema de tipos incrivelmente detalhado que até entende as unidades de medida, e você tem uma ferramenta poderosa para fazer cálculos em grandes conjuntos de dados.

Há muito mais na F# e outras linguagens funcionais que ainda está muito além do meu alcance. Mas o que quero fazer nesta coluna é focar em uma maneira de se beneficiar rapidamente de um aspecto particular das linguagens funcionais sem fazer um grande investimento: mover a lógica do banco de dados para meu aplicativo. Esta é a maneira que eu gosto de aprender: encontrar algumas coisas que eu possa entender e usá-las para dar passos lentos rumo a uma nova plataforma, linguagem, estrutura ou outro tipo de ferramenta.

Ouvi Reese tentar deixar claro para os desenvolvedores que usar F# não significa mudar de linguagem de desenvolvimento. Da mesma forma que você pode usar uma consulta LINQ ou um procedimento armazenado para resolver um problema particular, você pode criar uma biblioteca de métodos F# para resolver os tipos de problemas em seu aplicativo em que linguagens funcionais são realmente boas.

Meu foco aqui é a extração de lógica de negócios, que foi construída em meu banco de dados, a lógica para lidar com grandes conjuntos de dados – algo em que o banco de dados é excelente – e substituí-la por métodos funcionais.

E como a F# é projetada para trabalhar com conjuntos e é realmente inteligente com funções matemáticas, o código pode ser mais eficiente do que poderia ser em SQL ou em linguagens de procedimento, como C# ou Visual Basic. É muito fácil fazer F# executar a lógica nos itens do conjunto em paralelo. Não só isso pode reduzir a quantidade de código que você provavelmente precisaria em uma linguagem de procedimento para emular esse comportamento, a paralelização significa que o código será executado muito mais rapidamente. Você poderia projetar seu código C# para executar em paralelo, mas prefiro não passar por esse esforço também.

Um problema real

Muitos anos atrás, escrevi um aplicativo no Visual Basic 5 que tinha de coletar, manter e relatar muitos dados científicos e executar uma série de cálculos. Alguns desses cálculos eram tão complexos que os mandava para uma API do Excel.

Um dos cálculos envolvia determinar as libras por polegada quadrada (PSI) com base na quantidade de peso que causava a quebra de um bloco de materiais. O bloco poderia ser de qualquer forma e tamanho cilíndrico. O aplicativo usaria as medidas do cilindro e, dependendo de sua forma e tamanho, uma fórmula específica para calculara sua área. Depois, ele aplicaria um fator de tolerância correspondente e, por fim, a quantidade de peso que levou à quebra do cilindro. Tudo isso junto gerou a medida de PSI para o material em particular que estava sendo testado.

Em 1997, usar a API do Excel para avaliar a fórmula dentro do Visual Basic 5 e do Visual Basic 6 parecia uma solução muito inteligente.

Movendo a avaliação

Anos mais tarde, renovei o aplicativo no .NET. Naquela época, decidi aproveitar o poder do SQL Server para executar o cálculo de PSI em grandes conjuntos de cilindros depois de serem atualizados por um usuário, em vez fazer o computador o usuário gastar tempo em todos esses cálculos. Isso funcionou muito bem.

Mais anos se passaram e minhas ideias sobre a lógica de negócios no banco de dados mudaram. Eu queria retornar esse cálculo para o lado do cliente e, claro, os computadores clientes já estavam mais rápidos nessa época. Não foi muito difícil para reescrever a lógica em C#. Depois que o usuário atualizava uma série de cilindros com o peso que ocasiona a quebra deles (a carga, representada em libras), o aplicativo iterava os cilindros atualizados e calculava o PSI. Então, eu poderia atualizar os cilindros do banco de dados com as novas cargas e valores de PSI.

Por uma questão de comparação entre o C# conhecido e o resultado final em F# (que você verá em breve), forneci a lista dos tipos de cilindros, CylinderMeasurements, na Figura 1 minha classe C# Calculator na Figura 2, para que você possa ver como eu obtive os PSIs para um conjunto de cilindros. É o método CylinderCalculator.UpdateCylinders que é chamado para iniciar o cálculo de PSI para um conjunto de cilindros. Ele itera cada cilindro no conjunto e executa os cálculos apropriados. Observe que um dos métodos, GetAreaForCalculation, depende do tipo de cilindro, porque eu calculo a área do cilindro usando a fórmula apropriada.

Figura 1 Classe CylinderMeasurement

public class CylinderMeasurement
{
  public CylinderMeasurement(double widthA, double widthB,
    double height)
  {
    WidthA = widthA;
    WidthB = widthB;
    Height = height;
  }
  public int Id { get; private set; }
  public double Height { get; private set; }
  public double WidthB { get; private set; }
  public double WidthA { get; private set; }
  public int LoadPounds { get; private set; }
  public double Psi { get; set; }
  public CylinderType CylinderType { get; set; }
  public void UpdateLoadPoundsAndTypeEnum(int load, 
    CylinderType cylType) {
    LoadPounds = load; CylinderType = cylType;
  }
   private double? Ratio {
    get {
      if (Height > 0 && WidthA + WidthB > 0) {
        return Math.Round(Height / ((WidthA + WidthB) / 2), 2);
      }
      return null;
    }
  }
  public double ToleranceFactor {
    get {
      if (Ratio > 1.94 || Ratio < 1) {
        return 1;
      }
      return .979;
    }
  }
}

Figura 2 Classe Calculator para calcular o PSI

public static class CylinderCalculator
  {
    private static CylinderMeasurement _currentCyl;
    public static void UpdateCylinders(IEnumerable<CylinderMeasurement> cyls) {
      foreach (var cyl in cyls)
      {
        _currentCyl = cyl;
        cyl.Psi = GetPsi();
      }
    }
    private static double GetPsi() {
      var area = GetAreaForCylinder();
      return PsiCalculator(area);
    }
    private static double GetAreaForCylinder() {
      switch (_currentCyl.CylinderType)
      {
        case CylinderType.FourFourEightCylinder:
          return 3.14159*((_currentCyl.WidthA + _currentCyl.WidthB)/2)/2*
            ((_currentCyl.WidthA + _currentCyl.WidthB)/2/2);
        case CylinderType.SixSixTwelveCylinder:
          return 3.14159*((_currentCyl.WidthA + _currentCyl.WidthB)/2)/2*
            ((_currentCyl.WidthA + _currentCyl.WidthB)/2/2);
        case CylinderType.ThreeThreeSixCylinder:
          return _currentCyl.WidthA*_currentCyl.WidthB;
        case CylinderType.TwoTwoTwoCylinder:
          return ((_currentCyl.WidthA + _currentCyl.WidthB)/2)*
            ((_currentCyl.WidthA + _currentCyl.WidthB)/2);
        default:
          throw new ArgumentOutOfRangeException();
      }
    }
    private static int PsiCalculator(double area) {
      if (_currentCyl.LoadPounds > 0 && area > 0)
      {
        return (int) (Math.Round(_currentCyl.LoadPounds/area/1000*
          _currentCyl.ToleranceFactor, 2)*1000);
      }
      return 0;
    }
  }

Foco nos dados e processamento mais rápido com o F#

Finalmente, descobri que F#, graças à sua inclinação natural para a manipulação de dados, fornece uma solução muito melhor do que avaliar uma fórmula de cada vez.

Na sessão de introdução sobre F# dada por Reese, expliquei esse problema, que me incomodou por tantos anos, e perguntei se uma linguagem funcional poderia ser usada para resolvê-lo de uma forma mais satisfatória. Ela confirmou que eu poderia aplicar minha lógica de cálculo completa em um conjunto completo e deixar que F# obtenha os PSIs para muitos cilindros em paralelo. Eu poderia obter a funcionalidade por parte do cliente e um aumento de desempenho ao mesmo tempo.

O segredo para mim foi perceber que eu poderia usar F# para resolver um problema específico da mesma maneira que eu uso um procedimento armazenado – é simplesmente mais uma ferramenta no meu cinturão. Não é preciso que eu desista do meu investimento em C#. Talvez haja alguns que sejam simplesmente o inverso – escrever a maior parte de seus aplicativos em F# e usar C# para atacar problemas específicos. Em todo caso, usando o C# CylinderCalculator como guia, Reese criou um pequeno projeto em F# que cumpriu a tarefa e eu consegui substituir uma chamada para minha calculadora por uma chamada para a dela em meus testes, como mostrado na Figura 3.

Figura 3 A calculadora de PSI em F#

module calcPsi =
  let fourFourEightFormula WA WB = 3.14159*((WA+WB)/2.)/2.*((WA+WB)/2./2.)
  let sixSixTwelveFormula WA WB = 3.14159*((WA+WB)/2.)/2.*((WA+WB)/2./2.)
  let threeThreeSixFormula (WA:float) (WB:float) = WA*WB
  let twoTwoTwoFormula WA WB = ((WA+WB)/2.)*((WA+WB)/2.)
  // Ratio function
  let ratioFormula height widthA widthB =
    if (height > 0. && (widthA + widthB > 0.)) then
      Some(Math.Round(height / ((widthA + widthB)/2.), 2))
    else
      None
  // Tolerance function
  let tolerance (ratioValue:float option) = match ratioValue with
    | _ when (ratioValue.IsSome && ratioValue.Value > 1.94) -> 1.
    | _ when (ratioValue.IsSome && ratioValue.Value < 1.) -> 1.
    | _ -> 0.979
  // Update the PSI, and return the original cylinder information.
  let calculatePsi (cyl:CylinderMeasurement) =
    let formula = match cyl.CylinderType with
      | CylinderType.FourFourEightCylinder -> fourFourEightFormula
      | CylinderType.SixSixTwelveCylinder -> sixSixTwelveFormula
      | CylinderType.ThreeThreeSixCylinder -> threeThreeSixFormula
      | CylinderType.TwoTwoTwoCylinder -> twoTwoTwoFormula
      | _ -> failwith "Unknown cylinder"
    let basearea = formula cyl.WidthA cyl.WidthB
    let ratio = ratioFormula cyl.Height cylinder.WidthA cyl.WidthB
    let tFactor = tolerance ratio
    let PSI = Math.Round((float)cyl.LoadPounds/basearea/1000. * tFactor, 2)*1000.
    cyl.Psi <- PSI
    cyl
  // Map evaluate to handle all given cylinders.
  let getPsi (cylinders:CylinderMeasurement[])
              = Array.Parallel.map calculatePsi cylinders

Se, como eu, você é novo em F#, pode olhar apenas para a quantidade de código e não ver sentido em escolher essa opção em vez da linguagem C#. Após um exame mais profundo, no entanto, você pode apreciar a concisão da linguagem, a capacidade de definir as fórmulas de maneira mais elegante e, no final, a maneira simples de poder aplicar a função calculatePsi que Reese definiu à matriz de cilindros que passei para o método.

A concisão é graças ao fato de que F# é mais bem projetada para executar funções matemáticas do que C#, por isso, a definição dessas funções é mais eficiente. Mas, além do apelo aos nerds da linguagem, eu estava interessada em desempenho. Quando eu aumentei o número de cilindros por conjunto em meus testes, inicialmente não vi uma melhoria de desempenho em relação ao C#. Reese explicou que o ambiente de teste é mais caro quando se usa F#. Então, eu testei o desempenho de um aplicativo de console usando o cronômetro para relatar o tempo decorrido. O aplicativo criou uma lista de 50.000 cilindros, iniciou o cronômetro, passou os cilindros para a calculadora em C# ou F# para atualizar o valor de PSI para cada cilindro, depois parou o cronômetro quando os cálculos estavam concluídos.

Na maioria dos casos, o processo em C# demorou cerca de três vezes mais do que o processo em F#, embora em cerca de 20% do tempo C# venceria F# por uma pequena margem. Não consigo explicar esse fato estranho, mas é possível que eu precise saber mais para traçar um perfil mais fiel.

De olho na lógica que implora por uma linguagem funcional

Então, embora eu tenha de aprimorar minhas habilidades em F#, meu novo conhecimento se aplicará muito bem aos aplicativos que já tenho em produção, bem como a aplicativos futuros. Em meus aplicativos de produção, posso olhar para a lógica de negócios que eu exilei no banco de dados e considerar se um aplicativo se beneficiaria de uma substituição em F#. Com novos aplicativos, agora tenho um olhar mais aguçado para detectar a funcionalidade que posso codificar de forma mais eficiente com F#, manipular dados, usar unidades de medida fortemente tipadas e ganhar desempenho. E há sempre a diversão de aprender uma nova linguagem e encontrar os cenários certos para os quais ela criada!

Julie Lerman é MVP da Microsoft, mentora e consultora do .NET, que reside nas colinas de Vermont. Você pode encontrá-la fazendo apresentações sobre acesso a dados e outros tópicos do Microsoft .NET em grupos de usuários e conferências em todo o mundo. Seu blog está em thedatafarm.com/blog e é autora do livro “Programming Entity Framework” (2010), além das edições Code First (2011) e DbContext (2012), todos da O’Reilly Media. Siga Julie no Twitter em twitter.com/julielerman e confira seus cursos da Pluralsight em juliel.me/PS-Videos.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Rachel Reese (Firefly Logic)
Rachel Reese é uma engenheira de software de longa data e gênio da matemática em Nashville, TN. Até pouco tempo, ela gerenciava o grupo de usuários de programação funcional VT de Burlington, @VTFun, que era uma fonte constante de inspiração para ela, e no qual ela falava frequentemente sobre o F#. Ela também é ASPInsider, MVP em F#, entusiasta da comunidade, uma das fundadoras do @lambdaladies e Rachii. Você pode encontrá-la no twitter, @rachelreese ou no próprio blog: rachelree.se.