Setembro de 2019

Volume 34 – Número 9

[F#]

Faça tudo com F# no .NET Core

Por Phillip Carter

F# é a linguagem de programação funcional para .NET (bit.ly/2y4PeQG). Ela é multiplataforma e, como todo o .NET, é software livre (github.com/dotnet/FSharp). Na Microsoft, somos grandes fãs do F# porque ele traz a programação funcional para o .NET. Para aqueles que não estão familiarizados com o conceito, o paradigma de programação funcional é aquele que enfatiza determinadas abordagens:

  • Funções como os constructos primários usados para operar em dados
  • Expressões em vez de instruções
  • Valores imutáveis em vez de variáveis
  • Programação declarativa em vez de programação imperativa

Isso significa que o F# traz alguns ótimos recursos para o .NET:

  • Funções que são de primeira classe (podem ser passadas como valores para outras funções e retornadas dessas funções também como valores)
  • Sintaxe leve que enfatiza expressões e valores, não instruções
  • Imutabilidade interna e tipos não nulos
  • Tipos de dados avançados e técnicas de correspondência de padrões avançadas

Normalmente, o código F# típico acaba tendo a aparência mostrada na Figura 1.

Figura 1 Código F# típico

// Group data with Records
type SuccessfulWithdrawal = {
  Amount: decimal
  Balance: decimal
}
type FailedWithdrawal = {
  Amount: decimal
  Balance: decimal
  IsOverdraft: bool
}
// Use discriminated unions to represent data of 1 or more forms
type WithdrawalResult =
  | Success of SuccessfulWithdrawal
  | InsufficientFunds of FailedWithdrawal
  | CardExpired of System.DateTime
  | UndisclosedFailure
let handleWithdrawal amount =
  // Define an inner function to hide it from callers
  // Returns a WithdrawalResult
  let withdrawMoney amount =
    callDatabaseWith amount // Let's assume this has been implemented :)
  let withdrawalResult = withdrawMoney amount
  // The F# compiler enforces accounting for each case!
  match w with
  | Success s -> printfn "Successfully withdrew %f" s.Amount
  | InsufficientFunds f -> printfn "Failed: balance is %f" f.Balance
  | CardExpired d -> printfn "Failed: card expired on %O" d
  | UndisclosedFailure -> printfn "Failed: unknown :("

Além desses recursos principais, o F# também permite interoperabilidade com todo o .NET e é totalmente compatível com objetos, interfaces e assim por diante. Técnicas de programação em F# mais avançadas envolvem, com frequência, a combinação de recursos OO (orientados a objeto) com código funcional, mas sem sacrificar o paradigma de programação funcional.

Além disso, o F# tem muitos recursos exclusivos que as pessoas adoram, como Expressões de Computação, Unidade de Medida e tipos avançados como Registros e Uniões Discriminadas. Confira a referência de linguagem F# em bit.ly/2JSnipy para obter mais informações. O F# também incentiva um estilo de programação inclinado para segurança e exatidão: Muitos desenvolvedores de F# escolheram a linguagem após uma experiência significativa com outras linguagens que não enfatizam a segurança e a exatidão. Ele influenciou muitos dos trabalhos recentes realizados em C#, como programação assíncrona, tuplas, correspondência de padrões e o ainda não lançado conjunto de recursos de tipos de referência anuláveis.

O F# também tem uma comunidade vibrante que adora extrapolar os limites do .NET e criar componentes incríveis de software livre. A comunidade é altamente inovadora e incrivelmente valiosa para o .NET, pioneira em bibliotecas de interface do usuário, bibliotecas de processamento de dados, metodologias de teste, serviços Web e muito mais para o .NET!

A abundância de recursos e a comunidade vibrante de desenvolvedores entusiastas levaram muitas pessoas a se aprofundarem no F# tanto por diversão quanto para trabalho. O F# impulsiona instituições financeiras em todo o mundo, sistemas de comércio eletrônico, computação científica, organizações que fazem ciência de dados e aprendizado de máquina, consultorias e muito mais. O F# também é muito usado na Microsoft: Como uma das principais linguagens usadas pelo Microsoft Research, ele influenciou e ajudou a capacitar a plataforma de desenvolvimento de Q#, partes do Azure e do Office 365 e até mesmo o compilador de F# e as Ferramentas do Visual Studio para F#! Em suma, ele é usado em todos os lugares.

Interessado? Ótimo! Neste artigo, mostrarei como fazer algo interessante com F# no .NET Core.

Visão geral

Começarei com conceitos básicos de F# no .NET Core e passarei progressivamente para funcionalidades mais avançadas (e interessantes), incluindo como usar a CLI do .NET para criar um aplicativo de console e um projeto de biblioteca em qualquer sistema operacional. Embora seja simples e básico, apenas esse conjunto de ferramentas já pode ser suficiente para desenvolver um aplicativo inteiro quando combinado com um editor como o Visual Studio Code e o plug-in F# oficial, o Ionide (ionide.Io). Você também poderá usar o Vim ou Emacs se isso for mais a sua cara.

Em seguida, darei uma breve visão geral de algumas tecnologias para a criação de serviços Web. Cada uma tem alguns prós e contras diferentes que vale a pena mencionar. Em seguida, mostrarei um exemplo usando uma dessas tecnologias.

Por fim, falarei um pouco sobre como você pode aproveitar alguns dos constructos de alto desempenho no .NET Core, como o Span<'T>, para reduzir as alocações e realmente acelerar os caminhos críticos no sistema.

Introdução ao F# usando a CLI do .NET

Uma das maneiras mais fáceis de começar com o F# é usar a CLI do .NET. Primeiro, verifique se você instalou o SDK do .NET mais recente.

Agora, vamos criar alguns projetos que estão todos conectados entre si. Os terminais modernos dão suporte à conclusão da guia, portanto, embora haja seja necessário digitar um pouco, seu ambiente deverá ser capaz de concluir grande parte para você. Para começar, criarei uma solução:

dotnet new sln -o FSNetCore && cd FSNetCore

Isso criará um diretório chamado FSNetCore, criará um arquivo de solução nesse diretório e alterará os diretórios para FSNetCore.

Em seguida, criarei um projeto de biblioteca e o conectarei ao arquivo da solução:

dotnet new lib -lang F# -o src/Library
dotnet sln add src/Library/Library.fsproj

Adicionarei um pacote a essa biblioteca:

dotnet add src/Library/Library.fsproj package Newtonsoft.Json

Isso adicionará o pacote Json.NET ao projeto.

Agora, alterarei o arquivo Library.fs no projeto de biblioteca para que se torne o seguinte:

module Library
open Newtonsoft.Json
let getJsonNetJson value =
  sprintf "I used to be %s but now I'm %s thanks to JSON.NET!"
   value (JsonConvert.SerializeObject(value))

Esse é um módulo que contém uma única função do F# que usa o JSON.NET para serializar um valor genérico em uma cadeia de caracteres JSON com o restante de uma mensagem.

Em seguida, criarei um aplicativo de console que consome a biblioteca:

dotnet new console -lang F# -o src/App
dotnet add src/App/App.fsproj reference src/Library/Library.fsproj
dotnet sln add src/App/App.fsproj

Substituirei o conteúdo do Program.fs no projeto do aplicativo de console pelo seguinte:

open System
[<EntryPoint>]
let main argv =
  printfn "Nice command-line arguments! Here's what JSON.NET has to say about them:"
  argv
  |> Array.map Library.getJsonNetJson
  |> Array.iter (printfn "%s")
  0 // Return an integer exit code

Isso usará cada argumento de linha de comando, o transformará em uma cadeia de caracteres definida pela função de biblioteca e, em seguida, iterará essas cadeias e as imprimirá.

Agora posso executar o projeto:

dotnet run -p src/App Hello World

Isso imprimirá o seguinte no console:

Nice command-line arguments! Here's what JSON.NET has to say about them:
I used to be Hello but now I'm ""Hello"" thanks to JSON.NET!
I used to be World but now I'm ""World"" thanks to JSON.NET!

Muito fácil, não? Vamos adicionar um teste de unidade e conectá-lo à solução e à biblioteca:

dotnet new xunit -lang F# -o tests/LibTests
dotnet add tests/LibTests/LibTests.fsproj reference src/Library/Library.fsproj
dotnet sln add tests/LibTests/LibTests.fsproj

Agora, substituirei o conteúdo de Tests.fs no projeto de teste pelo seguinte:

module Tests
open Xunit
[<Fact>]
let ``Test Hello`` () =
  let expected = """I used to be Hello but now I'm "Hello" thanks to JSON.NET!"""
  let actual = Library.getJsonNetJson "Hello"
  Assert.Equal(expected, actual)

Esse teste simples apenas verifica se a saída está correta. Observe que o nome do teste está entre dois caracteres de acento grave para permitir o uso de um nome mais natural para o teste. Isso é bastante comum nos testes de F#. Além disso, você usa a cadeia de caracteres entre aspas triplas quando deseja inserir cadeias de caracteres entre aspas no F#. Se preferir, você poderá usar barras invertidas.

Agora eu posso executar o teste:

dotnet test

E a saída verifica que ele é aprovado!

Starting test execution, please wait...
Test Run Successful.
Total tests: 1
Passed: 1
Total time: 4.9443 Seconds

Viva! Com um conjunto de ferramentas mínimo, é totalmente possível criar uma biblioteca testada por unidade e um aplicativo de console que executa esse código de biblioteca. Isso por si só é suficiente para criar realmente uma solução completa, especialmente se você a emparelhar com um editor de código do F# como o Visual Studio Code e o plug-in Ionide. Na verdade, muitos desenvolvedores de F# profissionais usam apenas isso para fazer o trabalho diário deles! Você pode encontrar a solução completa no GitHub em bit.ly/2Svquuc.

Isso é muito divertido, mas vamos dar uma olhada em um material mais interessante do que um projeto de biblioteca ou um aplicativo de console.

Criar um aplicativo Web com o F#

O F# pode ser usado para muito mais do que apenas projetos de biblioteca e aplicativos de console. Entre as soluções F# mais comuns que os desenvolvedores criam estão os serviços e os aplicativos Web. Há três opções principais para fazer isso e abordarei brevemente cada uma delas:

Giraffe (bit.ly/2Z4zPeP) é mais bem definido como "associações de programação funcional para ASP.NET Core". Ele é fundamentalmente uma biblioteca de middleware que expõe rotas como funções de F#. O Giraffe é uma biblioteca de funções básicas que interfere pouco no modo como você cria seus serviços Web ou aplicativo Web. Ele oferece algumas maneiras excelentes de compor rotas usando técnicas funcionais e tem algumas funções internas interessantes para simplificar coisas como trabalhar com JSON ou XML, mas o modo como você compõe seus projetos cabe inteiramente a você. Muitos desenvolvedores de F# usam o Giraffe devido à flexibilidade e porque ele tem uma base muito sólida, pois usa subjacentemente o ASP.NET Core.

O Saturn (bit.ly/2YjgGsl) é algo como "associações de programação funcional para ASP.NET Core, mas com baterias incluídas". Ele usa uma faceta do padrão MVC, mas funcionalmente em vez de usar abstrações de programação orientadas a objeto. Ele foi criado com base no Giraffe e compartilha suas abstrações principais, mas é bem mais interferente quanto a como criar aplicativos Web com o .NET Core. Ele também inclui algumas ferramentas de linha de comando da CLI do .NET que oferecem geração de modelo de banco de dados, migrações de banco de dados e scaffolding de modos de exibição e controladores da Web. Como resultado de ser mais interferente, o Saturn inclui mais funções internas do que o Giraffe, mas requer que você aceite a abordagem que ele impõe.

O Suave (bit.ly/2YmDRSJ) já existe há muito mais tempo do que o Giraffe e o Saturn. Ele era a influência principal do Giraffe, porque foi o pioneiro no modelo de programação que ambos usam no F# para serviços Web. Uma diferença importante é que o Suave tem um servidor Web próprio em conformidade com OWIN que ele usa, em vez de apenas ficar com o ASP.NET Core. Esse servidor Web é altamente portátil, pois pode ser inserido em dispositivos de baixa potência por meio do Mono. O modelo de programação para Suave é um pouco mais simples do que o do Giraffe devido a não necessidade de trabalhar com abstrações do ASP.NET Core.

Começarei criando um serviço Web simples com o Giraffe. Isso é fácil com a CLI do .NET:

dotnet new -i "giraffe-template::*"
dotnet new giraffe -lang F# -V none -o GiraffeApp
cd GiraffeApp

Agora, posso compilar o aplicativo executando build.bat ou sh build.sh.

Agora executarei o projeto:

dotnet run -p src/GiraffeApp

Em seguida, posso navegar até a rota fornecida pelo modelo /api/hello, via localhost.

Navegar até https://localhost:5001/api/hello me dá:

{"text":"Hello world, from Giraffe!"}

Beleza! Vejamos como isso foi gerado. Para fazer isso, vou abrir o arquivo Program.fs e observar a função webApp:

let webApp =
  choose [
    subRoute "/api"
      (choose [
        GET >=> choose [
          route "/hello" >=> handleGetHello
        ]
      ])
    setStatusCode 404 >=> text "Not Found" ]

Há algumas coisas acontecendo aqui e, na verdade, isso tem como base alguns conceitos de programação funcional bastante complicados. Mas, basicamente, trata-se de uma DSL (linguagem específica de domínio) e você não precisa entender cada detalhe de como ela funciona para poder usá-la. Aqui estão os conceitos básicos:

A função webApp é composta de uma função chamada choose. A função choose é executada pelo runtime do ASP.NET Core sempre que alguém faz uma solicitação para o servidor. Ele examinará a solicitação e tentará encontrar uma rota correspondente. Se não conseguir, fará fallback para a rota 404 definida na parte inferior.

Como tenho uma sub-rota definida, a função choose saberá rastrear todas as respectivas rotas filho. A definição do que deve ser rastreado é especificada por uma função choose interna e a respectiva lista de rotas. Como você pode ver, dentro dessa lista de rotas há uma rota que especifica a cadeia de caracteres "/hello" como seu padrão de rota.

Essa rota é, na verdade, outra função do F# chamada route. Ela usa um parâmetro de cadeia de caracteres para especificar o padrão de rota e é, em seguida, composta com o operador >=>. Essa é uma maneira de dizer: "esta rota corresponde à função que a segue". Esse tipo de composição é o que é conhecido como composição de Kleisli e, embora não seja essencial entender essa base teórica para usar o operador, vale a pena saber que ele tem uma base matemática firme. Como mencionei anteriormente neste artigo, os desenvolvedores de F# se inclinam para a exatidão... E o que pode ser mais correto do que uma base matemática?

Você observará que a função no lado direito do operador >=>, handleGetHello, está definida em outro lugar. Vamos abrir o arquivo em que ele está definido, HttpHandlers.fs:

let handleGetHello =
  fun (next: HttpFunc) (ctx: HttpContext) ->
    task {
      let response = {
        Text = "Hello world, from Giraffe!"
       }
      return! json response next ctx
    }

Ao contrário das funções F# "normais", essa função de manipulador é, na verdade, definida como um lambda: Ela é uma função de primeira classe. Embora esse estilo não seja tão comum no F#, ele é escolhido porque os dois parâmetros que o lambda usa – next e ctx – normalmente são construídos e passados para o manipulador pelo runtime do ASP.NET Core subjacente, não necessariamente por código do usuário. Pela nossa perspectiva como programadores, não precisamos passá-los nós mesmos.

Esses parâmetros definidos na função lambda são abstrações que são definidas no runtime do ASP.NET Core propriamente dito e usadas por ele. Uma vez no corpo da função lambda, com esses parâmetros, você pode construir qualquer objeto que deseje serializar e enviar pelo pipeline do ASP.NET Core. O valor chamado response é uma instância de um tipo de registro do F# que contém um único rótulo, Text. Já que Text é uma cadeia de caracteres, uma cadeia de caracteres é passada a ele para que ele a serialize. A definição desse tipo reside no arquivo Models.fs. Em seguida, a função retorna uma representação codificada em JSON da resposta, com os parâmetros next e ctx.

Outra maneira de examinar um pipeline do Giraffe é com duas inserções de um pequeno texto clichê, nas partes superior e inferior de uma função, para conformidade com a abstração de pipeline ASP.NET Core e, depois, tudo o que você desejar entre elas:

let handlerName =
  fun (next: HttpFunc) (ctx: HttpContext) ->
    task {
      // Do anything you want here
      //
      // ... Well, anything within reason!
      //
      // Eventually, you’ll construct a response value of some kind,
      // and you’ll want to serialize it (as JSON, XML or whatever).
      //
      // Giraffe has multiple middleware-function utilities you can call.
      return! middleware-function response next ctx
    }

Embora isso pareça muito para aprender logo de início, é uma ferramenta muito produtiva e fácil para se criar serviços e aplicativos Web. Para demonstrar isso, adicionarei uma nova função de manipulador no arquivo HttpHandlers.fs que fornecerá uma saudação se você especificar seu nome:

let handleGetHelloWithName (name: string) =
  fun (next: HttpFunc) (ctx: HttpContext) ->
    task {
      let response = {
        Text = sprintf "Hello, %s" name
       }
      return! json response next ctx
    }

Como antes, configurei esse manipulador com o texto clichê necessário para estar em conformidade com o middleware do ASP.NET Core. Uma diferença importante é que minha função de manipulador usa uma cadeia de caracteres como entrada. Eu uso o mesmo tipo de resposta que antes.

Em seguida, adicionarei uma nova rota no arquivo Program.fs, mas, como quero especificar uma cadeia de caracteres arbitrária como entrada, precisarei usar algo diferente da função route. O Giraffe define a função routef exatamente para essa finalidade:

let webApp =
  choose [
    subRoute "/api"
      (choose [
        GET >=> choose [
          route "/hello" >=> handleGetHello
          // New route function added here
          routef "/hello/%s" handleGetHelloWithName
        ]
      ])
    setStatusCode 404 >=> text "Not Found" ]

A função routef usa duas entradas:

  • Uma cadeia de caracteres de formato que representa a rota e a respectiva entrada (nesse caso, uma cadeia de caracteres com %s)
  • Uma função de manipulador (que defini anteriormente)

Você observará que não forneci o operador >=> aqui. Isso ocorre porque a routef tem dois parâmetros: o padrão de cadeia de caracteres (especificado por uma cadeia de caracteres de formato do F#) e o manipulador que opera em tipos especificados pela cadeia de caracteres de formato do F#. Isso é diferente do que ocorre na função route, que usa apenas um padrão de cadeia de caracteres como entrada. Nesse caso, como não preciso compor routef e meu manipulador com mais nada, não uso o operador >=> para compor manipuladores adicionais. Mas, se eu quisesse fazer algo como definir um código de status HTTP específico, faria isso compondo com >=>.

Agora, posso recompilar o aplicativo e navegar até https://localhost:5001/api/hello/phillip. Quando faço isso, obtenho:

{"text":"Hello, Phillip”}

Viva! Muito fácil, não? Assim como acontece com qualquer biblioteca ou estrutura, há algumas coisas a serem aprendidas, mas quando você está familiarizado com as abstrações, é incrivelmente fácil adicionar rotas e manipuladores que fazem o que você precisa.

Você pode ler mais sobre como o Giraffe funciona na excelente documentação sobre essa estrutura (bit.ly/2GqBVhT). E você encontrará um aplicativo de exemplo executável que mostra o que demonstrei em bit.ly/2Z21yNq.

Acelerando

Agora, vou divergir do aplicativo prático do F# para aprofundar-me em algumas características de desempenho.

Ao criar algo como um serviço Web com alto tráfego, o desempenho é importante! Especificamente, evitar alocações desnecessárias para promover a limpeza do GC tende a ser uma das coisas mais impactantes que você pode fazer para processos de servidor Web de longa execução.

É aí que tipos como Span<'T> se destacam quando você está usando o F# e o .NET Core. Um span é uma espécie de janela em um buffer de dados que você pode usar para ler e manipular esses dados. O Span<'T> impõe uma variedade de restrições sobre como você pode usá-lo para que o runtime possa garantir que vários aprimoramentos de desempenho se apliquem.

Demonstrarei isso com um exemplo (conforme visto em "All About Span: Exploring a new .NET Mainstay” de Steven Toub em msdn.com/magazine/mt814808). Usarei o BenchmarkDotNet para medir os resultados.

Primeiro, criarei um aplicativo de console no .NET Core:

dotnet new console -lang F# -o Benchmark && cd Benchmark
dotnet add package benchmarkdotnet

Em seguida, o modificarei para submeter a benchmark uma rotina que tenha uma implementação típica e uma implementação que use o Span<'T>, conforme mostrado na Figura 2.

Figura 2 Como submeter a benchmark uma rotina de análise com e sem o Span<'T>

open System
open BenchmarkDotNet.Attributes
open BenchmarkDotNet.Running
module Parsing =
  /// "123,456" --> (123, 456)
  let getNums (str: string) (delim: char) =
    let idx = str.IndexOf(delim)
    let first = Int32.Parse(str.Substring(0, idx))
    let second = Int32.Parse(str.Substring(idx + 1))
    first, second
  let getNumsFaster (str: string) (delim: char) =
    let sp = str.AsSpan()
    let idx = sp.IndexOf(delim)
    let first = Int32.Parse(sp.Slice(0, idx))
    let second = Int32.Parse(sp.Slice(idx + 1))
    struct(first, second)
[<MemoryDiagnoser>]
type ParsingBench() =
  let str = "123,456"
  let delim = ','
  [<Benchmark(Baseline=true)>]
  member __.GetNums() =
    Parsing.getNums str delim |> ignore
  [<Benchmark>]
  member __.GetNumsFaster() =
    Parsing.getNumsSpan str delim |> ignore
[<EntryPoint>]
let main _ =
  let summary = BenchmarkRunner.Run<ParsingBench>()
  printfn "%A" summary
  0 // Return an integer exit code

O módulo chamado Parsing contém duas funções que dividem uma cadeia de caracteres por um determinado delimitador, retornando uma tupla representando cada metade da cadeia de caracteres. No entanto, um chamado getNumsFaster usa as duas tuplas Span<'T> e struct para eliminar alocações. Como você verá, os resultados são bastante profundos.

Executarei o parâmetro de comparação para produzir os resultados:

dotnet run -c release

Isso produzirá resultados que podem ser compartilhados como Markdown, HTML ou outros formatos.

Executei esse parâmetro de comparação em meu laptop com o seguinte hardware e ambiente de runtime:

  • BenchmarkDotNet v0.11.5
  • macOS Mojave 10.14.5 (18F132) [Darwin 18.6.0]
  • Intel Core i7-7700HQ, CPU com 2,80 GHz (Kaby Lake), 1 CPU, 8 núcleos lógicos e 4 núcleos físicos
  • SDK do .NET Core = 3.0.100-preview5-011568 (64 bits)

Os resultados são mostrados na Figura 3.

Figura 3 Resultados do parâmetro de comparação

Método Médio Erro Desvio-padrão Taxa Gen0 Gen1 Gen2 Alocado
GetNums 90,17 ns 0,6340 ns 0,5620 ns 1,00 0,5386 88 B
GetNumsFaster 60,01 ns 0,2480 ns 0,2071 ns 0,67

Impressionante, não? A rotina getNumsFaster não apenas alocou 0 byte adicional, mas também foi executada 33% mais rápido!

Se você ainda não estiver convencido de que isso é importante, imagine um cenário em que você precisa executar 100 transformações sobre esses dados e tudo isso tinha que acontecer no caminho crítico para um serviço Web com tráfego intenso. Se esse serviço recebeu solicitações na ordem de milhões por segundo, você observará um problema de desempenho bem severo se estiver alocando em cada transformação (ou em várias delas). No entanto, se você usar tipos como Span<'T> e tuplas struct, todas essas alocações poderão muitas vezes desaparecer. E, conforme mostra o parâmetro de comparação, também pode levar bem menos tempo total para executar uma determinada operação.

Conclusão

Como você pode ver, é possível fazer muita coisa com o F# no .NET Core! É muito fácil começar a usá-lo e a criar aplicativos Web também. Além disso, a capacidade de usar constructos como Span<'T> significa que o F# também pode ser usado para trabalhos em que o desempenho é crucial.

O F# está ficando cada vez melhor no .NET Core e a comunidade continua crescendo. Adoraríamos que você ingressasse na comunidade para participar da criação de algumas das próximas maravilhas para F# e .NET!


Phillip Carter é um membro da equipe do .NET na Microsoft. Ele trabalha na linguagem F# e nas ferramentas, na documentação do F#, no compilador C# e nas ferramentas de integração de projetos do .NET para Visual Studio.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Dustin Moris Gorski


Discuta esse artigo no fórum do MSDN Magazine