F# Primer
Use técnicas de programación funcional en .NET Framework
Ted Neward
En este artículo se analizan los siguientes temas:
- Instalación de F#
- Conceptos básicos del lenguaje F#
- Interoperabilidad con .NET
- F# asincrónico
|
En este artículo se utilizan las siguientes tecnologías:
.NET Framework, F#
|

Contenido
Una de las nuevas incorporaciones de la familia de Microsoft® .NET Framework, F#, proporciona seguridad de tipos, rendimiento y la posibilidad de trabajar del mismo modo que si se tratara de un lenguaje de scripting, y todo como parte del entorno .NET. Este lenguaje funcional fue creado por Don Syme del equipo de investigación de Microsoft como una variante compatible con sintaxis de OCaml para CLR, aunque F# ha pasado rápidamente del laboratorio al taller.
En un tiempo en que los conceptos de programación funcional se arrastran hacia lenguajes más convencionales como C# o Visual Basic® a través de tecnologías como los genéricos de .NET y LINQ, la visibilidad de F# ha crecido dentro de la comunidad de .NET hasta tal punto que en noviembre de 2007 Microsoft anunció que pasaría F# al conjunto de lenguajes de programación admitidos de .NET.
Durante años, el área de lenguajes funcionales (ML, Haskell, etc.) se ha considerado más apropiada para la investigación académica que para el desarrollo profesional. Y no es que estos lenguajes no sean interesantes. De hecho, algunas mejoras importantes realizadas en .NET (genéricos, LINQ, PLINQ, y futuros, por ejemplo) provienen de la aplicación de los conceptos de la programación funcional a lenguajes que hasta ahora nunca los habían visto. La falta de interés en estos lenguajes se ha basado más en el hecho de que estaban destinados a plataformas de poca relevancia para los desarrolladores que escriben programas para Windows®, no se integraban bien con la plataforma subyacente o no admitían funcionalidad clave como el acceso a bases de datos relacionales, análisis XML o mecanismos de comunicación ajenos al proceso.
Sin embargo, CLR y su método "varios lenguajes, una plataforma" hicieron inevitable el avance de un número creciente de estos lenguajes hacia el mundo del desarrollo de Windows. Resultó igualmente inevitable que empezarían a hacer notar su presencia entre los programadores en prácticas. F# es uno de estos lenguajes. En este artículo describiré algunos de los conceptos básicos y ventajas de F#. A continuación, para ayudarle a familiarizarse con F#, le guiaré a través de la instalación y escritura de varios programas sencillos.
¿Por qué usar F#?
Resultará obvio para un porcentaje reducido de programadores de .NET que aprender un lenguaje funcional para .NET Framework es un avance positivo en la programación de software eficaz. Para los demás, la motivación para aprender F# es un misterio absoluto. ¿Qué ventajas puede aportar F# a los desarrolladores?
Escribir programas simultáneos seguros ha pasado a ser una de las preocupaciones principales en los últimos tres años debido al uso cada vez más generalizado de CPU con varios núcleos. Los lenguajes funcionales ayudan a los desarrolladores a admitir la simultaneidad al promover la existencia de estructuras de datos inmutables capaces de transmitirse entre subprocesos y equipos sin tener que preocuparse por la seguridad del subproceso o el acceso atómico. Los lenguajes funcionales tienden también a facilitar la programación de mejores bibliotecas que sean compatibles con la simultaneidad, como por ejemplo los flujos de trabajo asincrónicos de F#, que trataré más adelante en este artículo.
Aunque no se lo parezca a los programadores expertos en el desarrollo orientado a objetos, los programas funcionales son a menudo más sencillos de escribir y mantener para determinadas clases de aplicaciones. Tomemos como ejemplo la escritura de un programa destinado a convertir un documento XML en una forma diferente de datos. Si bien sería ciertamente posible escribir un programa de C# que analizara todo el documento XML y aplicara una serie de instrucciones "if" para determinar qué medidas tomar en diferentes puntos del documento, un método posiblemente más avanzado sería escribir la transformación como un programa de lenguaje de transformación basado en hojas de estilo (XSLT). No resulta sorprendente que XSLT cuente con una gran variedad de funciones, al igual que SQL.
F# desaconseja totalmente el uso de valores NULL y promueve en cambio el uso de estructuras de datos inmutables. Estos aspectos combinados ayudan a reducir la frecuencia de errores en la programación gracias a la reducción de la cantidad de código necesario para casos especiales.
Los programas escritos en F# tienden a ser también más sucintos. Se escribe menos en ambos sentidos de la palabra: menos pulsaciones de tecla y menos lugares donde hay que indicarle al compilador el tipo de variable, los argumentos o el tipo de valor devuelto. Eso puede traducirse en mucho menos código para mantener.
F# tiene un perfil de rendimiento parecido a C#. Sin embargo, tiene un perfil del rendimiento mucho mejor que otros lenguajes sucintos comparables, en especial los lenguajes dinámicos y de scripting. Y, al igual que muchos de los lenguajes dinámicos, F# incluye herramientas que permiten explorar los datos escribiendo fragmentos de programa y ejecutándolos de forma interactiva.
Instalación de F#
Disponible como descarga gratuita en
research.microsoft.com/fsharp/fsharp.aspx, F# instala no sólo todas las herramientas de línea de comandos, sino también un paquete de extensión de Visual Studio
® que ofrece sintaxis resaltada en color, plantillas de proyectos y archivos (incluido un ejemplo muy detallado de código de F# a modo de guía de inicio) y compatibilidad con Intellisense
®. Existe también un shell interactivo de F# que se puede ejecutar dentro de Visual Studio, y que permite a los desarrolladores tomar las expresiones de las ventanas de archivos de código fuente, pegarlas en la ventana del shell interactivo y comprobar los resultados inmediatos del fragmento de código, dentro de lo que se podría describir como una ventana Immediates mejorada.
En el momento en que escribo este artículo, F# se ejecuta como una herramienta externa dentro de Visual Studio, lo que significa que se pierde parte de la perfección que los desarrolladores obtienen con C# o Visual Basic. F# carece de también de compatibilidad con el diseñador de páginas de ASP.NET, entre otras características. Esto no quiere decir ni mucho menos que F# no pueda usarse en ASP.NET. Se trata simplemente de que la compatibilidad de Visual Studio con F# no ofrece el mismo tipo de experiencia de desarrollo predeterminada de arrastrar y colocar para F# que la que ofrece para C# y Visual Basic.
Sin embargo, la versión actual de F# sí puede usarse en cualquier lugar que permita el uso de cualquier lenguaje compatible con .NET. En las siguientes páginas le mostraremos algunos ejemplos.
Hola, F#
La introducción inevitable a cualquier lenguaje tiene lugar a través del omnipresente programa "Hello, World". F# no será una excepción:
Si bien un poco decepcionante, este pequeño ejemplo muestra que F# pertenece a esa categoría de lenguajes que no requiere un punto de entrada explícito (C#, Visual Basic y C++/CLI, en cambio, sí lo requieren); el lenguaje entiende que la primera línea del programa es el punto de entrada y lo ejecuta desde ese punto.
Para ejecutarlo, el desarrollador novato de F# dispone de dos opciones: compilado o interpretado. Ejecutarlo dentro del intérprete de F# (fsi.exe) es un juego de niños. Basta con ejecutar fsi.exe desde la línea de comandos y escribir la línea anterior en el símbolo del sistema resultante, tal como se indica en la figura 1.
Figura 1 Ejecución de "Hello, World" desde el intérprete de F# (Hacer clic en la imagen para ampliarla)
Tenga en cuenta que, en el shell, la instrucción debe finalizarse con dos símbolos de punto y coma. Se trata de una peculiaridad del modo interactivo, por lo que no se requiere para los programas de F# compilados.
Para ejecutar este ejemplo como un archivo ejecutable estándar de .NET, ejecute Visual Studio como de costumbre y cree un nuevo proyecto de F#, que encontrará en Otros tipos de proyectos. Un proyecto de F# consiste, al principio, en un solo archivo de código fuente de F# denominado file1.fs. Al abrir este archivo se nos muestra una gran cantidad de código de F# de ejemplo. Observe el contenido para tener una idea sobre el aspecto de la sintaxis de F#. Cuando termine, reemplace todo el archivo con el código de "Hello, world!" mostrado anteriormente. Ejecute la aplicación; "Hello, world!" aparecerá lógicamente en una ventana de aplicación de consola.
Si prefiere una línea de comandos, puede compilar el código mediante la herramienta fsc.exe que encontrará en el subdirectorio \bin del directorio de instalación de F#. Tenga en cuenta que fsc.exe funciona como la mayoría de compiladores de línea de comandos, es decir, toma el código fuente en la línea de comandos y genera un archivo ejecutable como resultado. La mayor parte de los modificadores de línea de comandos están documentados, aunque muchos ya deben resultar familiares si tiene experiencia con los compiladores csc.exe o cl.exe. Tenga en cuenta, sin embargo, que un área en la que F# se queda rezagado actualmente es en MSBuild, ya que no hay compatibilidad directa en la versión actual (1.9.3.7 en el momento de escribir este artículo) para la compilación controlada mediante MSBuild.
Si prefiere que su "Hello, world!" sea un poco más gráfico, F# ofrece fácilmente fidelidad e interoperabilidad completas con la plataforma subyacente de CLR, incluidas las bibliotecas de Windows Forms. Pruebe con esto:
System.Windows.Forms.MessageBox.Show "Hello World"
Tener la posibilidad de usar la biblioteca de clases de .NET Framework además de las bibliotecas de F# hace de F# un lenguaje atractivo tanto para la comunidad matemática como la científica que ya usa lenguajes funcionales como OCaml o Haskell, así como para los desarrolladores de .NET de todo el mundo.
La expresión let
Observemos ahora un código de F# que no sea tan superficial como el tradicional "Hello, world!". Considere lo siguiente:
let results = [ for i in 0 .. 100 -> (i, i*i) ]
printfn "results = %A" results
Un elemento curioso en esta sintaxis de F# es la expresión let. Se trata de la expresión más importante de todo este lenguaje. Formalmente, let asigna un valor a un identificador. La tentación para el desarrollador de Visual Basic y de C# será traducirlo como "define una variable". Pues bien, se trata de una suposición errónea. Los identificadores en F#, en cambio, incorporan dos principios. En primer lugar, un identificador, una vez definido, nunca puede cambiar. Así es como F# ayuda a los programadores a crear programas de simultaneidad protegida, ya que desaconseja el estado mutable. En segundo lugar, el identificador puede ser no sólo una primitiva o un tipo de objeto, tal como ocurre en C# y Visual Basic, sino también un tipo de función, parecido a lo que podría ocurrir en LINQ.
Observe también cómo los identificadores nunca se definen explícitamente para indicar que tienen un tipo. El identificador de resultados, por ejemplo, nunca se define; se infiere del lado derecho de la expresión que le sigue. Esto se conoce como inferencia de tipos, y representa la capacidad del compilador para analizar el código, determinar el valor devuelto y conectarlo automáticamente. Es parecido a las nuevas expresiones de tipo inferido de C# a través de la palabra clave var.
La expresión let no tiene por qué funcionar únicamente con datos. Puede usarla para definir funciones, que F# reconocerá como conceptos de primera clase. Así, por ejemplo, el siguiente ejemplo define una función de adición que toma dos parámetros, a y b:
La implementación cumple en gran medida los resultados esperados: agrega a y b y devuelve implícitamente el resultado al autor de la llamada. Esto significa que, técnicamente, cada función en F# devuelve un valor, incluso si ese valor no es un valor, lo que se conoce como nombre especial unit. Este hecho presentará ciertas implicaciones interesantes en el código de F#, especialmente en el punto en que se cruza con la biblioteca de clases de .NET Framework pero, por ahora, los desarrolladores de C# y Visual Basic pueden considerar la unidad como un equivalente aproximado de void.
En ocasiones, una función debe omitir un parámetro transmitido. Para lograrlo en F#, basta con usar el carácter de subrayado como un marcador de posición para el propio parámetro:
let return10 _ =
add 5 5
// 12 is effectively ignored, and ten is set to the resulting
// value of add 5 5
let ten = return10 12
printf "ten = %d\n" ten
Al igual que ocurre con muchos lenguajes funcionales, F# permite realizar operaciones curry, en la que una aplicación de la función puede definirse sólo parcialmente, dependiendo de su invocación para suministrar los parámetros restantes:
En cierto modo, es parecido a crear un método sobrecargado que toma un conjunto diferente de parámetros y llama a otro método:
public class Adders {
public static int add(int a, int b) { return a + b; }
public static int add5(int a) { return add(a, 5); }
}
Pero existe una diferencia sutil. Tenga en cuenta que, en la versión de F#, los tipos no se definen explícitamente. Esto significa que el compilador hará su truco de interferencia de tipos, determinará si el parámetro de add5 es compatible por tipo con la adición al literal entero 5 y lo compilará de este modo o indicará un error. De hecho, gran parte del lenguaje F# está parametrizado implícitamente por tipos, es decir, hace uso de genéricos.
En Visual Studio, al desplazar el puntero por la definición anteriormente mostrada de diez revelará que su tipo se ha declarado como:
En F#, esto significa que diez es un valor, una función que toma un parámetro de cualquier tipo y proporciona un resultado int. La sintaxis de Tick es el equivalente aproximado a la sintaxis <T> en C#, así que la traducción más cercana a una función de C# sería decir que diez se parece a una instancia de delegación a un método parametrizado por tipos cuyo tipo se desea realmente omitir, lo cual no es posible según las reglas de C#:
delegate int Transformer<T>(T ignored);
public class App
{
public static int return10(object ignored) { return 5 + 5; }
static void Main()
{
Transformer<object> ten = return10;
System.Console.WriteLine("ten = {0}", return10(0));
}
}
La palabra clave For
Observemos ahora la palabra clave for en el primer ejemplo:
#light
let results = [ for i in 0 .. 100 -> (i, i*i) ]
printfn "results = %A" results
Empezando desde el principio del código, fíjese en la sintaxis #light. Se trata de una concesión a los programadores ajenos a OCaml que prueban F#, ya que otorga mayor flexibilidad a algunos de los requisitos de sintaxis del lenguaje OCaml y usa un significativo espacio en blanco para definir los bloques de código. Mientras no sea necesario, no facilita el análisis de la sintaxis para el desarrollador medio proveniente de C# o Visual Basic, por lo que a menudo aparece en los ejemplos de F# y fragmentos de código publicados, lo que lo convierte en el estándar de facto para la programación en F#. Una versión futura de F# podría convertir a #light en la sintaxis predeterminada, y no a la inversa.
Este bucle aparentemente inocente es, de hecho, de todo menos sencillo. Oficialmente, se trata de una lista generada, que es una manera elegante de decir que es un bloque de código que producirá un resultado en forma de lista.
Una lista es una construcción primitiva encontrada con frecuencia en lenguajes funcionales, y en ese sentido se parece en gran medida a una matriz. Sin embargo, una lista no permite el acceso basado en la posición, tal como ocurre con la tradicional sintaxis a[i] de C#. Las listas pueden aparecer en muchos lugares diversos en la programación funcional y, en su mayor parte, pueden considerarse como el equivalente de F# a la List<T> de .NET Framework con algunas capacidades mejoradas.
Una lista siempre pertenece a un tipo determinado, y en este caso los resultados del identificador son una lista de tuplas, en concreto el tipo de tupla identificado por F# como (int * int). Esta idea de lista de tuplas resulta familiar si se considera como equivalente a un par de columnas devueltas por una instrucción SELECT en SQL. Así pues, el ejemplo crea básicamente una lista de pares de enteros de 100 elementos de longitud.
Por lo general, en los lenguajes funcionales, las definiciones de funciones se usan en cualquier lugar en el que pueda aparecer el propio código. Así, si desea ampliar el ejemplo anterior, podría escribir lo siguiente:
let compute2 x = (x, x*x)
let compute3 x = (x, x*x, x*x*x)
let results2 = [ for i in 0 .. 100 -> compute2 i ]
let results3 = [ for i in 0 .. 100 -> compute3 i ]
Esta idea de realizar un bucle a través de una lista (o matriz o cualquier otra construcción iterable) es una tarea tan común en los lenguajes funcionales que ha acabado por generalizarse como una llamada a método básica: List.iter simplemente llama a una función en cada elemento de la lista. Hay otras funciones similares de la biblioteca que proporcionan capacidades prácticas. Por ejemplo, List.map toma una función como un argumento y la aplica a cada elemento de la lista, devolviendo una lista nueva en el proceso.
Canalización
Pasemos a examinar otra construcción en F#: el operador de canalización, que toma los resultados de una función y, de un modo similar a las canalizaciones de shells de comandos como Windows PowerShell®, los usa como entrada a una función de seguimiento. Tomemos el fragmento de código de F# mostrado en la figura 2. Este código usa el espacio de nombres System.Net para conectarse a un servidor HTTP, obtener el correspondiente código HTML y analizar los resultados.

Figure 2 Recuperación y análisis de HTML
/// Get the contents of the URL via a web request
let http(url: string) =
let req = System.Net.WebRequest.Create(url)
let resp = req.GetResponse()
let stream = resp.GetResponseStream()
let reader = new System.IO.StreamReader(stream)
let html = reader.ReadToEnd()
resp.Close()
html
let getWords s = String.split [ ' '; '\n'; '\t'; '<'; '>'; '=' ] s
let getStats site =
let url = "http://" + site
let html = http url
let words = html |> getWords
let hrefs = html |> getWords |> List.filter (fun s -> s = "href")
(site,html.Length, words.Length, hrefs.Length)
Observe el identificador de términos en la definición de getStats. Toma el valor html devuelto por la dirección URL y le aplica la función getWords. Podría haber escrito también la definición para que indicara:
let words = getWords html
Ambos son idénticos. Sin embargo, el identificador hrefs demuestra el poder del operador de canalización, ya que puede encadenar un número cualquiera de aplicaciones unas con otras. En este caso, tomo la lista resultante de términos y la canalizo a través de la función List.filter, la cual toma una función anónima para buscar el href del término y devolverlo si la expresión es válida. Y, para redondearlo, los resultados de la llamada de getStats formarán otra tupla, en este caso un (string * int *int * int). Escribir esto en C# ocuparía mucho más de 15 líneas de código.
El ejemplo de la figura 2 también muestra otro ejemplo de la compatibilidad de F# con .NET Framework, un tema recurrente:
open System.Collections.Generic
let capitals = Dictionary<string, string>()
capitals.["Great Britain"] <- "London"
capitals.["France"] <- "Paris"
capitals.ContainsKey("France")
En este caso, simplemente se trata de ejercitar el tipo Dictionary<K,V>, aunque se muestra cómo se especifican los genéricos en F# (mediante corchetes angulares, al igual que C#), cómo usar indizadores en F# (mediante la sintaxis de corchetes, de nuevo como en C#) y cómo ejecutar los métodos .NET (mediante el "punto" y paréntesis, igual que en C#). De hecho, la única novedad consiste en la asignación de valores mutables, la cual usa el operador de flecha hacia la izquierda. Esto es necesario porque F#, al igual que la mayoría de los lenguajes funcionales, reserva el uso del operador Es igual a para la comparación, de acuerdo con la noción matemática de si x = y, entonces x e y son del mismo valor, no que y se asigna a x. Se dice que los verdaderos matemáticos arrugan la nariz o incluso ríen histéricamente ante la idea de que x = x + 1 pudiera ser cierto en cualquier universo verdadero o imaginado.
F# también puede crear objetos
Por supuesto, no todos los desarrolladores de .NET que prueban F# desean inmediatamente recibir con brazos abiertos los conceptos funcionales. De hecho, la mayoría de los desarrolladores que se pasan a F# procedentes de C# o Visual Basic necesitarán saber que pueden recaer en los hábitos de siempre sin destrozar el lenguaje. Y, hasta cierto punto, sí pueden.
Tomemos, por ejemplo, la definición de clase de vector bidimensional que se muestra en la parte superior de la figura 3. Esto nos sugiere algunas ideas interesantes. En primer lugar, observe que no hay cuerpo de constructor explícito; los parámetros de la primera línea indican los parámetros mediante los cuales se construyen instancias de Vector2D, que funcionará esencialmente como constructor. El identificador de longitud, junto con los identificadores dx y dy, pasan a ser así elementos privados dentro del tipo Vector2D, mientras que la palabra clave member indica los miembros que deben estar disponibles fuera de Vector2D, a través del acceso a la propiedad de .NET estándar. En esencia, este código de F# declara lo que se ve en la parte inferior de la figura 3 (tal como nos indica Reflector).

Figure 3 Variaciones de vectores en F# y C#
VECTOR2D IN F#
type Vector2D(dx:float,dy:float) =
let length = sqrt(dx*dx + dy*dy)
member obj.Length = length
member obj.DX = dx
member obj.DY = dy
member obj.Move(dx2,dy2) = Vector2D(dx+dx2,dy+dy2)
VECTOR2D IN C# (REFLECTOR>
[Serializable, CompilationMapping(SourceLevelConstruct.ObjectType)]
public class Vector2D
{
// Fields
internal double _dx@48;
internal double _dy@48;
internal double _length@49;
// Methods
public Vector2D(double dx, double dy)
{
Hello.Vector2D @this = this;
@this._dx@48 = dx;
@this._dy@48 = dy;
double d = (@this._dx@48 * @this._dx@48) +
(@this._dy@48 * @this._dy@48);
@this._length@49 = Math.Sqrt(d);
}
public Hello.Vector2D Move(double dx2, double dy2)
{
return new Hello.Vector2D(this._dx@48 + dx2, this._dy@48 + dy2);
}
// Properties
public double DX
{
get
{
return this._dx@48;
}
}
public double DY
{
get
{
return this._dy@48;
}
}
public double Length
{
get
{
return this._length@49;
}
}
}
Recuerde que F#, al igual que la mayoría de los lenguajes funcionales, promueve los valores y el estado inmutables. Esto debe ser obvio si observamos el código de la figura 3, ya que todas las propiedades son de sólo lectura, y el miembro Move no modifica el Vector2D existente, sino que crea uno nuevo a partir del Vector2D actual y le aplica los valores de modificación antes de devolverlo.
Observe también que la versión de F# no sólo protege totalmente los subprocesos, sino que permite totalmente el acceso desde código tradicional de C# o Visual Basic. Esto representa una manera fácil de familiarizarse con F#: usarlo para definir objetos de negocio o de otros tipos que desean o necesitan proteger los subprocesos y ser inmutables. Y, si bien es ciertamente posible crear tipos en F# que proporcionen el conjunto habitual de operaciones mutables (establecer propiedades, por ejemplo), ello requiere más trabajo y el uso de la palabra clave mutable. En un mundo donde las preocupaciones por la simultaneidad pasan a estar a la orden del día, es así exactamente como muchos afirman que debería ser: inmutable de forma predeterminada, y mutable cuando sea necesario o se desee.
Crear tipos en F# es interesante; sin embargo, también es posible usar F# para hacer lo mismo que con el código tradicional de C# o Visual Basic, como por ejemplo crear una aplicación sencilla de Windows Forms y reunir la entrada del usuario, como se muestra en la figura 4.

Figure 4 Windows Forms con F#
#light
open System
open System.IO
open System.Windows.Forms
open Printf
let form = new Form(Text="My First F# Form", Visible=true)
let menu = form.Menu <- new MainMenu()
let mnuFile = form.Menu.MenuItems.Add("&File")
let filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*"
let mnuiOpen =
new MenuItem("&Open...",
new EventHandler(fun _ _ ->
let dialog =
new OpenFileDialog(InitialDirectory="c:\\",
Filter=filter;
FilterIndex=2,
RestoreDirectory=true)
if dialog.ShowDialog() = DialogResult.OK then
match dialog.OpenFile() with
| null -> printf "Could not read the file...\n"
| s ->
let r = new StreamReader(s)
printf "First line is: %s!\n" (r.ReadLine());
s.Close();
),
Shortcut.CtrlO)
mnuFile.MenuItems.Add(mnuiOpen)
[<STAThread>]
do Application.Run(form)
Cualquier desarrollador familiarizado con Windows Forms podrá reconocer con relativa rapidez lo que sucede en este ejemplo: se ha creado un formulario sencillo, se han rellenado algunas propiedades y un controlador de eventos, y se ha indicado a la aplicación que se ejecute hasta que el usuario haga clic en el botón Close de la esquina superior derecha. Los elementos estándar como las aplicaciones .NET permiten centrar totalmente la atención en la sintaxis de F#.
La instrucción open funciona de manera muy similar a la instrucción using de C#, ya que abre básicamente un espacio de nombres de .NET para su uso sin calificadores formales. El espacio de nombres Printf es original de F#, y constituye técnicamente un puerto del módulo OCaml del mismo nombre. F# se proporciona no sólo con fidelidad absoluta a la biblioteca de clases de .NET Framework, sino también con un puerto en su mayor parte sencillo y directo de las bibliotecas de OCaml, lo que permite a los programadores que conocen ese lenguaje sentirse más cómodos con .NET Framework. Para los curiosos, Printf reside en el ensamblado FSharp.Core.dll. Nada le impide usar System.Console.WriteLine; se trata de un asunto puramente de preferencia estética.
La creación del identificador de formularios hace uso de los parámetros con nombre de F#, lo que equivale a crear una instancia del objeto y, a continuación, elaborar una serie de llamadas a conjuntos de propiedades para rellenar esas propiedades con valores. Hago lo mismo con el diálogo que el identificador crea unas pocas líneas más abajo.
La definición del identificador mnuiOpen contiene una construcción interesante, que no resultará demasiado extraña para los desarrolladores familiarizados con los delegados anónimos de .NET Framework 2.0 o las expresiones lambda de .NET Framework 3.5. En la construcción del EventHandler asociado con el MenuItem Open puede observarse una función anónima, definida mediante la siguiente sintaxis:
Al igual que los delegados anónimos, se crea una función que se invocará cuando se seleccione el elemento de menú, aunque la sintaxis es un poco complicada.
La definición de la parte de EventHandler de la definición de MenuItem es una función anónima que omite los dos parámetros transmitidos, los cuales corresponden exactamente a los argumentos de emisor y evento del tipo de delegado EventHandler estándar. La función establece que debe mostrarse un OpenFileDialog nuevo y que, al hacer clic en OK, se examinen los resultados... de algún modo:
if dialog.ShowDialog() = DialogResult.OK then
match dialog.OpenFile() with
| null -> printf "Could not read the file...\n"
| s ->
let r = new StreamReader(s) in
printf "First line is: %s!\n" (r.ReadLine());
s.Close();
Los resultados se examinan mediante la coincidencia de patrones, una característica eficaz del mundo de los lenguajes funcionales. Muy similar en cierto modo a una estructura Switch Case de C#, la coincidencia de patrones hace básicamente lo que su nombre implica: compara un valor con una serie de patrones distintos (no todos ellos deben ser valores constantes) y ejecuta el bloque de código coincidente. Así, por ejemplo, en el bloque coincidente que se muestra, el resultado de OpenFile se hace coincidir con dos posibles valores: NULL, lo que significa que no se ha podido abrir ningún archivo, o s, al que se asigna cualquier valor distinto de NULL y se usa a continuación como constructor para StreamReader para abrir y leer la primera línea del archivo de texto en cuestión.
La coincidencia de patrones es una parte fundamental de la mayoría de los lenguajes funcionales, por lo que merece una explicación un poco más detallada. Uno de los usos más comunes es junto con un tipo de unión discriminada, que recuerda vagamente a un tipo enumerado de C# o Visual Basic:
// Declaration of the 'Expr' type
type Expr =
| Binary of string * Expr * Expr
| Variable of string
| Constant of int
// Create a value 'v' representing 'x + 10'
let v = Binary("+", Variable "x", Constant 10)
Este uso es común en lenguajes funcionales para crear representaciones principales de lenguajes específicos de dominio que permiten a los desarrolladores escribir construcciones más complicadas y eficaces. Por ejemplo, no es difícil imaginar una ampliación de esta sintaxis para crear un lenguaje informático completo que pueda seguir ampliándose fácilmente mediante la adición de nuevos elementos al tipo Expr. Sin embargo, hay algo que es preciso tener presente: la sintaxis que usa el carácter * no indica que usamos la multiplicación, sino que se trata de un método estándar entre los lenguajes funcionales para indicar un tipo formado por varias partes.
De hecho, los lenguajes funcionales se usan generalmente para escribir herramientas de programación orientadas al lenguaje como intérpretes y compiladores, en los que el tipo Expr puede llegar a ser un conjunto completo de tipos de expresión de lenguaje. F# simplifica aún más este proceso mediante la inclusión de dos herramientas, fslex y fsyacc, diseñadas específicamente para tomar las entradas tradicionales del lenguaje (los archivos lex y yacc) y compilarlas en código de F# para permitir una manipulación más sencilla. Descargue el instalador de F# para explorar estas herramientas si tiene interés; en particular, el ejemplo de Análisis en la distribución estándar de F# ofrecerá una estructura básica agradable con la que empezar a familiarizarse.
La unión discriminada es sólo una de las ventajas de la coincidencia de patrones; la segunda es la ejecución de las expresiones, que podrá comprobar en la figura 5. El rec en la definición de eval es necesario para indicarle al compilador de F# que eval será invocado de forma recursiva en el cuerpo de la definición. Sin ello, F# estará esperando que exista una función anidada local de nombre eval. Hago uso de la función getVarValue para devolver algunos valores predefinidos para las variables; en una aplicación del mundo real, getVarValue probablemente examinaría un Diccionario para consultar los valores que debe devolver, tal como se ha establecido en el momento de crear la variable.

Figure 5 Ejecución de expresiones
let getVarValue v =
match v with
| "x" -> 25
| "y" -> 12
| _ -> 0
let rec eval x =
match x with
| Binary(op, l, r) ->
let (lv, rv) = (eval l, eval r) in
if (op = "+") then lv + rv
elif (op = "-") then lv - rv
else failwith "E_UNSUPPORTED"
| Variable(var) ->
getVarValue var
| Constant(n) ->
n
do printf "Results = %d\n" (eval v)
Cuando se llama a eval, éste toma el valor v y descubre que se trata de un valor binario. Esto coincide con la primera subexpresión, que a su vez enlaza el valor (lv, rv) con los resultados evaluados de las partes derecha e izquierda del valor binario examinado. El valor sin nombre (lv, rv) es una tupla, básicamente un valor único formado por varias partes, al igual que un conjunto relacional o C struct.
Cuando se llama primero a eval l, l de la instancia binaria resulta ser un tipo variable, así que la llamada recursiva a eval coincide con esa rama del bloque de coincidencia de patrones. Ésta a su vez llama a getVarValue, la cual devuelve el 25 codificado, que está en última instancia enlazado con el valor lv. La misma secuencia se ejecuta para r, que corresponde a una constante que contiene el valor 10, enlazado así con rv. Se ejecuta a continuación el resto del bloque, un bloque if/else-if/else, que resulta bastante fácil de leer para un desarrollador familiarizado con C#, Visual Basic o C++.
El aspecto clave que hay que reconocer en este ejemplo es que, una vez más, cada expresión devuelve un valor, aunque se encuentre dentro del bloque de coincidencia de patrones. En este caso, el valor devuelto es un valor entero, ya sea el valor de la operación, el valor recuperado de la variable o la propia constante. Este punto en concreto, más de cualquier otro, parece confundir a los desarrolladores más acostumbrados a la programación orientada a objetos o imperativa desde que, en C#, Visual Basic o C++, tener valores devueltos es opcional y puede omitirse aunque se especifique. En los lenguajes funcionales como F#, para omitir un valor devuelto es necesario un lenguaje de codificación explícito. En estos casos, los programadores pueden enviar los resultados a una función denominada ignore, que hace exactamente lo que su nombre indica.
F# asincrónico
Hasta el momento, mi presentación de la sintaxis de F# cubría dos formas distintas: trabajar con construcciones funcionales bastante sencillas o hacer que parezca una variación extraña y más concisa de los lenguajes tradicionales orientados a objetos compatibles con .NET (C#, Visual Basic o C++/CLI). Ambos difícilmente constituirán un argumento lo suficientemente atractivo como para adoptar F# en la empresa.
Pero observemos la figura 6. Este ejemplo definitivamente no se ajusta a ninguna de las dos categorías mencionadas anteriormente. Excepto los caracteres ! que aparecen en algunos puntos y el uso del modificador async, tiene el aspecto de ser un código relativamente sencillo: carga de una imagen gráfica de origen, extracción de sus datos, transmisión de los datos a una función independiente para su manipulación (rotación, sesgado o cualquier otra función) y nueva escritura de los datos en un archivo de salida.

Figure 6 Manipulación de una imagen
let TransformImage pixels i =
// Some kind of graphic manipulation of images
let ProcessImage(i) =
async { use inStream = File.OpenRead(sprintf "source%d.jpg" i)
let! pixels = inStream.ReadAsync(1024*1024)
let pixels' = TransformImage(pixels,i)
use outStream = File.OpenWrite(sprintf "result%d.jpg" i)
do! outStream.WriteAsync(pixels')
do Console.WriteLine "done!" }
let ProcessImages() =
Async.Run (Async.Parallel
[ for i in 1 .. numImages -> ProcessImage(i) ])
Lo que no resulta tan obvio es que el uso del modificador async convierte a este código en lo que F# llama un flujo de trabajo asincrónico (no guarda relación alguna con Windows Workflow Foundation), lo que significa que los diferentes pasos de carga/proceso/almacenamiento se producen en subprocesos paralelos procedentes de un grupo de subprocesos de .NET.
Para que resulte más sencillo de entender, tomemos el código de la figura 7. Esta secuencia concreta incluye flujos de trabajo async pero de un modo bastante sencillo y fácil de digerir. Sin entrar demasiado en detalles, evals es una matriz de funciones en espera de ser ejecutadas, cada una de las cuales se pone en cola para su ejecución en el grupo de subprocesos por medio de la llamada a Async.Parallel. Cuando se ejecuta, se hace patente que las funciones de evals se encuentran, de hecho, en un subproceso independiente de la función en awr aunque, debido a la naturaleza del grupo de subprocesos de sistema de .NET, alguna o todas las funciones de evals pueden ser ejecutadas en el mismo subproceso.

Figure 7 Ejecución asincrónica de funciones
#light
open System.Threading
let printWithThread str =
printfn "[ThreadId = %d] %s" Thread.CurrentThread.ManagedThreadId str
let evals =
let z = 4.0
[ async { do printWithThread "Computing z*z\n"
return z * z };
async { do printWithThread "Computing sin(z)\n"
return (sin z) };
async { do printWithThread "Computing log(z)\n"
return (log z) } ]
let awr =
async { let! vs = Async.Parallel evals
do printWithThread "Computing v1+v2+v3\n"
return (Array.fold_left (fun a b -> a + b) 0.0 vs) }
let R = Async.Run awr
printf "Result = %f\n" R
El hecho de que se ejecuten fuera del grupo de subprocesos de .NET destaca una vez más el nivel de interoperabilidad del lenguaje F# con el tiempo de ejecución subyacente. Su dependencia de la biblioteca de clases de .NET Framework incluso en áreas reservadas tradicionalmente para la implementación especializada, como por ejemplo la creación de subprocesos, en lenguajes funcionales significa que el programador de C# puede usar las bibliotecas o módulos de F# del mismo modo que los desarrolladores de F# hacen uso de las bibliotecas de C#. De hecho, en el futuro, características de F# como las tareas de async podrán beneficiarse de las nuevas bibliotecas de .NET Framework como la biblioteca de procesamiento de tareas de la biblioteca de extensiones paralelas.
Compatibilidad con F#
Como imaginará, hay mucho más por decir acerca del lenguaje F# que lo que se pueda cubrir en un solo artículo; de hecho, entre la nueva sintaxis y la manera de pensar totalmente nueva (funcional frente a imperativa), es justo decir que, para el desarrollador medio orientado a objetos acostumbrado a C# o Visual Basic, dominar F# le llevará algún tiempo. Afortunadamente, F# posee aún total interoperabilidad con el resto del ecosistema de .NET, lo que significa que puede aprovechar gran parte de los conocimientos de que dispone y numerosas herramientas existentes para ayudarle a incorporar F# en su arsenal de codificación.
El desarrollador de F# dispone de acceso a todas las bibliotecas básicas; asimismo, desde que F# admite ciertos aspectos del desarrollo imperativo y por objetos, resulta totalmente factible la idea de usar el modo interactivo de F# como una manera de aprender tanto su sintaxis como los detalles de Windows Presentation Foundation, Windows Communication Foundation o Windows Workflow Foundation sin tener que detenerse para compilar ciclos.
Como ya hemos mencionado, los desarrolladores pueden escribir objetos de negocios en F# para que las usen otras partes de su código de aplicación. Dado que la construcción de tipos de F# produce clases que son, en su mayor parte, idénticas a sus equivalentes en C# o Visual Basic, las bibliotecas de persistencia como NHibernate resistirán sin problemas los tipos de F#, lo que contribuirá a una introducción óptima de F# en las aplicaciones de negocios en funcionamiento.
Aprender F# le ayudará, entre otros aspectos, a comprender muchas de las características de versiones futuras de C# y Visual Basic, ya que muchas de esas ideas y conceptos, incluidos los genéricos, los iteradores (la palabra clave yield en C#) y LINQ tienen orígenes funcionales y deben su existencia a la labor de investigación realizada por el equipo de F#. Independientemente del enfoque que quiera darle, la programación funcional ha llegado y parece que para quedarse bastante tiempo.
Ted Neward es consultor independiente especializado en sistemas empresariales a gran escala. Es autor y coautor de varios libros, arquitecto MVP de Microsoft, director técnico de BEA, portavoz de INETA e instructor de PluralSight. Póngase en contacto con Ted mediante la dirección
ted@tedneward.com o visite su blog en
blogs.tedneward.com.