Dérivés d'expressions régulières expliqués à l'aide de Pac-Man
Manger des cerises rouges vous donne la possibilité de manger des fantômes bleus. L'idée que des dérivées puissent être utilisées pour créer un algorithme de correspondance d'expressions régulières est presque aussi ridicule. Laissez-moi vous expliquer comment cet algorithme fonctionne et comment il se rapporte à Pac-Man.
En 1964, Brzozowski a publié le premier article sur les dérivés d'expressions régulières . C'est de loin l'un de mes algorithmes préférés. En utilisant des dérivés d'expressions régulières, nous pouvons implémenter un algorithme pour faire correspondre les expressions régulières. Cet algorithme est très :
- Facile
- fonctionnel
- facilement extensible avec vos propres opérateurs
Dans cet article, je vais vous montrer comment nous pouvons faire correspondre une chaîne avec une expression régulière en utilisant seulement deux fonctions pures et quelques analogies Pac-Man. Si vous préférez, vous pouvez regarder la vidéo suivante au lieu de lire l'article car elle couvre le même sujet :
Récapitulatif des expressions régulières
Tout d'abord, passons en revue rapidement les expressions régulières pour nous assurer que nous sommes sur la même page. L'expression a(a|b)*
correspond à une chaîne qui commence par un a
, suivi d'un nombre quelconque de a
's et b
's.
- La chaîne
ab
correspondraa(a|b)*
. Nous l'indiquerons avec un fantôme bleu comestible. - La chaîne
aabbba
correspond égalementa(a|b)*
puisqu'elle commence par una
et est suivie de plusieursa
's etb
's. - Ensuite, la chaîne
ac
ne correspond pas àa(a|b)*
, car la regex ne correspond à aucunc
et notre regex ne fait aucune correspondance de sous-chaîne. Nous l'indiquerons en utilisant un fantôme rouge qui poursuit le Pac-Man. - Enfin, la chaîne
ba
ne correspond pas non plusa(a|b)*
car elle ne commence pas par una
.
Présentation de l'algorithme
Avant d'entrer dans les détails, voyons un aperçu du fonctionnement de cet algorithme. J'ai conçu un jeu étrange de Pac-Man où vous ne pouvez manger les fantômes que si vous mangez les fruits dans une séquence qui correspond à la regex. Notre Pac-Man représente la regex aba*
. Il a le chapelet de fruits suivant à manger : une pomme, puis une banane, puis une pomme : aba
.
- Lorsque nous commençons, le fantôme nous poursuit et l'expression régulière qu'il nous reste à faire correspondre est
aba*
. - Nous mangeons la première pomme, et l'expression régulière qu'il nous reste maintenant à faire correspondre est
ba*
. Le fantôme nous poursuit toujours car le fruit que nous avons mangé jusqu'à présent, la pomme, ne correspond pas à la regex. - Ensuite, nous mangeons la banane. La regex qu'il nous reste alors à faire correspondre est
a*
. Maintenant, le fantôme commence à s'enfuir car, techniquement,ab
correspond déjà àaba*
. - Nous pouvons essayer de manger le fantôme ou de manger une autre pomme, auquel cas l'expression régulière qu'il nous reste à faire correspondre est toujours
a*
. Le fantôme s'enfuit toujours caraba
correspond également à l'expression régulièreaba*
.
Il y a une autre fonction à l'œuvre ici. La fonction vérifie si le fantôme poursuit le Pac-Man ou si le Pac-Man a déjà trouvé la regex et poursuit le fantôme. Cette fonction s'appelle la fonction nullable ; il vérifie si l'expression régulière restante correspond à la chaîne vide. Il peut le faire car si la regex restante correspond à la chaîne vide, le fruit qu'il a mangé doit déjà avoir été suffisant pour satisfaire la regex.
Algorithme d'appariement des dérivées
Cela signifie que nous n'avons besoin que de deux fonctions pour écrire l'algorithme d'appariement dérivé :
- la fonction dérivée
- la fonction nullable
Un en Golang pour les programmeurs impératifs :
et un autre en Haskell pour les programmeurs fonctionnels :
Ces deux fonctions sont équivalentes et simplement écrites dans des styles de programmation différents. Dans le code Haskell, foldl
également appelé pli gauche ou réduction dans d'autres langages, fait le travail de la boucle for pour vous. De plus, dans Haskell, nous n'avons pas besoin de virgules pour passer des paramètres aux fonctions ; puisque l'application de fonction est l'opération la plus courante dans un langage de programmation fonctionnel, nous utilisons l'espace pour délimiter les paramètres.
Maintenant, approfondissons la façon d'implémenter les fonctions nulles et dérivées.
Digression de l'histoire de l'origine de Pac-Man
Mais avant de faire cela, je ne sais pas si vous vous êtes déjà interrogé sur l'histoire d'origine de Pac-Man. Je soutiens qu'il n'y avait pas de baril de déchets nucléaires dans lequel Pac-Man est tombé, ce qui a permis à Pac-Man d'acquérir le pouvoir de manger des fantômes. La logique est beaucoup plus simple.
Pac-Man est un fruit ! Quand Pac-Man mange d'autres fruits, Pac-Man est un cannibale. Donc, si jamais vous êtes poursuivi par un fantôme, vous devez manger de la chair humaine, et le fantôme devrait, au moins temporairement, commencer à vous fuir. Maintenant, je n'ai pas essayé cela moi-même, mais la logique semble solide.
Cela explique pourquoi les zombies poursuivent toujours les humains. Comme l'a dit un jour David Attenborough :
« Les zombies qui nous poursuivent sont eux-mêmes poursuivis par des fantômes que nous ne pouvons pas voir. Après que les zombies aient mangé une partie de notre chair humaine, vous les verrez montrer l'étrange comportement de mâcher l'air, c'est le zombie qui mange le fantôme qui le poursuivait auparavant.
Opérateurs de base
L'implémentation des fonctions nullables et dérivées nous oblige d'abord à définir les opérateurs de base disponibles dans nos expressions régulières.
Nous pouvons considérer une expression régulière comme décrivant un ensemble de chaînes.
- Cela signifie que l'ensemble vide représente un opérateur qui ne correspond à aucune chaîne.
- La chaîne vide représente un ensemble singleton d'une seule chaîne qui correspond uniquement à la chaîne vide.
- Le caractère représente également un ensemble singleton qui correspond uniquement au caractère unique
a
. - Nous pouvons ensuite combiner ces expressions régulières de base à l'aide d'opérateurs tels que :
or
,concatenation
etKleene star
, oùr
ets
représente les deux expressions d'expression régulière que nous combinons.
Fonction Nullable
Nous pouvons commencer par la fonction nullable. Examinons quelques exemples et déterminons laquelle de ces expressions régulières correspond à la chaîne vide pour avoir un aperçu de la façon dont cette fonction est implémentée.
a*
correspond à la chaîne vide car zéro ou plus inclut zéro.(a*|b)
correspond à la chaîne vide puisque le côté gauche du ou correspond à la chaîne vide.ab
ne correspond pas à la chaîne vide, puisqu'il ne correspond qu'à la chaîneab
ab*
ne correspond pas non plus à la chaîne vide, carab*
nécessite une chaîne qui commence par una
(a|b)
ne correspond pas à la chaîne vide, car ni le côté gauche ni le côté droit de neor
correspondent à la chaîne vide.
Voici l'implémentation de la fonction nullable. Le côté gauche représente les valeurs transmises à la fonction et le côté droit représente l'implémentation de la fonction dans ce cas. Les fantômes rouges représentent faux et les fantômes bleus représentent vrai :
- L'ensemble vide ne correspond pas à la chaîne vide puisqu'il ne correspond à aucune chaîne.
- La chaîne vide correspond à la chaîne vide car elle ne correspond qu'à la chaîne vide.
- Le caractère
a
ne correspond pas à la chaîne vide car il ne correspond qu'au caractèrea
. - Si nous avons une logique
or
, nous devons vérifier les deux côtés. Si l'un ou l'autre correspond à la chaîne vide, la logiqueor
correspond à la chaîne vide. - Pour que les
concatenation
deux expressions régulières correspondent à la chaîne vide, elles doivent toutes deux correspondre à la chaîne vide. - Et enfin, si nous avons
zero or more
de quelque chose, cela inclut zéro, ce qui signifie qu'il correspond toujours à la chaîne vide.
- Notre opérateur supérieur est le
or
this signifie que nous devons vérifier la nullabilité des côtés gauche et droit :b
eta*
. - Nous vérifions et voyons que le caractère
b
sur le côté gauche n'est pas nullable :false
. - Ensuite, nous vérifions et voyons que le
a*
sur le côté droit est nullable :true
. - Maintenant, nous sommes revenus
false
ettrue
nous pouvonsor
les obtenirtrue
.
Exercices nullables
Essayez de parcourir l'implémentation et vérifiez si les expressions régulières suivantes acceptent les valeurs NULL. Vous pouvez cliquer dessus pour vérifier votre réponse :
- une
- un*(b*|∅)
- εa
- ∅*
- (∅|b)*(abc|ε)
Avant d'examiner l'implémentation de la fonction, examinons des exemples de dérivée. Ici nous allons prendre la dérivée de quelques expressions régulières, toutes par rapport au caractère a
:
- L'expression régulière qui reste à trouver après
a*
avoir mangé unea
pomme est toujoursa*
. - La dérivée de
ab*
par rapport àa
estb*
, puisque nous avons déjà identifié le préfixea
. - La dérivée de
(a|b)b
par rapport àa
estb
. - La dérivée de
b|(a*b)
par rapport àa
esta*b
. La gaucheb
ne correspondait pas, nous pouvions donc la jeter et laa
était consommée par leszero or more
a
's à droite. - Ensuite, nous avons
ab*
, celui-ci est un peu délicat. Après qu'il ait mangé la pomme, l'expression régulière qui reste à faire correspondre estb(ab)*
. Puisque nous n'avons fait qu'apparier lea
, nous nous attendons à en voir au moins un de plusb
.
- La dérivée de l'ensemble vide est toujours l'ensemble vide. Il n'y a aucun moyen de récupérer puisque l'ensemble vide ne correspond à rien.
- La dérivée de la chaîne vide concernant tout caractère est l'ensemble vide. Il ne s'attendait pas à correspondre à un personnage. Il ne correspondra qu'à la chaîne vide.
- La dérivée d'un caractère unique à un caractère similaire (dans ce cas, le
a
pple) est la chaîne vide car après qu'elle se soit trouvée, il ne reste plus que la chaîne vide à correspondre. - La dérivée d'un caractère par rapport à un caractère différent qui n'est pas égal, dans ce cas, l'
b
anana, est l'ensemble vide puisque nous n'avons pas apparié le caractère spécifique. - La dérivée d'une
or
expression est laor
des dérivées. Il repousse simplement le problème à ses enfants. - La dérivée de l'
concat
expression doit considérer si elle peut sauter la première expression. Il ne peut ignorer la première expression que si la première expression correspond à la chaîne vide et est nullable. Donc, la première chose que nous faisons est de vérifier cela. Pensons au cas où il ne peut pas ignorer la première expression lorsque l'expressionr
n'est pas nullable. Alors la dérivée est la dérivée de la première expressionconcatenated
avec la seconde expressions
. Si nous pouvons ignorer la première expression régulière, nous devons envisager une alternative qui n'est que la dérivée de la deuxième expression. Nous pouvons alorsor
les deux alternatives de sauterr
et de ne pas sauterr
et de retourner cela en conséquence. - Enfin, nous avons l'
star
opérateur. Il correspond à une expression zéro ou plusieurs fois. Puisqu'on nous passe un caractère, ce n'est pas le cas zéro. Il faut donc considérer leone or more
cas. Cela signifie que nous devons prendre la dérivée de l'expression à l'intérieur destar
etconcatenate
à nouveau avec l'zero or more
expression.
Exemple dérivé 1
Prenons la dérivée de (ab)*
par rapport à a
.
(ab)*
est une zero or more
expression, nous nous tournons donc vers la zero or more
règle. Nous voyons que cela nécessite de prendre la dérivée de l'expression à l'intérieur du star
.
C'est le concatenation
de a
et b
. Nous vérifions donc si le côté gauche est nullable et le caractère a
n'est pas nullable. Cela signifie que nous ne pouvons pas passer outre. Il faut prendre la dérivée de a
par rapport à a
. Mais c'est la chaîne vide, donc si nous concatenate
la chaîne vide avec le côté droit, qui était b
, nous obtenons b
.
Maintenant, nous revenons au zero or more
, souvenez-vous que nous avons pris la dérivée de ab
par rapport à a
et avons récupéré a b
. Maintenant, nous pouvons concaténer cela avec à (ab)*
nouveau et nous obtenons b(ab)*
.
Exemple dérivé 2
Prenons la dérivée de (a*ba)
par rapport à b
.
a*
est concaténé avecba
, nous examinons donc la règle de concaténation.- Nous vérifions si le côté gauche
a*
est nullable, ce qui est le cas. Cela signifie que nous pouvons l'ignorer, ce qui signifie également que nous devons créer une combinaisonor
de deux dérivées. - Le côté gauche finit par ne pas correspondre, car
a*
ne correspond pas àb
. - Heureusement, nous avons une alternative le
ba
. La dérivée deba
par rapport àb
est eta
.
J'ai sauté quelques détails ici. Considérez cela comme un exercice pour vérifier mon travail en parcourant vous-même la fonction.
Exercices dérivés
Essayez de parcourir l'implémentation et vérifiez quelles sont les dérivées des expressions régulières suivantes par rapport à b
. Vous pouvez cliquer dessus pour vérifier votre réponse :
- εb
- b*(b|c)
- un*(b|c)
- bεb
- ∅*b
J'espère que vous comprenez maintenant pourquoi manger des cerises rouges vous donne la possibilité de manger des fantômes bleus et comment implémenter un matcher d'expression régulière à l'aide de l'algorithme dérivé.
Nous avons couvert l'algorithme de travail de base ici, mais il existe de nombreuses façons de rendre cet algorithme encore meilleur avec de très petites modifications. Nous avons triché et passé sous silence les règles de simplification dans cet article, en les utilisant sans les expliquer, ce qui deviendra particulièrement évident si vous parcourez les exercices. Nous n'avons pas non plus discuté de la façon dont vous pouvez utiliser la mémorisation pour construire paresseusement un automate efficace.
Nous pouvons également facilement étendre l'algorithme pour inclure de nouveaux opérateurs tels que not
, interleave
et même prendre en charge les grammaires sans contexte. Je vais discuter de certains de ces sujets dans le prochain article .
En attendant, j'aimerais voir votre implémentation de cet algorithme dans un langage de programmation avec lequel vous êtes le plus à l'aise. Merci de m'envoyer un lien dans les commentaires.
Merci
- Brink van der Merwe pour avoir pris le temps de m'expliquer cet algorithme.
- Brzozowski, Janusz A. "Dérivés d'expressions régulières." Journal de l'ACM (JACM) 11.4 (1964): 481–494.
- Owens, Scott, John Reppy et Aaron Turon. "Les dérivés d'expression régulière réexaminés." Journal de programmation fonctionnelle 19.2 (2009): 173–190.