Septiembre de 2019

Volumen 34, número 9

[F#]

Hágalo todo con F# en .NET Core

Por Phillip Carter

F# es el lenguaje de programación funcional para .NET (bit.ly/2y4PeQG). Es multiplataforma y, al igual que todo .NET, es de código abierto (github.com/dotnet/fsharp). En Microsoft, somos muy aficionados a F# porque aporta programación funcional a .NET. Para aquellos que no estén familiarizados con el concepto, el paradigma de programación funcional es el que hace hincapié en los siguientes aspectos:

  • Funciones como construcciones principales para operar con datos.
  • Expresiones en lugar de instrucciones.
  • Valores inmutables en lugar de variables.
  • Programación declarativa en lugar de programación imperativa.

Esto significa que F# incorpora algunas características muy interesantes a .NET:

  • Funciones de primera clase (se pueden pasar como valores a otras funciones, que también las pueden devolver).
  • Sintaxis ligera que da más importancia a las expresiones y los valores, no a las instrucciones.
  • Inmutabilidad integrada y tipos no nulos.
  • Tipos de datos enriquecidos y técnicas avanzadas de coincidencia de patrones.

El código de F# típico suele acabar teniendo un aspecto como el que se muestra en la figura 1.

Figura 1. Código de 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 :("

Además de estas características principales, F# también puede interoperar con todo .NET y es totalmente compatible con los objetos, las interfaces, etc. Las técnicas de programación más avanzadas de F# suelen implicar una combinación sutil de características orientadas a objetos (OO) con código funcional, pero sin sacrificar el paradigma de programación funcional.

Además, F# tiene muchas características únicas que a la gente le encantan, como expresiones de cómputo, unidades de medida y tipos muy eficaces, como registros y uniones discriminadas. Consulte el material de referencia del lenguaje F# en bit.ly/2JSnipy para obtener más información. F# también promueve un estilo de programación que tiende a la seguridad y la corrección: Muchos desarrolladores de F# han recurrido a él después de tener mucha experiencia con otros lenguajes que no hacen tanto hincapié en la seguridad y la corrección. Ha influido en gran parte del trabajo reciente que se ha incorporado a C#, como la programación asincrónica, las tuplas, la coincidencia de patrones y el próximo conjunto de características de tipos de referencia que aceptan valores NULL.

F# tiene también una comunidad muy activa a la que le encanta superar los límites de .NET y crear componentes de código abierto increíbles. La comunidad es muy innovadora e increíblemente valiosa para .NET, ya que es pionera en la creación de bibliotecas de interfaz de usuario, bibliotecas de procesamiento de datos, metodologías de prueba, servicios web y mucho más para NET.

La abundancia de características y la comunidad tan activa de desarrolladores entusiastas han llevado a muchas personas a profundizar en F#, tanto por diversión como por trabajo. F# se utiliza en instituciones financieras de todo el mundo, sistemas de comercio electrónico, la informática científica, organizaciones que usan ciencia de datos y aprendizaje automático, consultorías, etc. También se utiliza bastante en Microsoft: Al ser uno de los principales lenguajes que utiliza Microsoft Research, influyó y ayudó a poner en marcha la plataforma de desarrollo Q#, partes de Azure y Office 365, e incluso el compilador de F# y herramientas de Visual Studio para F#. En resumen, se usa en todas partes.

¿Interesado? ¡Genial! En este artículo, explicaré cómo hacer algunas cosas fantásticas con F# en .NET Core.

Descripción general

Empezaré con los aspectos básicos de F# en .NET Core y seguiré progresivamente con funcionalidad más avanzada (y más interesante); por ejemplo, cómo se puede usar la CLI de .NET para crear una aplicación de consola y un proyecto de biblioteca en cualquier sistema operativo. A pesar de ser un conjunto de herramientas sencillo y básico, puede ser suficiente para desarrollar una aplicación completa cuando se combina con un editor como Visual Studio Code y el complemento oficial de F#, Ionide(ionide.IO). También puede usar Vim o Emacs si lo desea.

A continuación, haré una introducción a algunas tecnologías para crear servicios web. Cada una de ellas tiene diferentes ventajas y desventajas que merece la pena mencionar. Después, mostraré un ejemplo usando una de estas tecnologías.

Por último, hablaré un poco sobre cómo se pueden aprovechar algunas de las construcciones de alto rendimiento en .NET Core, como Span<'T>, para reducir las asignaciones y agilizar realmente las rutas de procesamiento rápido en el sistema.

Comience a usar F# con la CLI de .NET

Una de las formas más sencillas de comenzar a usar F# es utilizar la CLI de .NET. En primer lugar, debe asegurarse de que tiene instalada la última versión del SDK de .NET.

Ahora vamos a crear algunos proyectos que están conectados entre sí. Los terminales actuales ofrecen finalización con TAB, por tanto, aunque hay que escribir un poco, el entorno debería finalizar gran parte automáticamente. Para comenzar, crearé una solución nueva:

dotnet new sln -o FSNetCore && cd FSNetCore

Este código creará un nuevo directorio denominado FSNetCore, creará un archivo de solución en ese directorio y cambiará los directorios a FSNetCore.

Después, crearé un proyecto de biblioteca y lo enlazaré al archivo de solución:

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

Agregaré un paquete a esa biblioteca:

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

Este código agregará el paquete Json.NET al proyecto.

Ahora cambiaré el archivo Library.fs del proyecto de biblioteca para que sea el siguiente:

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

Este es un módulo que contiene una sola función de F# que utiliza JSON.NET para serializar un valor genérico en una cadena JSON con el resto de un mensaje.

A continuación, crearé una aplicación de consola que consuma la 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

Reemplazaré el contenido de Program.fs en el proyecto de aplicación de consola por lo siguiente:

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

Esto tomará cada argumento de la línea de comandos, lo transformará en una cadena definida por la función de biblioteca y, después, procesará una iteración en esas cadenas y las imprimirá.

Ahora ya puedo ejecutar el proyecto:

dotnet run -p src/App Hello World

Este código imprimirá lo siguiente en la consola:

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!

Muy fácil, ¿no? Vamos a agregar una prueba unitaria y la vamos a conectar a la solución y a la 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

Ahora reemplazaré el contenido de Tests.fs en el proyecto de prueba por lo siguiente:

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)

Esta sencilla prueba solo comprueba que la salida sea correcta. Tenga en cuenta que el nombre de la prueba se encierra entre acentos graves dobles para permitir el uso de un nombre más natural para la prueba. Esto es bastante común en las pruebas de F#. Además, se usa una cadena con comillas triples cuando se quieren insertar cadenas entre comillas en F#. También puede usar barras diagonales inversas si lo prefiere.

Ahora ya puedo ejecutar la prueba:

dotnet test

Y la salida confirma que se supera.

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

¡Ta chan! Con un conjunto de herramientas mínimo, es totalmente posible crear una biblioteca sometida a pruebas unitarias y una aplicación de consola que ejecute el código de esa biblioteca. Solo esto es suficiente para compilar realmente una solución completa, sobre todo si se combina con un editor de código de F#, como Visual Studio Code y el complemento Ionide. De hecho, muchos desarrolladores profesionales de F# solo usan esto para realizar su trabajo diario. Encontrará la solución completa en GitHub en bit.ly/2Svquuc.

Esto es bastante divertido, pero echemos un vistazo a algún material que sea más interesante que un proyecto de biblioteca o una aplicación de consola.

Creación de una aplicación web con F#

F# puede usarse para hacer muchas más cosas, no solo proyectos de biblioteca y aplicaciones de consola. Entre las soluciones más comunes que crean los desarrolladores de F# están los servicios y las aplicaciones web, para lo que hay tres opciones principales que explicaré brevemente:

Giraffe (bit.ly/2Z4zPeP). Se puede definir como “enlaces de programación funcionales con ASP.NET Core”. Básicamente, es una biblioteca de middleware que expone rutas como funciones de F#. Giraffe es una especie de biblioteca “básica” que interfiere relativamente poco en el modo en el que se crean servicios o aplicaciones web. Ofrece algunas maneras fantásticas de crear rutas usando técnicas funcionales y tiene algunas funciones integradas muy interesantes para simplificar las cosas, como trabajar con JSON o XML, pero la forma de crear los proyectos depende totalmente del desarrollador. Muchos desarrolladores de F# utilizan Giraffe por lo flexible que es y por la base tan sólida que tiene, ya que usa ASP.NET Core en segundo plano.

Saturn (bit.ly/2YjgGsl) es como “enlaces de programación funcionales con ASP.NET Core, pero con las pilas incluidas”. Utiliza un formulario del patrón MVC, pero lo hace funcionalmente en lugar de con abstracciones de programación orientada a objetos. Se basa en Giraffe y comparte sus abstracciones principales, pero interfiere mucho más en el modo de crear aplicaciones web con .NET Core. También incluye algunas herramientas de línea de comandos de la CLI de .NET que ofrecen generación de modelos de base de datos, migraciones de bases de datos y scaffolding de controladores y vistas web. Saturn incluye más funciones integradas que Giraffe por el hecho de interferir más, pero requiere que se acepte el enfoque que impone.

Suave (bit.ly/2YmDRSJ) existe desde hace mucho más tiempo que Giraffe y Saturn. Fue la principal influencia para Giraffe porque fue el primero en usar el modelo de programación que los dos usan en F# para los servicios web. Una diferencia clave es que Suave utiliza su propio servidor web, que es conforme con OWIN, en lugar de ASP.NET Core. Este servidor web es muy portátil, ya que se puede insertar en dispositivos de poca capacidad a través de Mono. El modelo de programación de Suave es ligeramente más sencillo que el de Giraffe, porque no es necesario trabajar con abstracciones de ASP.NET Core.

Empezaré por crear un servicio web sencillo con Giraffe. Es fácil con la CLI de .NET:

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

Ahora puedo compilar la aplicación ejecutando build.bat o sh build.sh.

Ahora ejecutaré el proyecto:

dotnet run -p src/GiraffeApp

A continuación, puedo ir a la ruta que proporciona la plantilla /api/hello a través de localhost.

Al ir a https://localhost:5001/api/hello, obtengo esto:

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

¡Genial! Echemos un vistazo a la forma en la que se ha generado este código. Para ello, abriré el archivo Program.fs. Observe la función webApp:

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

Aquí están pasando muchas cosas y, de hecho, este código se basa en algunos conceptos de programación funcional bastante complejos. Pero, en definitiva, es un lenguaje específico del dominio (DSL) y no es necesario comprender totalmente cómo funciona para usarlo. Estos son los aspectos básicos:

La función webApp está formada por una función denominada choose. Esta función choose se ejecuta en el entorno en tiempo de ejecución de ASP.NET Core cada vez que alguien realiza una solicitud al servidor. Examinará la solicitud e intentará buscar una ruta que coincida. Si no la encuentra, recurrirá a la ruta 404 definida al final.

Como he definido un elemento subRoute, la función choose sabrá que tiene que rastrear todas sus rutas secundarias. La definición de lo que se va a rastrear se especifica con una función choose interna y su lista de rutas. Como puede ver, en esa lista de rutas hay una ruta que especifica la cadena “/hello” como su patrón de ruta.

Esta ruta es, en realidad, otra función de F# denominada route. Toma un parámetro de cadena para especificar el patrón de ruta y, después, se compone con el operador >=>. Es una forma de decir “esta ruta corresponde a la función que la sigue”. Este tipo de composición es lo que se conoce como composición Kleisli y, aunque no es fundamental comprender la base teórica para usar el operador, merece la pena saber que tiene una base matemática firme. Como ya comenté al principio del artículo, los desarrolladores de F# tienen tendencia a la corrección... ¿y qué hay más correcto que una base matemática?

Verá que la función del lado derecho del operador >=>, handleGetHello, se define en otra parte. Abramos el archivo donde se define, HttpHandlers.fs:

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

A diferencia de las funciones “normales” de F#, esta función de controlador se define en realidad como una función lambda: Es una función de primera clase. Aunque este estilo no es muy común en F#, se elige porque los dos parámetros que toma la expresión lambda (next y ctx) los suele construir y pasar al controlador el entorno en tiempo de ejecución de ASP.NET Core subyacente, no necesariamente código del usuario. Desde nuestro punto de vista como programadores, no es necesario que los pasemos nosotros.

Estos parámetros definidos en la función lambda son abstracciones que se definen y se utilizan en el propio entorno en tiempo de ejecución de ASP.NET Core. Una vez en el cuerpo de la función lambda, con estos parámetros, puede construir cualquier objeto que desee serializar y enviar por la canalización de ASP.NET Core. El valor denominado response es una instancia de un tipo de registro de F# que contiene una sola etiqueta, Text. Puesto que Text es una cadena, se le da una cadena para serializarla. La definición de este tipo reside en el archivo Models.fs. Después, la función devuelve una representación de la respuesta codificada con JSON, con los parámetros next y ctx.

Otra forma de ver una canalización de Giraffe es con dos pequeños fragmentos reutilizables al principio y al final de una función para que sea conforme con la abstracción de canalización de ASP.NET Core y, luego, todo lo que desee entre ambos:

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
    }

Aunque esto puede parecer mucho para aprenderlo al principio, es muy productivo y sencillo para crear aplicaciones y servicios web. Para demostrarlo, voy a agregar una nueva función de controlador en el archivo HttpHandlers.fs que le salude si especifica su nombre:

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

Al igual que antes, configuro este controlador con el fragmento reutilizable para que sea conforme con el middleware de ASP.NET Core. Una diferencia fundamental es que la función de controlador toma una cadena como entrada. Utilizo el mismo tipo de respuesta que antes.

A continuación, voy a agregar una nueva ruta en el archivo Program.fs, pero, como quiero especificar una cadena arbitraria como entrada, tendré que usar otra cosa que no sea la función route. Giraffe define la función routef justo para este fin:

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" ]

La función routef toma dos entradas:

  • Una cadena de formato que representa la ruta y su entrada (en este caso, una cadena con %s)
  • Una función de controlador (que ya definí antes)

Verá que aquí no he proporcionado el operador >=>. Esto se debe a que routef tiene dos parámetros: el patrón de cadena (especificado con una cadena de formato de F#) y el controlador que opera en los tipos especificados por la cadena de formato de F#. Esto contrasta con la función route, que solo toma un patrón de cadena como entrada. En este caso, puesto que no es necesario componer routef ni mi controlador con nada más, no utilizo el operador >=> para componer más controladores. Pero, si quisiera hacer algo como establecer un código de estado de HTTP específico, lo haría usando el operador >=>.

Ahora puedo recompilar la aplicación e ir a https://localhost:5001/api/hello/phillip. Al hacerlo, obtengo esto:

{"text":"Hello, Phillip”}

¡Ta chan! Muy fácil, ¿no? Al igual que con cualquier biblioteca o marco, hay algunas cosas que aprender, pero, una vez que se haya familiarizado con las abstracciones, es increíblemente fácil agregar rutas y controladores que hagan lo que necesite.

Puede leer más información sobre el funcionamiento de Giraffe en su excelente documentación (bit.ly/2GqBVhT). Y encontrará una aplicación de ejemplo ejecutable que muestra lo que he explicado aquí en bit.ly/2Z21yNq.

Avance más rápido

Voy a dejar ahora a un lado la aplicación práctica de F# para profundizar en algunas características de rendimiento.

Cuando se compila algo como un servicio web que tiene mucho tráfico, el rendimiento es importante. En concreto, evitar asignaciones innecesarias para que las limpie el recolector de elementos no utilizados tiende a ser una de las cosas más impactantes que puede hacer para los procesos de servidor web de ejecución prolongada.

Aquí es donde los tipos como Span<'T> empiezan a destacar cuando se utilizan F# y .NET Core. Un tipo Span es una especie de ventana en un búfer de datos que puede usar para leer y manipular esos datos. Span<'T> impone una serie de restricciones sobre cómo se puede utilizar para que el entorno en tiempo de ejecución pueda garantizar que se aplicarán varias mejoras de rendimiento.

Lo mostraré con un ejemplo (como en “C#: todo sobre Span. Exploración de un nuevo pilar de .NET”, de Steven Toub, en msdn.com/magazine/mt814808). Usaré BenchmarkDotNet para medir los resultados.

En primer lugar, crearé una aplicación de consola en .NET Core:

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

A continuación, la modificaré para realizar una prueba comparativa de una rutina que tiene una implementación típica y una implementación que usa Span<'T>, como se muestra en la figura 2.

Figura 2. Realización de una prueba comparativa de una rutina de análisis con y sin 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

El módulo Parsing contiene dos funciones que dividen una cadena con un delimitador determinado y devuelven una tupla que representa cada mitad de la cadena. Sin embargo, otro módulo denominado getNumsFaster utiliza tanto Span<'T> como tuplas de estructura para eliminar las asignaciones. Como verá, los resultados son muy significativos.

Voy a ejecutar la prueba comparativa para producir resultados:

dotnet run -c release

Esto producirá resultados que se podrán compartir como Markdown, HTML u otros formatos.

He realizado la prueba comparativa en un portátil con la siguiente configuración de hardware y entorno en tiempo de ejecución:

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

Los resultados se muestran en la figura 3.

Figura 3. Resultados del banco de pruebas

Método Media Error Desv. est. Proporc. Gen0 Gen1 Gen2 Asignado
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 - - - -

Impresionante, ¿verdad? La rutina getNumsFaster no solo asignó 0 bytes adicionales, sino que también se ejecutó un 33 % más rápido.

Si todavía no está convencido de que esto es importante, imagine una situación en la que tenga que realizar 100 transformaciones en esos datos y todo tenga que hacerse en la ruta de procesamiento rápido para un servicio web con mucho tráfico. Si ese servicio recibiera solicitudes del orden de millones por segundo, tendría un problema de rendimiento muy grave si realizara asignaciones en cada transformación (o en varias). Sin embargo, si utiliza tipos como Span<'T> y tuplas de estructura, es frecuente que todas esas asignaciones puedan desaparecer. Y, como muestra la prueba comparativa, también puede tardar mucho menos tiempo en ejecutar una operación determinada.

Resumen

Como puede ver, es mucho lo que se puede hacer con F# en .NET Core. Comenzar a usarlo es muy sencillo y también empezar a crear aplicaciones web. Además, la posibilidad de usar construcciones como Span<'T> significa que F# se puede usar también para trabajos donde el rendimiento es importante.

F# sigue mejorando en .NET Core y la comunidad continúa creciendo. Nos encantaría que se uniera a la comunidad para crear algunas de las próximas genialidades para F# y .NET.


Phillip Carter forma parte del equipo de .NET en Microsoft. Trabaja en el lenguaje F#, sus herramientas y documentación, en el compilador de C# y en las herramientas de integración de proyectos de .NET para Visual Studio.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Dustin Moris Gorski


Comente este artículo en el foro de MSDN Magazine