2. Valores, funciones e inferencia de tipos
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Código: los scripts examples/l02_*.fsx, los fragmentos rechazados compile_fail/l02_*.fsx y la sesión sessions/l02_inference.txt.
let asocia un nombre a un valor
Sección titulada «let asocia un nombre a un valor»En C#, var strings = 6; declara una variable: puedes volver a asignarla más tarde. En F#, let strings = 6 da el nombre strings al valor 6, y es definitivo:
let strings = 6strings <- 7l02_immutable.fsx(2,1): error FS0027: This value is not mutable. Consider using the mutable keyword, e.g. 'let mutable strings = expression'.Un let se parece más a un campo readonly de C# o a una variable local final de Java, salvo que es lo predeterminado y que se aplica a valores de cualquier tipo. Cuando de verdad necesitas una variable, el compilador te dice cómo: let mutable y el operador de asignación <-.
// mutable permite la asignación, y <- asignalet mutable total = 0
for fret in 1..12 do total <- total + fret
printfn "Frets 1 to 12 add up to %d" totalFrets 1 to 12 add up to 78for fret in 1..12 do es el foreach de C# sobre el rango de 1 a 12, ambos incluidos. El F# idiomático rara vez necesita este bucle: la lección 5 lo sustituye por List.sum. mutable no es un error, pero es local y explícito, y destaca al leer el código.
= compara, <- asigna
Sección titulada «= compara, <- asigna»La trampa en la que cae todo desarrollador C# el primer día:
let restring () = let mutable strings = 6 strings = 7 // = compara: esta línea calcula false y lo descarta printfn "%d strings" strings
restring ()l02_equality_warning.fsx(3,5): warning FS0020: The result of this equality expression has type 'bool' and is implicitly discarded. Consider using 'let' to bind the result to a name, e.g. 'let result = expression'. If you intended to mutate a value, then use the '<-' operator e.g. 'strings <- expression'.
6 stringsEn F#, = dentro de una expresión es la igualdad, el == de C#. strings = 7 es false, un valor que la línea descarta; strings sigue valiendo 6. Solo es una advertencia, así que el script se ejecuta: trata FS0020 como un error.
Ocultamiento
Sección titulada «Ocultamiento»Dos let con el mismo nombre en el nivel superior de un script o de un módulo se rechazan:
let tuning = "E2 A2 D3 G3 B3 E4"let tuning = "D2 A2 D3 G3 B3 E4"printfn "%s" tuningl02_duplicate.fsx(2,5): error FS0037: Duplicate definition of value 'tuning'Dentro de una función, un nuevo let puede reutilizar un nombre: define un valor nuevo que oculta al anterior a partir de esa línea. No se modifica nada:
let label (name: string) = let name = name.Trim() // un valor nuevo que oculta el parámetro let name = name.ToUpperInvariant() // y otro más $"[{name}]"
printfn "%s" (label " e minor ")[E MINOR]C# rechaza una variable local que reutiliza el nombre de un parámetro; Rust permite el ocultamiento igual que F#.
El compilador infiere los tipos
Sección titulada «El compilador infiere los tipos»Esto es lo que responde F# Interactive para algunas definiciones (sessions/l02_inference.txt):
> let semitones = 7;;val semitones: int = 7
> let ratio = 2.0 ** (7.0 / 12.0);;val ratio: float = 1.498307077
> let name = "fifth";;val name: string = "fifth"
> let isPerfect = true;;val isPerfect: bool = true
> let add a b = a + b;;val add: a: int -> b: int -> int
> let addFloats (a: float) b = a + b;;val addFloats: a: float -> b: float -> float
> let twice f x = f (f x);;val twice: f: ('a -> 'a) -> x: 'a -> 'a
> let first (a, b) = a;;val first: a: 'a * b: 'b -> 'afloates el nombre F# deSystem.Double, eldoublede C#.intesSystem.Int32,stringesSystem.String.**es el operador de potencia (Math.Pow).- El
varde C# infiere el tipo de una variable local. F# infiere también los parámetros y los tipos de retorno.addno tiene anotación;+, sin más información, usaintpor defecto. - Basta una anotación para cambiarlo: en
addFloats,(a: float)convierte+en una suma de flotantes, así queby el resultado son flotantes. twicefunciona con cualquier tipo: F# la ha hecho genérica por sí solo.'aes un parámetro de tipo, elTde C#. El tipo se lee «toma una función de'aa'ay un valor'a, y devuelve un'a». En C#, escribiríasstatic T Twice<T>(Func<T, T> f, T x).firsttoma una tupla y devuelve su primer elemento, sean cuales sean los tipos:'a * 'b -> 'a. La lección 3 trata de las tuplas.
La inferencia va de arriba abajo y de izquierda a derecha, que es una razón más para el orden de los archivos de la lección 1. Las funciones públicas de una biblioteca suelen llevar anotaciones de todos modos: documentan la API e impiden que un cambio dentro de la función modifique su firma sin que nadie lo note.
Sin conversiones implícitas
Sección titulada «Sin conversiones implícitas»C# convierte un int en double por ti. F# nunca convierte un número implícitamente:
let semitones = 7let ratio = 2.0 ** (semitones / 12.0)l02_int_float.fsx(2,33): error FS0001: The type 'float' does not match the type 'int'semitones / 12.0 divide un int entre un float: F# se niega, donde C# calcularía un double sin decir nada. Las funciones de conversión tienen el nombre del tipo de destino:
let semitones = 7let ratio = 2.0 ** (float semitones / 12.0) // float convierte explícitamente
printfn "A fifth multiplies the frequency by %.4f" ratioprintfn "A2 = 110 Hz, so E3 = %.2f Hz" (110.0 * ratio)
printfn "%d" (int 3.99) // int trunca, como un cast en C#printfn "%s" (string 440) // string llama a ToStringprintfn "%d" (int "22") // int también analiza una cadena, y lanza una excepción si no puedeA fifth multiplies the frequency by 1.4983A2 = 110 Hz, so E3 = 164.81 Hz344022float semitones es una llamada a función, como printfn "…" ratio: la función y luego su argumento. Es el mismo float que el nombre del tipo; F# usa el nombre para ambas cosas. La página de conversiones y cast enumera las funciones.
Todo es una expresión
Sección titulada «Todo es una expresión»En C#, if es una instrucción y ?: es su forma de expresión. En F#, if … then … else es la expresión, y un bloque también:
// if/then/else es una expresión: tiene un valor, como el operador ?: de C#let fretLabel fret = if fret = 0 then "open" else $"fret {fret}"printfn "%s, %s" (fretLabel 0) (fretLabel 3)
// Un bloque también es una expresión: su valor es su última línealet positions = let strings = 6 let frets = 22 strings * (frets + 1)
printfn "%d positions, open strings included" positions
// Las funciones que solo tienen un efecto devuelven unit, que se escribe ()let nothing = printfn "printfn returns unit"printfn "%A" nothingopen, fret 3138 positions, open strings includedprintfn returns unit()positionsse calcula con un bloque cuyos valores internos,stringsyfrets, no existen fuera de él.- F# no tiene
void. Una función que no devuelve nada útil devuelveunit, cuyo único valor se escribe(). Por esolet restring () = …recibe(): recibe un argumento, el valor unit, y se llama conrestring (). Llamar a un métodovoidde C# desde F# también devuelveunit.
Un if sin else solo puede ser de tipo unit: si produjera una cadena cuando la condición es verdadera, ¿qué produciría cuando es falsa?
let fretLabel fret = if fret = 0 then "open"l02_if_without_else.fsx(1,39): error FS0001: This 'if' expression is missing an 'else' branch. Because 'if' is an expression, and not a statement, add an 'else' branch which also returns a value of type 'string'.Funciones
Sección titulada «Funciones»Los parámetros se separan con espacios
Sección titulada «Los parámetros se separan con espacios»Las clases de altura de la teoría musical numeran las doce notas de una octava: 0 es do, 1 es do♯, y así hasta 11, si. Transponer una nota suma semitonos y vuelve a 0 después de 11. El HarmonicTransformationService de Guitar Alchemist empieza con esta función:
let normalize pc = ((pc % 12) + 12) % 12El % de un número negativo es negativo en F#, como en C#: -5 % 12 es -5. Por eso la función suma 12 y vuelve a aplicar % 12, para quedar entre 0 y 11: normalize -5 es 7, sol.
// Clases de altura: 0 = do, 1 = do#, ..., 11 = si. El HarmonicTransformationService de Guitar Alchemist usa esta fórmula.let normalize pc = ((pc % 12) + 12) % 12printfn "%d %d" (-5 % 12) (normalize -5) // % conserva el signo, normalize no
// Dos parámetros separados por espacios: transpose tiene el tipo int -> int -> intlet transpose interval pc = normalize (pc + interval)
let names = [| "C"; "C#"; "D"; "D#"; "E"; "F"; "F#"; "G"; "G#"; "A"; "A#"; "B" |]let name pc = names[pc]
printfn "%s" (name (transpose 7 0))
// Aplicación parcial: se da solo el primer argumento y se obtiene una funciónlet upAFifth = transpose 7printfn "%s %s %s" (name (upAFifth 0)) (name (upAFifth 7)) (name (upAFifth 2))
// Un argumento negativo: el signo menos va pegado al númeroprintfn "%s" (name (transpose -1 0))
// Pipeline: x |> f es f xprintfn "%s" (0 |> upAFifth |> upAFifth |> upAFifth |> name)
// Composición: f >> g es fun x -> g (f x)let fifthName = upAFifth >> nameprintfn "%s" (fifthName 9)
// Lambda: fun pc -> ... es el pc => ... de C#let downASemitone = fun pc -> transpose -1 pcprintfn "%s" (name (downASemitone 0))-5 7GG D ABAEB[| … |] es un array, y names[pc] lo indexa. name (transpose 7 0) necesita sus paréntesis: sin ellos, name transpose 7 0 pasaría tres argumentos a name.
Funciones currificadas y aplicación parcial
Sección titulada «Funciones currificadas y aplicación parcial»transpose tiene el tipo int -> int -> int. Lee las flechas desde la derecha: transpose toma un int y devuelve una función int -> int. transpose 7 0 es (transpose 7) 0. Las funciones que reciben sus parámetros de uno en uno de esta forma se llaman currificadas, por el lógico Haskell Curry.
Así que transpose 7, con un solo argumento, es un valor completo: una función que transpone una quinta hacia arriba. Eso es la aplicación parcial. El equivalente en C# necesita lambdas anidadas:
Func<int, Func<int, int>> transpose = interval => pc => Normalize(pc + interval);var upAFifth = transpose(7);y Java tiene Function<Integer, Function<Integer, Integer>>. F# lo convierte en la forma normal de escribir una función, y por eso importa el orden de los parámetros: el que más probablemente vas a fijar va primero. interval va antes que pc, para que transpose 7 tenga sentido.
La costumbre de C# de llamar con paréntesis y comas pasa una tupla en lugar de dos argumentos:
let normalize (pc: int) = ((pc % 12) + 12) % 12let transpose (interval: int) (pc: int) = normalize (pc + interval)
let g = transpose(7, 0)l02_tuple_call.fsx(4,19): error FS0001: This expression was expected to have type 'int'but here has type 'int * int'int * int es el tipo de la tupla (7, 0). Los métodos de las clases .NET, en cambio, se llaman con paréntesis y comas: name.Trim(), Regex.Replace(s, pattern, ""). La sección siguiente muestra dónde se encuentran ambas formas.
Los espacios alrededor de un signo menos también importan. transpose -1 0 pasa -1, pero transpose - 1 0 resta:
printfn "%d" (transpose - 1 0)l02_minus_spaced.fsx(4,27): error FS0003: This value is not a function and cannot be applied.F# lee transpose - (1 0), y 1 no es una función que se pueda aplicar a 0.
Lambdas, |> y >>
Sección titulada «Lambdas, |> y >>»fun pc -> transpose -1 pces una lambda, elpc => Transpose(-1, pc)de C#. Comotranspose -1ya es esa función,let downASemitone = transpose -1haría lo mismo.x |> fesf x: el operador pipe pasa el valor de su izquierda a la función de su derecha.0 |> upAFifth |> upAFifth |> upAFifth |> namese lee en el orden en que se hace el trabajo, como una cadena de métodos LINQ. Está definido en FSharp.Core como una función ordinaria de dos argumentos, que llama a la segunda con el primero.f >> ges la composición de dos funciones: una función nueva que llama afy luego agcon el resultado.
TARS escribe su preprocesamiento de texto como un pipeline, en TextNormalizer.fs:
/// Extract keywords from text (normalize -> tokenize -> remove stop words -> dedup) let extractKeywords (text: string) = text |> normalize |> tokenize |> removeStopWords |> List.distinctCada paso es una función del módulo que toma el resultado del anterior; el comentario y el código dicen lo mismo, en el mismo orden. Su primer paso, normalize, mezcla métodos .NET (llamados con paréntesis) y pipes de F#, con lambdas en medio:
let normalize (text: string) = if String.IsNullOrWhiteSpace(text) then "" else text.ToLowerInvariant() |> fun s -> Regex.Replace(s, @"[^a-z0-9\s]", "") // Keep only alphanumeric and space |> fun s -> Regex.Replace(s, @"\s+", " ") // Collapse multiple spaces |> fun s -> s.Trim()(text: string)está anotado porquetext.ToLowerInvariant()llama a un método: F# necesita conocer el tipo detextantes de un., ya que la inferencia va de izquierda a derecha.@"…"es una cadena literal, como en C#.|> fun s -> …envía a una lambda, una por línea.
El script examples/l02_tars_normalize.fsx del curso vuelve a escribir esta función (TARS no tiene archivo de licencia, así que el curso no copia el archivo) y la ejecuta:
printfn "[%s]" (normalize " How do I learn F#? ")printfn "[%s]" (normalize "C# and F# on .NET 10")printfn "[%s]" (normalize "Qu'est-ce qu'un café ? ¿Qué es un acorde?")[how do i learn f][c and f on net 10][questce quun caf qu es un acorde]El patrón [^a-z0-9\s] elimina todo lo que no sea una letra ASCII, un dígito o un espacio. Para un framework de agentes escrito en F#, eso incluye #: las palabras clave de «learn F#» son learn y f, y C# se convierte en la letra c. Las letras acentuadas desaparecen en medio de las palabras: café se convierte en caf. La prueba de TARS solo usa una frase en inglés. El diario lo anota; la lección 5 vuelve sobre este módulo.
Código real: miembros currificados en GA
Sección titulada «Código real: miembros currificados en GA»HarmonicTransformationService es una clase (lección 9), cuyos miembros están currificados como las funciones anteriores. Su Transpose:
/// <summary> /// Transposes a set of pitch classes. /// </summary> member _.Transpose (interval: Interval) (pcs: PitchClassSet) : PitchClassSet = pcs |> Set.map (fun pc -> normalize (pc + interval))Interval y PitchClassSet son abreviaturas de tipo de MusicalSetTypes.fs: otros nombres para int y Set<int>. Set.map aplica la lambda a cada elemento del conjunto. El : PitchClassSet tras los parámetros anota el resultado.
El curso carga el archivo de GA con #load y lo llama (examples/l02_ga_transformations.fsx):
// El HarmonicTransformationService de Guitar Alchemist, cargado desde las copias de external/ga#load "../external/ga/MusicalSetTypes.fs" "../external/ga/HarmonicTransformationService.fs"
open GA.Business.DSL.Services
let service = HarmonicTransformationService()let cMajor = set [ 0; 4; 7 ] // do mi sol
printfn "%A" (service.Transpose 7 cMajor) // una quinta arriba: sol si re
let upAFifth = service.Transpose 7 // un método con parámetros currificados se puede aplicar parcialmenteprintfn "%A" (cMajor |> upAFifth |> upAFifth) // re fa# la
printfn "%A" (service.Invert 0 cMajor) // reflejado alrededor de do: do fa lab
// Una vez compilado, el miembro currificado es un método .NET ordinario de dos parámetros: C# llama a Transpose(2, set)let transpose = typeof<HarmonicTransformationService>.GetMethod "Transpose"
for parameter in transpose.GetParameters() do printfn "%s %s" parameter.ParameterType.Name parameter.Nameset [2; 7; 11]set [2; 6; 9]set [0; 5; 8]Int32 intervalFSharpSet`1 pcsLas dos últimas líneas muestran cómo compila F# un miembro currificado: un método ordinario Transpose(int interval, FSharpSet<int> pcs). Las pruebas C# de GA lo llaman así, en HarmonicTransformationTests.cs: _service.Transpose(2, cMajor). La aplicación parcial se queda del lado de F#; la lección 20 trata del diseño de API F# que C# consume cómodamente.
Puntos clave
Sección titulada «Puntos clave»letda un nombre a un valor que nunca cambia;let mutabley<-son la excepción explícita, y=siempre compara.- El compilador infiere los tipos de valores, parámetros y resultados, y generaliza las funciones cuando puede (
'a); basta una anotación para orientarlo. - Los números nunca se convierten implícitamente:
float,intystringconvierten de forma explícita. if, los bloques ytryson expresiones;unity()sustituyen avoid.- Las funciones reciben sus argumentos separados por espacios y están currificadas:
transpose 7es una función.|>envía un valor a una función,>>compone dos funciones yfun x -> …es una lambda.
Ejercicios
Sección titulada «Ejercicios»- En el temperamento igual, cada semitono multiplica la frecuencia por la raíz duodécima de 2, y el la4 está a 440 Hz. Escribe
frequency, que recibe el número de semitonos desde el la4 (unint, negativo por debajo del la4), e imprime las frecuencias del la4, del mi4 (5 semitonos por debajo) y del mi2 (29 por debajo) con dos decimales.
Solución
exercises/l02_ex_frequency.fsx:
// Temperamento igual: cada semitono multiplica la frecuencia por 2^(1/12), y el la4 está a 440 Hzlet frequency semitonesFromA4 = 440.0 * 2.0 ** (float semitonesFromA4 / 12.0)
printfn "A4 = %.2f Hz" (frequency 0)printfn "E4 = %.2f Hz" (frequency -5)printfn "E2 = %.2f Hz" (frequency -29)A4 = 440.00 HzE4 = 329.63 HzE2 = 82.41 Hzfloat semitonesFromA4 es obligatorio: sin él, semitonesFromA4 / 12.0 es el error FS0001 de Sin conversiones implícitas. F# infiere frequency: semitonesFromA4: int -> float.
- Escribe
interval fromPc toPc, el número de semitonos que hay que subir de una clase de altura a otra (de mi, 4, a do, 0, son 8). Después definefromEpor aplicación parcial y úsala con sol (7), do (0) y, con|>, mi (4).
Solución
let normalize pc = ((pc % 12) + 12) % 12
// Semitonos que hay que subir de una clase de altura a otralet interval fromPc toPc = normalize (toPc - fromPc)
let fromE = interval 4 // aplicación parcial: int -> int
printfn "E up to G: %d semitones" (fromE 7)printfn "E up to C: %d semitones" (fromE 0)printfn "E up to E: %d semitones" (4 |> fromE)E up to G: 3 semitonesE up to C: 8 semitonesE up to E: 0 semitones0 - 4 es -4, y normalize lo convierte en 8: por eso la fórmula de GA suma 12 antes del segundo % 12.
- Este script tiene tres errores. Ejecútalo, corrige lo que señale F# y vuelve a ejecutarlo hasta que imprima
capo on fret 3: frequencies multiplied by 1.1892. ¿Cuántas ejecuciones hicieron falta?
let capo = 2capo <- 3let ratio = 2.0 ** (capo / 12.0)let label = if capo = 0 then "no capo"Solución
Cuatro ejecuciones: aquí F# Interactive señala un error por ejecución. La primera:
l02_ex_broken.fsx(2,1): error FS0027: This value is not mutable. Consider using the mutable keyword, e.g. 'let mutable capo = expression'.Tras let mutable capo = 2, la segunda:
l02_ex_broken_step2.fsx(3,28): error FS0001: The type 'float' does not match the type 'int'Tras float capo, la tercera:
l02_ex_broken_step3.fsx(4,30): error FS0001: This 'if' expression is missing an 'else' branch. Because 'if' is an expression, and not a statement, add an 'else' branch which also returns a value of type 'string'.El script corregido (exercises/l02_ex_fixed.fsx), con el printfn que pedía el enunciado:
let mutable capo = 2capo <- 3let ratio = 2.0 ** (float capo / 12.0)let label = if capo = 0 then "no capo" else $"capo on fret {capo}"
printfn "%s: frequencies multiplied by %.4f" label ratiocapo on fret 3: frequencies multiplied by 1.1892Una cejilla en el traste 3 sube cada cuerda 3 semitonos, y 2.0 ** (3.0 / 12.0) es aproximadamente 1.1892.