summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDivay Prakash <divayprakash@users.noreply.github.com>2018-09-19 12:24:26 +0530
committerGitHub <noreply@github.com>2018-09-19 12:24:26 +0530
commit42a4294151d0f86069ed01e5d34f52dc450a8d0d (patch)
treecd1355ac75405c6c6d52b3719ad391ed75891e0f
parenta7cb341a7ab073999bc7baeabb7d98565fb01667 (diff)
parentaa51a89522709ec7b824fbf11f896994d2d783a0 (diff)
Merge pull request #3220 from AngelsProjects/master
Add F# tutorial in Spanish
-rw-r--r--es-es/fsharp-es.html.markdown629
1 files changed, 629 insertions, 0 deletions
diff --git a/es-es/fsharp-es.html.markdown b/es-es/fsharp-es.html.markdown
new file mode 100644
index 00000000..b7f80c44
--- /dev/null
+++ b/es-es/fsharp-es.html.markdown
@@ -0,0 +1,629 @@
+---
+language: F#
+lang: es-es
+contributors:
+ - ['Scott Wlaschin', 'http://fsharpforfunandprofit.com/']
+translators:
+ - ['Angel Arciniega', 'https://github.com/AngelsProjects']
+filename: learnfsharp-es.fs
+---
+
+F# es un lenguaje de programación funcional y orientado a objetos. Es gratis y su código fuente está abierto. Se ejecuta en Linux, Mac, Windows y más.
+
+Tiene un poderoso sistema de tipado que atrapa muchos errores de tiempo de compilación, pero usa inferencias de tipados que le permiten ser leídos como un lenguaje dinámico.
+
+La sintaxis de F# es diferente de los lenguajes que heredan de C.
+
+- Las llaves no se usan para delimitar bloques de código. En cambio, se usa sangría (como en Python).
+- Los espacios se usan para separar parámetros en lugar de comas.
+
+Si quiere probar el siguiente código, puede ir a [tryfsharp.org](http://www.tryfsharp.org/Create) y pegarlo en [REPL](https://es.wikipedia.org/wiki/REPL).
+
+```fsharp
+// Los comentarios de una línea se escibren con una doble diagonal
+(* Los comentarios multilínea usan parentesis (* . . . *)
+
+-final del comentario multilínea- *)
+
+// ================================================
+// Syntaxis básica
+// ================================================
+
+// ------ "Variables" (pero no realmente) ------
+// La palabra reservada "let" define un valor (inmutable)
+let miEntero = 5
+let miFlotante = 3.14
+let miCadena = "hola" // Tenga en cuenta que no es necesario ningún tipado
+
+// ------ Listas ------
+let dosACinco = [2;3;4;5] // Los corchetes crean una lista con
+ // punto y coma para delimitadores.
+let unoACinco = 1 :: dosACinco // :: Crea una lista con un nuevo elemento
+// El resultado es [1;2;3;4;5]
+let ceroACinco = [0;1] @ dosACinco // @ Concatena dos listas
+
+// IMPORTANTE: las comas no se usan para delimitar,
+// solo punto y coma !
+
+// ------ Funciones ------
+// La palabra reservada "let" también define el nombre de una función.
+let cuadrado x = x * x // Tenga en cuenta que no se usa paréntesis.
+cuadrado 3 // Ahora, ejecutemos la función.
+ // De nuevo, sin paréntesis.
+
+let agregar x y = x + y // ¡No use add (x, y)! Eso significa
+ // algo completamente diferente.
+agregar 2 3 // Ahora, ejecutemos la función.
+
+// Para definir una función en varias líneas, usemos la sangría.
+// Los puntos y coma no son necesarios.
+let pares lista =
+ let esPar x = x%2 = 0 // Establece "esPar" como una función anidada
+ List.filter esPar lista // List.filter es una función de la biblioteca
+ // dos parámetros: una función que devuelve un
+ // booleano y una lista en la que trabajar
+
+pares unoACinco // Ahora, ejecutemos la función.
+
+// Puedes usar paréntesis para aclarar.
+// En este ejemplo, "map" se ejecuta primero, con dos argumentos,
+// entonces "sum" se ejecuta en el resultado.
+// Sin los paréntesis, "List.map" se pasará como argumento a List.sum.
+let sumaDeCuadradosHasta100 =
+ List.sum ( List.map cuadrado [1..100] )
+
+// Puedes redirigir la salida de una función a otra con "|>"
+// Redirigir datos es muy común en F#, como con los pipes de UNIX.
+
+// Aquí está la misma función sumOfSquares escrita usando pipes
+let sumaDeCuadradosHasta100piped =
+ [1..100] |> List.map cuadrado |> List.sum // "cuadrado" se declara antes
+
+// Puede definir lambdas (funciones anónimas) gracias a la palabra clave "fun"
+let sumaDeCuadradosHasta100ConFuncion =
+ [1..100] |> List.map (fun x -> x*x) |> List.sum
+
+// En F#, no hay palabra clave "return". Una función siempre regresa
+// el valor de la última expresión utilizada.
+
+// ------ Coincidencia de patrones ------
+// Match..with .. es una sobrecarga de la condición de case/ switch.
+let coincidenciaDePatronSimple =
+ let x = "a"
+ match x with
+ | "a" -> printfn "x es a"
+ | "b" -> printfn "x es b"
+ | _ -> printfn "x es algo mas" // guion bajo corresponde con todos los demás
+
+// F# no permite valores nulos por defecto - debe usar el tipado de Option
+// y luego coincide con el patrón.
+// Some(..) y None son aproximadamente análogos a los envoltorios Nullable
+let valorValido = Some(99)
+let valorInvalido = None
+
+// En este ejemplo, match..with encuentra una coincidencia con "Some" y "None",
+// y muestra el valor de "Some" al mismo tiempo.
+let coincidenciaDePatronDeOpciones entrada =
+ match entrada with
+ | Some i -> printfn "la entrada es un int=%d" i
+ | None -> printfn "entrada faltante"
+
+coincidenciaDePatronDeOpciones validValue
+coincidenciaDePatronDeOpciones invalidValue
+
+// ------ Viendo ------
+// Las funciones printf/printfn son similares a las funciones
+// Console.Write/WriteLine de C#.
+printfn "Imprimiendo un int %i, a float %f, a bool %b" 1 2.0 true
+printfn "Un string %s, y algo generico %A" "hola" [1;2;3;4]
+
+// También hay funciones printf/sprintfn para formatear datos
+// en cadena. Es similar al String.Format de C#.
+
+// ================================================
+// Mas sobre funciones
+// ================================================
+
+// F# es un verdadero lenguaje funcional - las funciones son
+// entidades de primer nivel y se pueden combinar fácilmente
+// para crear construcciones poderosas
+
+// Los módulos se utilizan para agrupar funciones juntas.
+// Se requiere sangría para cada módulo anidado.
+module EjemploDeFuncion =
+
+ // define una función de suma simple
+ let agregar x y = x + y
+
+ // uso básico de una función
+ let a = agregar 1 2
+ printfn "1+2 = %i" a
+
+ // aplicación parcial para "hornear en" los parámetros (?)
+ let agregar42 = agregar 42
+ let b = agregar42 1
+ printfn "42+1 = %i" b
+
+ // composición para combinar funciones
+ let agregar1 = agregar 1
+ let agregar2 = agregar 2
+ let agregar3 = agregar1 >> agregar2
+ let c = agregar3 7
+ printfn "3+7 = %i" c
+
+ // funciones de primer nivel
+ [1..10] |> List.map agregar3 |> printfn "la nueva lista es %A"
+
+ // listas de funciones y más
+ let agregar6 = [agregar1; agregar2; agregar3] |> List.reduce (>>)
+ let d = agregar6 7
+ printfn "1+2+3+7 = %i" d
+
+// ================================================
+// Lista de colecciones
+// ================================================
+
+// Il y a trois types de collection ordonnée :
+// * Les listes sont les collections immutables les plus basiques
+// * Les tableaux sont mutables et plus efficients
+// * Les séquences sont lazy et infinies (e.g. un enumerator)
+//
+// Des autres collections incluent des maps immutables et des sets
+// plus toutes les collections de .NET
+
+module EjemplosDeLista =
+
+ // las listas utilizan corchetes
+ let lista1 = ["a";"b"]
+ let lista2 = "c" :: lista1 // :: para una adición al principio
+ let lista3 = lista1 @ lista2 // @ para la concatenación
+
+ // Lista de comprensión (alias generadores)
+ let cuadrados = [for i in 1..10 do yield i*i]
+
+ // Generador de números primos
+ let rec tamiz = function
+ | (p::xs) -> p :: tamiz [ for x in xs do if x % p > 0 then yield x ]
+ | [] -> []
+ let primos = tamiz [2..50]
+ printfn "%A" primos
+
+ // coincidencia de patrones para listas
+ let listaDeCoincidencias unaLista =
+ match unaLista with
+ | [] -> printfn "la lista esta vacia"
+ | [primero] -> printfn "la lista tiene un elemento %A " primero
+ | [primero; segundo] -> printfn "la lista es %A y %A" primero segundo
+ | _ -> printfn "la lista tiene mas de dos elementos"
+
+ listaDeCoincidencias [1;2;3;4]
+ listaDeCoincidencias [1;2]
+ listaDeCoincidencias [1]
+ listaDeCoincidencias []
+
+ // Récursion en utilisant les listes
+ let rec suma unaLista =
+ match unaLista with
+ | [] -> 0
+ | x::xs -> x + suma xs
+ suma [1..10]
+
+ // -----------------------------------------
+ // Funciones de la biblioteca estándar
+ // -----------------------------------------
+
+ // mapeo
+ let agregar3 x = x + 3
+ [1..10] |> List.map agregar3
+
+ // filtrado
+ let par x = x % 2 = 0
+ [1..10] |> List.filter par
+
+ // mucho más - consulte la documentación
+
+module EjemploDeArreglo =
+
+ // los arreglos usan corchetes con barras.
+ let arreglo1 = [| "a";"b" |]
+ let primero = arreglo1.[0] // se accede al índice usando un punto
+
+ // la coincidencia de patrones de los arreglos es la misma que la de las listas
+ let coincidenciaDeArreglos una Lista =
+ match unaLista with
+ | [| |] -> printfn "la matriz esta vacia"
+ | [| primero |] -> printfn "el arreglo tiene un elemento %A " primero
+ | [| primero; second |] -> printfn "el arreglo es %A y %A" primero segundo
+ | _ -> printfn "el arreglo tiene mas de dos elementos"
+
+ coincidenciaDeArreglos [| 1;2;3;4 |]
+
+ // La biblioteca estándar funciona como listas
+ [| 1..10 |]
+ |> Array.map (fun i -> i+3)
+ |> Array.filter (fun i -> i%2 = 0)
+ |> Array.iter (printfn "el valor es %i. ")
+
+module EjemploDeSecuencia =
+
+ // Las secuencias usan llaves
+ let secuencia1 = seq { yield "a"; yield "b" }
+
+ // Las secuencias pueden usar yield y
+    // puede contener subsecuencias
+ let extranio = seq {
+ // "yield" agrega un elemento
+ yield 1; yield 2;
+
+ // "yield!" agrega una subsecuencia completa
+ yield! [5..10]
+ yield! seq {
+ for i in 1..10 do
+ if i%2 = 0 then yield i }}
+ // prueba
+ extranio |> Seq.toList
+
+ // Las secuencias se pueden crear usando "unfold"
+    // Esta es la secuencia de fibonacci
+ let fib = Seq.unfold (fun (fst,snd) ->
+ Some(fst + snd, (snd, fst + snd))) (0,1)
+
+ // prueba
+ let fib10 = fib |> Seq.take 10 |> Seq.toList
+ printf "Los primeros 10 fib son %A" fib10
+
+// ================================================
+// Tipos de datos
+// ================================================
+
+module EejemploDeTipoDeDatos =
+
+ // Todos los datos son inmutables por defecto
+
+     // las tuplas son tipos anónimos simples y rápidos
+     // - Usamos una coma para crear una tupla
+ let dosTuplas = 1,2
+ let tresTuplas = "a",2,true
+
+ // Combinación de patrones para desempaquetar
+ let x,y = dosTuplas // asignado x=1 y=2
+
+ // ------------------------------------
+ // Los tipos de registro tienen campos con nombre
+ // ------------------------------------
+
+ // Usamos "type" con llaves para definir un tipo de registro
+ type Persona = {Nombre:string; Apellido:string}
+
+ // Usamos "let" con llaves para crear un registro
+ let persona1 = {Nombre="John"; Apellido="Doe"}
+
+ // Combinación de patrones para desempaquetar
+ let {Nombre=nombre} = persona1 // asignado nombre="john"
+
+ // ------------------------------------
+ // Los tipos de unión (o variantes) tienen un conjunto de elección
+     // Solo un caso puede ser válido a la vez.
+ // ------------------------------------
+
+ // Usamos "type" con barra/pipe para definir una unión estándar
+ type Temp =
+ | GradosC of float
+ | GradosF of float
+
+ // Una de estas opciones se usa para crear una
+ let temp1 = GradosF 98.6
+ let temp2 = GradosC 37.0
+
+ // Coincidencia de patrón en todos los casos para desempaquetar (?)
+ let imprimirTemp = function
+ | GradosC t -> printfn "%f gradC" t
+ | GradosF t -> printfn "%f gradF" t
+
+ imprimirTemp temp1
+ imprimirTemp temp2
+
+ // ------------------------------------
+ // Tipos recursivos
+ // ------------------------------------
+
+ // Los tipos se pueden combinar recursivamente de formas complejas
+    // sin tener que crear subclases
+ type Empleado =
+ | Trabajador of Persona
+ | Gerente of Empleado lista
+
+ let jdoe = {Nombre="John";Apellido="Doe"}
+ let trabajador = Trabajador jdoe
+
+ // ------------------------------------
+ // Modelado con tipados (?)
+ // ------------------------------------
+
+ // Los tipos de unión son excelentes para modelar el estado sin usar banderas (?)
+ type DireccionDeCorreo =
+ | DireccionDeCorreoValido of string
+ | DireccionDeCorreoInvalido of string
+
+ let intentarEnviarCorreo correoElectronico =
+ match correoElectronico with // uso de patrones de coincidencia
+ | DireccionDeCorreoValido direccion -> () // enviar
+ | DireccionDeCorreoInvalido direccion -> () // no enviar
+
+ // Combinar juntos, los tipos de unión y tipos de registro
+     // ofrece una base excelente para el diseño impulsado por el dominio.
+     // Puedes crear cientos de pequeños tipos que reflejarán fielmente
+     // el dominio.
+
+ type ArticuloDelCarrito = { CodigoDelProducto: string; Cantidad: int }
+ type Pago = Pago of float
+ type DatosActivosDelCarrito = { ArticulosSinPagar: ArticuloDelCarrito lista }
+ type DatosPagadosDelCarrito = { ArticulosPagados: ArticuloDelCarrito lista; Pago: Pago}
+
+ type CarritoDeCompras =
+ | CarritoVacio // sin datos
+ | CarritoActivo of DatosActivosDelCarrito
+ | CarritoPagado of DatosPagadosDelCarrito
+
+ // ------------------------------------
+ // Comportamiento nativo de los tipos
+ // ------------------------------------
+
+ // Los tipos nativos tienen el comportamiento más útil "listo para usar", sin ningún código para agregar.
+     // * Inmutabilidad
+     // * Bonita depuración de impresión
+     // * Igualdad y comparación
+     // * Serialización
+
+     // La impresión bonita se usa con %A
+ printfn "dosTuplas=%A,\nPersona=%A,\nTemp=%A,\nEmpleado=%A"
+ dosTuplas persona1 temp1 trabajador
+
+ // La igualdad y la comparación son innatas
+     // Aquí hay un ejemplo con tarjetas.
+ type JuegoDeCartas = Trebol | Diamante | Espada | Corazon
+ type Rango = Dos | Tres | Cuatro | Cinco | Seis | Siete | Ocho
+ | Nueve | Diez | Jack | Reina | Rey | As
+
+ let mano = [ Trebol,As; Corazon,Tres; Corazon,As;
+ Espada,Jack; Diamante,Dos; Diamante,As ]
+
+ // orden
+ List.sort mano |> printfn "la mano ordenada es (de menos a mayor) %A"
+ List.max mano |> printfn "la carta más alta es%A"
+ List.min mano |> printfn "la carta más baja es %A"
+
+// ================================================
+// Patrones activos
+// ================================================
+
+module EjemplosDePatronesActivos =
+
+ // F# tiene un tipo particular de coincidencia de patrón llamado "patrones activos"
+    // donde el patrón puede ser analizado o detectado dinámicamente.
+
+    // "clips de banana" es la sintaxis de los patrones activos
+
+    // por ejemplo, definimos un patrón "activo" para que coincida con los tipos de "caracteres" ...
+ let (|Digito|Latra|EspacioEnBlanco|Otros|) ch =
+ if System.Char.IsDigit(ch) then Digito
+ else if System.Char.IsLetter(ch) then Letra
+ else if System.Char.IsWhiteSpace(ch) then EspacioEnBlanco
+ else Otros
+
+ // ... y luego lo usamos para hacer que la lógica de análisis sea más clara
+ let ImprimirCaracter ch =
+ match ch with
+ | Digito -> printfn "%c es un Digito" ch
+ | Letra -> printfn "%c es una Letra" ch
+ | Whitespace -> printfn "%c es un Espacio en blanco" ch
+ | _ -> printfn "%c es algo mas" ch
+
+ // ver una lista
+ ['a';'b';'1';' ';'-';'c'] |> List.iter ImprimirCaracter
+
+ // -----------------------------------------
+ // FizzBuzz usando patrones activos
+ // -----------------------------------------
+
+ // Puede crear un patrón de coincidencia parcial también
+    // Solo usamos un guión bajo en la definición y devolvemos Some si coincide.
+ let (|MultDe3|_|) i = if i % 3 = 0 then Some MultDe3 else None
+ let (|MultDe5|_|) i = if i % 5 = 0 then Some MultDe5 else None
+
+ // la función principal
+ let fizzBuzz i =
+ match i with
+ | MultDe3 & MultDe5 -> printf "FizzBuzz, "
+ | MultDe3 -> printf "Fizz, "
+ | MultDe5 -> printf "Buzz, "
+ | _ -> printf "%i, " i
+
+ // prueba
+ [1..20] |> List.iter fizzBuzz
+
+// ================================================
+// concisión
+// ================================================
+
+module EjemploDeAlgoritmo =
+
+ // F# tiene una alta relación señal / ruido, lo que permite leer el código
+    // casi como un algoritmo real
+
+ // ------ Ejemplo: definir una función sumaDeCuadrados ------
+ let sumaDeCuadrados n =
+ [1..n] // 1) Tome todos los números del 1 al n
+ |> List.map cuadrado // 2) Elevar cada uno de ellos al cuadrado
+ |> List.sum // 3) Realiza su suma
+
+ // prueba
+ sumaDeCuadrados 100 |> printfn "Suma de cuadrados = %A"
+
+ // ------ Ejemplo: definir una función de ordenación ------
+ let rec ordenar lista =
+ match lista with
+ // Si la lista está vacía
+ | [] ->
+ [] // devolvemos una lista vacía
+       // si la lista no está vacía
+ | primerElemento::otrosElementos -> // tomamos el primer elemento
+ let elementosMasPequenios = // extraemos los elementos más pequeños
+ otrosElementos // tomamos el resto
+ |> List.filter (fun e -> e < primerElemento)
+ |> ordenar // y los ordenamos
+ let elementosMasGrandes = // extraemos el mas grande
+ otrosElementos // de los que permanecen
+ |> List.filter (fun e -> e >= primerElemento)
+ |> ordenar // y los ordenamos
+ // Combinamos las 3 piezas en una nueva lista que devolvemos
+ List.concat [elementosMasPequenios; [primerElemento]; elementosMasGrandes]
+
+ // prueba
+ ordenar [1;5;23;18;9;1;3] |> printfn "Ordenado = %A"
+
+// ================================================
+// Código asíncrono
+// ================================================
+
+module AsyncExample =
+
+ // F# incluye características para ayudar con el código asíncrono
+    // sin conocer la "pirámide del destino"
+    //
+    // El siguiente ejemplo descarga una secuencia de página web en paralelo.
+
+ open System.Net
+ open System
+ open System.IO
+ open Microsoft.FSharp.Control.CommonExtensions
+
+ // Recuperar el contenido de una URL de forma asincrónica
+ let extraerUrlAsync url =
+ async { // La palabra clave "async" y llaves
+ // crear un objeto "asincrónico"
+ let solicitud = WebRequest.Create(Uri(url))
+ use! respuesta = solicitud.AsyncGetResponse()
+ // use! es una tarea asincrónica
+ use flujoDeDatos = resp.GetResponseStream()
+ // "use" dispara automáticamente la funcion close()
+ // en los recursos al final de las llaves
+ use lector = new IO.StreamReader(flujoDeDatos)
+ let html = lector.ReadToEnd()
+ printfn "terminó la descarga %s" url
+ }
+
+ // una lista de sitios para informar
+ let sitios = ["http://www.bing.com";
+ "http://www.google.com";
+ "http://www.microsoft.com";
+ "http://www.amazon.com";
+ "http://www.yahoo.com"]
+
+ // ¡Aqui vamos!
+ sitios
+ |> List.map extraerUrlAsync // crear una lista de tareas asíncrona
+ |> Async.Parallel // decirle a las tareas que se desarrollan en paralelo
+ |> Async.RunSynchronously // ¡Empieza!
+
+// ================================================
+// Compatibilidad .NET
+// ================================================
+
+module EjemploCompatibilidadNet =
+
+ // F# puede hacer casi cualquier cosa que C# pueda hacer, y se ajusta
+    // perfectamente con bibliotecas .NET o Mono.
+
+  // ------- Trabaja con las funciones de las bibliotecas existentes -------
+
+ let (i1success,i1) = System.Int32.TryParse("123");
+ if i1success then printfn "convertido como %i" i1 else printfn "conversion fallida"
+
+ // ------- Implementar interfaces sobre la marcha! -------
+
+ // Crea un nuevo objeto que implemente IDisposable
+ let crearRecurso name =
+ { new System.IDisposable
+ with member this.Dispose() = printfn "%s creado" name }
+
+ let utilizarYDisponerDeRecursos =
+ use r1 = crearRecurso "primer recurso"
+ printfn "usando primer recurso"
+ for i in [1..3] do
+ let nombreDelRecurso = sprintf "\tinner resource %d" i
+ use temp = crearRecurso nombreDelRecurso
+ printfn "\thacer algo con %s" nombreDelRecurso
+ use r2 = crearRecurso "segundo recurso"
+ printfn "usando segundo recurso"
+ printfn "hecho."
+
+ // ------- Código orientado a objetos -------
+
+ // F# es también un verdadero lenguaje OO.
+    // Admite clases, herencia, métodos virtuales, etc.
+
+ // interfaz de tipo genérico
+ type IEnumerator<'a> =
+ abstract member Actual : 'a
+ abstract MoverSiguiente : unit -> bool
+
+ // Clase base abstracta con métodos virtuales
+ [<AbstractClass>]
+ type Figura() =
+ // propiedades de solo lectura
+ abstract member Ancho : int with get
+ abstract member Alto : int with get
+ // método no virtual
+ member this.AreaDelimitadora = this.Alto * this.Ancho
+ // método virtual con implementación de la clase base
+ abstract member Imprimir : unit -> unit
+ default this.Imprimir () = printfn "Soy una Figura"
+
+ // clase concreta que hereda de su clase base y sobrecarga
+ type Rectangulo(x:int, y:int) =
+ inherit Figura()
+ override this.Ancho = x
+ override this.Alto = y
+ override this.Imprimir () = printfn "Soy un Rectangulo"
+
+ // prueba
+ let r = Rectangulo(2,3)
+ printfn "La anchura es %i" r.Ancho
+ printfn "El area es %i" r.AreaDelimitadora
+ r.Imprimir()
+
+ // ------- extensión de método -------
+
+ // Al igual que en C#, F# puede extender las clases existentes con extensiones de método.
+ type System.String with
+ member this.EmpiezaConA = this.EmpiezaCon "A"
+
+ // prueba
+ let s = "Alice"
+ printfn "'%s' empieza con una 'A' = %A" s s.EmpiezaConA
+
+ // ------- eventos -------
+
+ type MiBoton() =
+ let eventoClick = new Event<_>()
+
+ [<CLIEvent>]
+ member this.AlHacerClick = eventoClick.Publish
+
+ member this.PruebaEvento(arg) =
+ eventoClick.Trigger(this, arg)
+
+ // prueba
+ let miBoton = new MiBoton()
+ miBoton.AlHacerClick.Add(fun (sender, arg) ->
+ printfn "Haga clic en el evento con arg=%O" arg)
+
+ miBoton.PruebaEvento("Hola Mundo!")
+```
+
+## Más información
+
+Para más demostraciones de F#, visite el sitio [Try F#](http://www.tryfsharp.org/Learn), o sigue la serie [why use F#](http://fsharpforfunandprofit.com/why-use-fsharp/).
+
+Aprenda más sobre F# en [fsharp.org](http://fsharp.org/).