8. Expresiones de cómputo — un parser para un DSL
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: examples/l08_parser_ce.fsx, ejecutado por el harness del curso.
Una sintaxis, varios tipos de cómputo
Sección titulada «Una sintaxis, varios tipos de cómputo»Una expresión de cómputo de F# tiene la forma builder { ... }. El builder decide qué significan let!, do!, return, yield y las demás palabras clave.
| Expresión | Cómputo descrito | Vecino aproximado en C#/Java |
|---|---|---|
seq { yield value } |
secuencia perezosa | iterador / IEnumerable, Stream |
async { let! value = work } |
workflow asíncrono de F# | composición de futures |
task { let! value = work } |
Task de .NET |
async/await, CompletableFuture |
parser { let! value = rule } |
parser que consume texto o falla | pipeline de combinadores de parsing |
Esta sintaxis está dirigida por métodos ordinarios. Por ejemplo:
parser { let! value = rule return transform value}se traduce conceptualmente a:
parser.Bind(rule, fun value -> parser.Return(transform value))let! no es una asignación ni es automáticamente asíncrono. Extrae el contexto elegido por el builder. and! representa cómputos independientes y requiere operaciones como MergeSources; nuestro parser es secuencial porque cada regla consume el resto que dejó la anterior.
El tipo de cómputo
Sección titulada «El tipo de cómputo»El ejemplo representa un parser como una función que recibe el texto no consumido y devuelve un valor con el resto, o un error:
type Parser<'value> = private | Parser of (string -> Result<'value * string, string>)bind ejecuta el primer parser. Si falla conserva el error; si tiene éxito entrega el valor a la función que construye el siguiente parser y lo ejecuta sobre el texto restante:
let bind next parser = Parser(fun input -> match run parser input with | Error error -> Error error | Ok(value, rest) -> run (next value) rest)El builder solo necesita tres miembros para la sintaxis usada aquí:
type ParserBuilder() = member _.Bind(parser, next) = Parser.bind next parser member _.Return(value) = Parser.result value member _.ReturnFrom(parser) = parser
let parser = ParserBuilder()Agregar Delay, Combine, TryWith, Using, While o MergeSources habilitaría más construcciones. No las agregues por costumbre: los métodos del builder son su gramática pública.
El DSL de notas
Sección titulada «El DSL de notas»El script completo define reglas pequeñas para un literal, un carácter que satisface un predicado, una alteración opcional y el final del texto. La gramática expresa entonces su intención:
let noteParser = parser { let! _ = Parser.literal "note " let! letter = Parser.satisfy "note letter A-G" (fun value -> value >= 'A' && value <= 'G') let! accidental = Parser.optionalChar [ '#'; 'b' ] let! octave = Parser.satisfy "octave 0-9" Char.IsDigit do! Parser.endOfInput
let name = match accidental with | Some symbol -> $"{letter}{symbol}" | None -> string letter
return { Name = name; Octave = int (string octave) } }Salida medida:
OK note C#4 -> C#4OK note Eb3 -> Eb3ERROR note H2 -> expected note letter A-G, got 'H'ERROR note C#4 tail -> expected end of input, got " tail"do! Parser.endOfInput es esencial. Sin él, note C#4 tail tendría éxito e ignoraría el sufijo, exactamente la clase de error de DSL registrada para el parser de acordes de GA en el diario.
Hasta dónde construirlo uno mismo
Sección titulada «Hasta dónde construirlo uno mismo»Este parser didáctico no conserva línea y columna, no distingue un desacuerdo recuperable de un fallo comprometido y no implementa backtracking controlado. Para una gramática de producción, usa una biblioteca probada como FParsec o un parser de proyecto con las mismas garantías. La expresión de cómputo sigue siendo útil porque hace visible la política de secuenciación y separa la gramática de la propagación de errores.
Ejercicio
Sección titulada «Ejercicio»Escribe un parser para octave 4. Reutiliza literal, satisfy y endOfInput, y devuelve la octava como int. Comprueba que octave 42 falla porque queda texto.
Solución
let octaveParser = parser { let! _ = Parser.literal "octave " let! digit = Parser.satisfy "octave 0-9" Char.IsDigit do! Parser.endOfInput return int (string digit) }La regla de un dígito es deliberada. Admitir varios dígitos es una decisión nueva de gramática, no una razón para quitar la comprobación del final.
Para recordar
Sección titulada «Para recordar»- Una expresión de cómputo es sintaxis dirigida por un builder, no un sinónimo de código asíncrono.
let!llama aBind;returnllama aReturn; el builder define su efecto.- Un builder de parsing reutiliza la secuenciación y la propagación de errores sin ocultar la gramática.
- Una regla explícita de final impide que un prefijo válido acepte un comando DSL inválido.