8. Expressions de calcul — un parseur pour un DSL
Sections
Réglages de lecture
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
Code : examples/l08_parser_ce.fsx, exécuté par le harness du cours.
Une syntaxe, plusieurs calculs
Section intitulée « Une syntaxe, plusieurs calculs »Une expression de calcul F# a la forme builder { ... }. Le builder décide de la signification de let!, do!, return, yield et des autres mots-clés.
| Expression | Calcul décrit | Équivalent approximatif en C#/Java |
|---|---|---|
seq { yield value } |
séquence paresseuse | itérateur / IEnumerable, Stream |
async { let! value = work } |
workflow asynchrone F# | composition de futures |
task { let! value = work } |
Task .NET |
async/await, CompletableFuture |
parser { let! value = rule } |
parseur qui consomme du texte ou échoue | pipeline de combinators de parsing |
Cette syntaxe est pilotée par des méthodes ordinaires. Ainsi :
parser { let! value = rule return transform value}est conceptuellement traduit en :
parser.Bind(rule, fun value -> parser.Return(transform value))let! n’est donc ni une affectation ni forcément une opération asynchrone. Il extrait le contexte choisi par le builder. and! représente des calculs indépendants et demande notamment MergeSources ; notre parseur reste séquentiel, car chaque règle consomme le reste laissé par la précédente.
Le type de calcul
Section intitulée « Le type de calcul »L’exemple représente un parseur par une fonction qui reçoit le texte restant et renvoie soit une valeur avec le reste, soit une erreur :
type Parser<'value> = private | Parser of (string -> Result<'value * string, string>)bind exécute le premier parseur. En cas d’échec, il conserve l’erreur ; en cas de succès, il transmet la valeur à la fonction qui construit le parseur suivant, puis l’exécute sur le texte restant :
let bind next parser = Parser(fun input -> match run parser input with | Error error -> Error error | Ok(value, rest) -> run (next value) rest)Le builder n’a besoin que de trois membres pour la syntaxe utilisée ici :
type ParserBuilder() = member _.Bind(parser, next) = Parser.bind next parser member _.Return(value) = Parser.result value member _.ReturnFrom(parser) = parser
let parser = ParserBuilder()Ajouter Delay, Combine, TryWith, Using, While ou MergeSources activerait d’autres constructions. Ne les ajoutez pas par réflexe : les méthodes du builder constituent sa grammaire publique.
Le DSL de notes
Section intitulée « Le DSL de notes »Le script complet définit de petites règles pour un littéral, un caractère satisfaisant un prédicat, une altération facultative et la fin du texte. La grammaire exprime alors directement son intention :
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) } }Sortie mesurée :
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 est essentiel. Sans lui, note C#4 tail réussirait en ignorant silencieusement le suffixe — exactement la classe de bug DSL relevée dans le parseur d’accords de GA dans le journal.
Jusqu’où le construire soi-même
Section intitulée « Jusqu’où le construire soi-même »Ce parseur pédagogique ne suit ni ligne ni colonne, ne distingue pas une divergence récupérable d’un échec engagé et n’implémente pas de backtracking contrôlé. Pour une grammaire de production, utilisez une bibliothèque éprouvée comme FParsec ou un parseur de projet offrant les mêmes garanties. L’expression de calcul reste utile : elle rend explicite la politique d’enchaînement et sépare la grammaire de la propagation des erreurs.
Exercice
Section intitulée « Exercice »Écrivez un parseur pour octave 4. Réutilisez literal, satisfy et endOfInput, puis renvoyez l’octave comme int. Vérifiez que octave 42 échoue parce qu’il reste du texte.
Solution
let octaveParser = parser { let! _ = Parser.literal "octave " let! digit = Parser.satisfy "octave 0-9" Char.IsDigit do! Parser.endOfInput return int (string digit) }La règle à un chiffre est volontaire. Accepter plusieurs chiffres est une nouvelle décision de grammaire, pas une raison de retirer le contrôle de fin.
À retenir
Section intitulée « À retenir »- Une expression de calcul est une syntaxe pilotée par un builder, pas un synonyme d’asynchronisme.
let!appelleBind;returnappelleReturn; le builder définit leur effet.- Un builder de parsing réutilise l’enchaînement et la propagation des erreurs sans masquer la grammaire.
- Une règle explicite de fin empêche un préfixe valide d’accepter une commande DSL invalide.