4. Coincidencia de patrones
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/l04_*.fsx y el fragmento rechazado compile_fail/l04_warnaserror.fsx.
match es una expresión switch
Sección titulada «match es una expresión switch»C# 8 añadió la expresión switch, y C# 9 sus patrones relacionales y or:
string IntervalName(int semitones) => semitones switch{ 0 => "unison", 3 or 4 => "third", 7 => "perfect fifth", 12 => "octave", > 12 => "compound interval", _ => "other",};Java 21 tiene switch con patrones y guardas when. El match de F# es la construcción en la que se inspiraron:
// match prueba los patrones de arriba abajo; gana el primero que encajalet intervalName semitones = match semitones with | 0 -> "unison" | 3 | 4 -> "third" // patrón «o» | 7 -> "perfect fifth" | 12 -> "octave" | n when n > 12 -> $"compound interval ({n - 12} above an octave)" // guarda | _ -> "other" // comodín
for semitones in [ 0; 4; 7; 10; 19 ] do printfn "%d: %s" semitones (intervalName semitones)
// Una tupla compara varios valores a la vezlet position (stringNumber, fret) = match stringNumber, fret with | _, 0 -> "open string" | (5 | 6), f -> $"bass string, fret {f}" | s, f -> $"string {s}, fret {f}"
printfn "%s" (position (6, 0))printfn "%s" (position (5, 3))printfn "%s" (position (2, 1))0: unison4: third7: perfect fifth10: other19: compound interval (7 above an octave)open stringbass string, fret 3string 2, fret 1- Cada regla es
| patrón -> expresión. Las reglas se prueban en orden, y el valor delmatches la expresión de la primera regla que encaja. Comoif,matches una expresión: todas sus ramas tienen el mismo tipo. 3 | 4es un patrón «o», el3 or 4de C#.n when n > 12enlaza el valor conn, y luego una guarda lo comprueba. F# no tiene patrones relacionales como el> 12de C#: una guarda hace el mismo trabajo._coincide con cualquier cosa, como el descarte de C#.match stringNumber, fret withcompara una tupla._, 0significa «cualquier cuerda, traste 0»;(5 | 6), fanida un patrón «o» dentro del patrón de tupla y enlaza el traste.
Casos de unión y opciones
Sección titulada «Casos de unión y opciones»Los patrones rinden de verdad con las uniones discriminadas: un patrón puede comprobar el caso y descomponer sus datos en un solo paso.
type Fingering = | Open | Fretted of fret: int | Barre of fret: int * strings: int | Muted
let describe fingering = match fingering with | Open -> "open string" | Muted -> "not played" | Fretted 1 -> "first fret" // un caso con una constante dentro | Fretted fret -> $"fret {fret}" // un caso que enlaza sus datos | Barre(fret, 6) -> $"full barre on fret {fret}" | Barre(fret = f; strings = n) -> $"barre on fret {f} across {n} strings" // por nombre de campo
for fingering in [ Open; Muted; Fretted 1; Fretted 5; Barre(1, 6); Barre(3, 4) ] do printfn "%s" (describe fingering)
// Option también es una unión: Some y None son sus casoslet capoLabel capo = match capo with | None | Some 0 -> "no capo" | Some fret -> $"capo on fret {fret}"
printfn "%s, %s, %s" (capoLabel None) (capoLabel (Some 0)) (capoLabel (Some 2))
// function es fun x -> match x with, como en el ChordRenderer de Guitar Alchemistlet isPlayed = function | Muted -> false | _ -> true
printfn "%b %b" (isPlayed Muted) (isPlayed (Fretted 3))open stringnot playedfirst fretfret 5full barre on fret 1barre on fret 3 across 4 stringsno capo, no capo, capo on fret 2false trueFretted 1solo coincide con el valorFretted 1;Fretted fretcoincide con cualquierFrettedy llamafreta sus datos. El orden importa: al revés,Fretted fretatraparía primeroFretted 1.Barre(fret = f; strings = n)compara los campos por nombre, separados por;.- Un patrón «o» puede ocupar dos líneas, como
NoneySome 0. functiones un atajo para una lambda que compara su argumento de inmediato:let isPlayed = function | …eslet isPlayed x = match x with | ….
La versión en C# de describe usaría patrones de tipo sobre una jerarquía de records (Fretted { Fret: 1 } => …, Fretted f => …). Se parece, y lo es, con una diferencia de la que trata la sección siguiente.
Código real: el renderizador de acordes de GA
Sección titulada «Código real: el renderizador de acordes de GA»El ChordRenderer.fs de GA convierte de nuevo en texto el ChordAst de la lección 3. Es casi todo coincidencia de patrones:
module ChordRenderer = let renderAccidental = function | Natural -> "" | Sharp -> "#" | Flat -> "b" | DoubleSharp -> "##" | DoubleFlat -> "bb"
// (renderQuality, líneas 14-21, tiene la misma forma)
let renderComponent = function | Extension s -> s | Alteration(acc, deg) -> (renderAccidental acc) + deg | Omission deg -> "(no " + deg + ")" | Alt -> "alt"
let render (ast: ChordAst) = let root = ast.Root + (renderAccidental ast.RootAccidental) let qual = ast.Quality |> Option.map renderQuality |> Option.defaultValue "" let comps = ast.Components |> List.map renderComponent |> String.concat ""
let bass = match ast.Bass with | Some(n, acc) -> "/" + n.ToUpper() + (renderAccidental acc) | None -> ""
root + qual + comps + bassrenderAccidentales unafunctioncon una regla por caso deAccidentalType, y sin_: si GA añade algún día un triple sostenido, el compilador señalará esta función.Alteration(acc, deg)descompone la tupla que lleva el caso.Some(n, acc)anida un patrón de tupla dentro del patrónSome: coincide con un bajo y nombra su letra y su alteración en un solo paso.List.map renderComponentaplica la función a cada componente; la lección 5 trata deList.
El compilador comprueba que se trata cada caso
Sección titulada «El compilador comprueba que se trata cada caso»Este es un match que olvida dos casos:
type Accidental = | Natural | Sharp | Flat | DoubleSharp | DoubleFlat
let symbol accidental = match accidental with | Natural -> "" | Sharp -> "#" | Flat -> "b"
printfn "C%s" (symbol Sharp)printfn "C%s" (symbol DoubleFlat)printfn "never printed"l04_incomplete.fsx(9,11): warning FS0025: Incomplete pattern matches on this expression. For example, the value 'DoubleFlat' may indicate a case not covered by the pattern(s).
C#Microsoft.FSharp.Core.MatchFailureException: The match cases were incompleteStopped due to errorEl compilador conoce todos los casos de Accidental y nombra uno que ninguna regla cubre. Es una advertencia, así que el script se ejecuta, y falla en tiempo de ejecución con una MatchFailureException cuando DoubleFlat llega de verdad (F# Interactive también imprime la pila de llamadas, que el check.sh del curso elimina).
C# también avisa, pero no puede ser tan preciso. Una expresión switch sobre la jerarquía de records Fingering de la lección 3 que trata Open, Fretted y Muted, las tres subclases, sigue recibiendo warning CS8509: … For example, the pattern '_' is not covered: otro ensamblado podría añadir una subclase. Sobre un enum que enumera todos sus valores con nombre, recibe CS8524, porque (Accidental)3 también es un valor válido. Así que el código C# termina con un brazo _ =>, que silencia la advertencia para siempre (el diario tiene los dos archivos). Java 21 sí comprueba un switch sobre una interfaz sealed, y rechaza un caso que falta. Las uniones F# son cerradas como los tipos sellados de Java, y F# comprueba todos los patrones: tuplas de uniones, opciones anidadas, listas.
Una advertencia que deja que el programa falle más tarde debería ser un error. En un proyecto, <TreatWarningsAsErrors>true</TreatWarningsAsErrors>, o <WarningsAsErrors>FS0025</WarningsAsErrors> solo para esta advertencia, lo hace para toda la compilación; TARS activa la primera en v2/Directory.Build.props, GA no. Para un script, dotnet fsi acepta la opción del compilador --warnaserror+:25:
> dotnet fsi --warnaserror+:25 compile_fail/l04_warnaserror.fsxl04_warnaserror.fsx(10,11): error FS0025: Incomplete pattern matches on this expression. For example, the value 'DoubleFlat' may indicate a case not covered by the pattern(s).Las guardas anulan la comprobación
Sección titulada «Las guardas anulan la comprobación»El compilador no puede evaluar una guarda. Estas tres reglas cubren todos los enteros, pero solo un lector puede verlo:
let direction interval = match interval with | n when n > 0 -> "up" | n when n < 0 -> "down" | n when n = 0 -> "same note"
printfn "%s %s %s" (direction 5) (direction -2) (direction 0)l04_guards_warning.fsx(2,11): warning FS0025: Incomplete pattern matches on this expression.
up down same noteEsta vez el mensaje no da ningún ejemplo: el compilador no sabe qué valor se escapa. Escribe la última regla sin guarda, | _ -> "same note", y la advertencia desaparece.
Reglas que nunca coinciden
Sección titulada «Reglas que nunca coinciden»Las reglas se prueban en orden, así que una regla situada después de un patrón que lo atrapa todo es código muerto:
let intervalName semitones = match semitones with | 0 -> "unison" | _ -> "other" | 7 -> "perfect fifth"
printfn "%s" (intervalName 7)l04_never_matched.fsx(5,7): warning FS0026: This rule will never be matched
otherLos comodines ocultan los casos nuevos
Sección titulada «Los comodines ocultan los casos nuevos»_ es cómodo y peligroso del mismo modo que el default de C#. La función isPlayed de arriba usa | _ -> true para tres casos; si se añade a Fingering un quinto caso Harmonic of fret: int, isPlayed responde true para él sin decir nada, y no aparece ninguna advertencia. renderAccidental en GA, con una regla por caso, recibiría la advertencia. Es mejor enumerar los casos de una unión cuya lista puede crecer; deja _ para conjuntos abiertos de valores como los enteros y las cadenas.
Patrones de listas y recursión
Sección titulada «Patrones de listas y recursión»Una lista está vacía, [], o es un elemento seguido del resto de la lista, head :: tail. Los patrones descomponen las listas según esas dos formas:
// Patrones de listas: [] es la lista vacía, head :: tail separa el primer elementolet describeStrings strings = match strings with | [] -> "no strings" | [ single ] -> $"only {single}" | [ low; high ] -> $"{low} and {high}" | lowest :: rest -> $"{lowest}, then {List.length rest} more"
printfn "%s" (describeStrings [])printfn "%s" (describeStrings [ "G4" ])printfn "%s" (describeStrings [ "C4"; "G4" ])printfn "%s" (describeStrings [ "E2"; "A2"; "D3"; "G3"; "B3"; "E4" ])
// Una función recursiva (rec) recorre la lista elemento a elementolet rec total frets = match frets with | [] -> 0 | fret :: rest -> fret + total rest
printfn "%d" (total [ 3; 2; 0; 1; 0 ])
// Los patrones también funcionan en let y en los parámetroslet (note, octave) = ("A", 4)let lowest (first :: _) = first // advertencia: [] no se trataprintfn "%s%d %s" note octave (lowest [ "E2"; "A2" ])l04_lists.fsx(24,13): warning FS0025: Incomplete pattern matches on this expression. For example, the value '[]' may indicate a case not covered by the pattern(s).
no stringsonly G4C4 and G4E2, then 5 more6A4 E2[ single ]coincide con una lista de exactamente un elemento,[ low; high ]de exactamente dos.lowest :: restcoincide con cualquier lista no vacía. Colocado primero, atraparía también las listas de uno y dos elementos.- Una función que se llama a sí misma necesita
rec.totaltiene la forma clásica: una regla para la lista vacía y una regla que trata un elemento y se llama recursivamente con el resto. La lección 5 muestra las funciones de biblioteca que te ahorran escribir la mayoría de estas funciones. - Los patrones no son solo para
match.let (note, octave) = …descompone una tupla, ylet describeAgent (AgentId id) = …en la lección 3 desenvolvía una unión de un solo caso en un parámetro. También se comprueban:lowest (first :: _)recibe FS0025, ya que una lista vacía no tiene primer elemento.
Código real: el script BinObj de GA
Sección titulada «Código real: el script BinObj de GA»El Scripts/BinObj.fsx de GA busca las carpetas bin y obj de un árbol de código fuente, con una función recursiva sobre una lista:
open System.Globalizationopen System.IO
let EnumerateDirectories path = Directory.EnumerateDirectories(path) |> Seq.toList
let isObjOrBinFolder (folderName: string) = folderName.EndsWith("obj", true, CultureInfo.InvariantCulture) || folderName.EndsWith("bin", true, CultureInfo.InvariantCulture)
let rec getFoldersToDelete path = match EnumerateDirectories path with | [] -> [] | subfolders -> let targetFolders = subfolders |> List.filter isObjOrBinFolder
let targets = subfolders |> List.filter (isObjOrBinFolder >> not) |> List.collect getFoldersToDelete |> List.append targetFolders
targetsmatch EnumerateDirectories path with | [] -> [] | subfolders -> …trata una carpeta sin subcarpetas y luego enlaza la lista no vacía con un nombre.isObjOrBinFolder >> notcompone la prueba connot, la composición de la lección 2: «no es una carpeta bin u obj».List.collect getFoldersToDeleteentra recursivamente en cada subcarpeta restante y concatena los resultados.
El curso carga el script y ejecuta su función sobre un árbol pequeño (examples/l04_ga_binobj.fsx):
// Un árbol pequeño en la carpeta temporallet root = Path.Combine(Path.GetTempPath(), $"fsharp-course-binobj-{Environment.ProcessId}")
for folder in [ "App/bin"; "App/obj"; "App/src"; "Docs/cabin"; "Robin/Songs"; "Empty" ] do Directory.CreateDirectory(Path.Combine(root, folder)) |> ignore
for path in getFoldersToDelete root |> List.sort do printfn "%s" (Path.GetRelativePath(root, path).Replace('\\', '/'))
Directory.Delete(root, true)App/binApp/objDocs/cabinRobinCódigo real: coincidencia sobre Result en el parser de acordes de GA
Sección titulada «Código real: coincidencia sobre Result en el parser de acordes de GA»El ChordParser.parse de GA ejecuta un parser de FParsec y convierte su resultado, una unión de FParsec, en el Result de F#:
let parse chordStr = match run pChord chordStr with | Success(result, _, _) -> Result.Ok result | Failure(errorMsg, _, _) -> Result.Error errorMsgSuccess(result, _, _) enlaza el primero de tres valores e ignora los otros dos. El curso carga el parser y compara el Result que devuelve (examples/l04_ga_parse.fsx):
#r "nuget: FParsec, 1.1.1"#load "../external/ga/ChordAst.fs" "../external/ga/ChordRenderer.fs" "../external/ga/ChordParser.fs"
open GA.Business.DSL.Parsersopen GA.Business.DSL.Generators
// El ChordParser.parse de Guitar Alchemist devuelve un Result: Ok con el AST, o Error con el mensaje de FParseclet normalize (input: string) = match ChordParser.parse input with | Ok ast -> $"{input} -> {ChordRenderer.render ast}" | Error message -> $"{input} -> error:\n{message}"
for input in [ "Cmi7"; "EbΔ9"; "F#m7b5/C"; "C7sus4"; "Am(maj7)"; "H7" ] do printfn "%s" (normalize input)Cmi7 -> Cm7EbΔ9 -> Ebmaj9F#m7b5/C -> F#m7b5/CC7sus4 -> C7Am(maj7) -> AmH7 -> error:Error in Ln: 1 Col: 1H7^Expecting: any char in ‘CDEFGABcdefgab’Las tres primeras líneas son casos de la propia prueba de GA, ChordDslTests.cs. H7, el nombre alemán de B7, se rechaza con el mensaje de FParsec.
Puntos clave
Sección titulada «Puntos clave»match value with | pattern -> resultprueba las reglas en orden y es una expresión;functiones una lambda que compara su argumento.- Los patrones se combinan: constantes,
_,|para las alternativas, guardaswhen, tuplas, casos de unión con sus datos, campos por nombre,[]yhead :: tailpara las listas. - El compilador avisa cuando falta un caso (FS0025) o una regla es inalcanzable (FS0026); convierte FS0025 en error en los proyectos, y evita
_en uniones que pueden crecer. - Las guardas no se analizan: la última regla de un match con guardas no debería tener guarda.
- Los patrones funcionan también en
lety en los parámetros, con las mismas comprobaciones.
Ejercicios
Sección titulada «Ejercicios»- Escribe
quality semitones, que devuelve"perfect"para 0, 5, 7 y 12,"minor"para 1, 3, 8 y 10,"major"para 2, 4, 9 y 11,"tritone"para 6, un mensaje para los números negativos y otro para los intervalos mayores que una octava. Usa patrones «o» y una guarda, e imprime el resultado para -3, 0, 3, 4, 6, 7, 11 y 14.
Solución
exercises/l04_ex_intervals.fsx:
let quality semitones = match semitones with | n when n < 0 -> "negative: turn it into an upward interval first" | 0 | 5 | 7 | 12 -> "perfect" | 1 | 3 | 8 | 10 -> "minor" | 2 | 4 | 9 | 11 -> "major" | 6 -> "tritone" | _ -> "compound: larger than an octave"
for semitones in [ -3; 0; 3; 4; 6; 7; 11; 14 ] do printfn "%3d: %s" semitones (quality semitones) -3: negative: turn it into an upward interval first 0: perfect 3: minor 4: major 6: tritone 7: perfect 11: major 14: compound: larger than an octaveLa guarda va primero, así que no hace falta excluir los números negativos más adelante. El _ final es correcto aquí: los enteros son un conjunto abierto, y todo valor por encima de 12 merece la misma respuesta. %3d rellena el número hasta tres caracteres.
- Corrige la función
symbolde El compilador comprueba que se trata cada caso para que el script imprimaC#yCbbsin advertencia. ¿Por qué no añadir| _ -> ""?
Solución
exercises/l04_ex_symbol_fixed.fsx:
let symbol accidental = match accidental with | Natural -> "" | Sharp -> "#" | Flat -> "b" | DoubleSharp -> "##" | DoubleFlat -> "bb"C#Cbb| _ -> "" también quita la advertencia, pero imprime C para un doble bemol: una respuesta errónea en lugar de un error. Y el próximo caso que se añada a Accidental recibiría la misma respuesta errónea, sin advertencia que señale esta función.
- La prueba
Test_Complex_Alterationsde GA analizaC13#11b9y espera tres componentes. Escribe una función recursivacountAlterationssobre unaChordComponent list, con patrones de listas, que cuente los componentesAlterationyAlt, y úsala con el resultado del parser de GA para ese acorde.
Solución
#r "nuget: FParsec, 1.1.1"#load "../external/ga/ChordAst.fs" "../external/ga/ChordParser.fs"
open GA.Business.DSL.Typesopen GA.Business.DSL.Parsers
let rec countAlterations components = match components with | [] -> 0 | (Alteration _ | Alt) :: rest -> 1 + countAlterations rest | _ :: rest -> countAlterations rest
// El acorde de la prueba Test_Complex_Alterations de GA (Tests/Common/GA.Business.DSL.Tests/ChordDslTests.cs)match ChordParser.parse "C13#11b9" with| Ok chord -> printfn "%A: %d alterations" chord.Components (countAlterations chord.Components)| Error message -> printfn "%s" message[Extension "13"; Alteration (Sharp, "11"); Alteration (Flat, "9")]: 2 alterations(Alteration _ | Alt) :: rest pone un patrón «o» en la cabeza de un patrón de lista; Alteration _ ignora los datos del caso. Los dos lados de un patrón «o» deben enlazar los mismos nombres, aquí solo rest, fuera de los paréntesis.