Expressions régulières applicatives en tant que foncteur alternatif gratuit


J'attire votre attention sur la traduction d'un merveilleux article frais de Justin Le. Dans son blog dans Code, cet auteur parle dans un langage assez simple de l'essence mathématique de solutions fonctionnelles belles et élégantes pour des problèmes pratiques. Cet article examine en détail un exemple de la manière dont le transfert de la structure mathématique que forment les données d'un domaine à un système de types de programmes peut immédiatement, comme l'écrivirent "et automatiquement" Gerald et Sassman, aboutir à une solution de travail.


Le code montré dans l'image est une implémentation autonome complète et extensible de l'analyseur d'expressions régulières, écrit à partir de zéro. De la magie de première classe et de vrai type!


Aujourd'hui, nous implémentons des expressions régulières et des analyseurs applicatifs (dans l'esprit de la bibliothèque applicative regex ) en utilisant des structures algébriques libres! Les structures libres sont l'un de mes outils préférés à Haskell et j'ai écrit plus tôt sur les groupes libres , les variations sur le thème des monades libres et le foncteur applicatif «libre» sur les monoïdes .


Les expressions régulières (et leurs analyseurs) sont omniprésentes en programmation et en informatique, j'espère donc qu'en démontrant à quel point elles sont faciles à implémenter en utilisant des structures libres, j'aiderai le lecteur à apprécier les mérites de cette approche sans craindre de se perdre dans des détails inutiles.


Tout le code de l'article est disponible en ligne sous la forme d'un «exécutable de pile». Si vous l'exécutez ( ./regexp.hs ), la session GHCi commencera avec toutes les définitions, vous aurez donc la possibilité de jouer avec les fonctions et leurs types.


Cet article sera parfaitement compréhensible pour le "débutant avancé", ou le "spécialiste débutant" à Haskell. Il nécessite la connaissance des concepts de base d'un langage: correspondance de motifs, types de données algébriques et abstractions telles que les monoides, les foncteurs et les notations.


Langues régulières


Une expression régulière est un moyen de définir un langage régulier. Formellement, une telle expression est constituée de trois éléments de base:


  1. Un ensemble vide est un élément qui ne correspond à rien.
  2. Une chaîne vide est un élément neutre qui correspond trivialement à une chaîne vide.
  3. Un littéral est un symbole qui lui correspond. Beaucoup d'un élément.

Et aussi de trois opérations:


  1. Concaténation: RS , séquence d'expressions. Le produit des ensembles (cartésiens).
  2. Alternative: R|S , choix entre les expressions. L'union des ensembles.
  3. Wedge Star: R* , répétition d'une expression un nombre arbitraire de fois (y compris zéro).

Et c'est tout ce qui compose les expressions régulières, ni plus, ni moins. A partir de ces composants de base, vous pouvez construire toutes les autres opérations connues sur des expressions régulières - par exemple, a+ peut être exprimé en aa* et des catégories comme \w peuvent être représentées comme une alternative aux caractères appropriés.


Note du traducteur

La définition minimale ci-dessus d'une langue régulière est assez complète pour un mathématicien, mais peu pratique. Par exemple, l'opération de négation ou d'addition ("n'importe quel caractère sauf le spécifié") peut être écrite dans le cadre de la définition de base, mais son application directe entraînera une augmentation exponentielle des ressources utilisées.


Foncteur alternatif


Lorsque vous regardez la structure des expressions régulières, cela ne vous semble-t-il pas familier? Cela me rappelle beaucoup la classe de type Alternative . Si un foncteur appartient à cette classe, cela signifie que les éléments suivants sont définis pour lui:


  1. Un élément vide correspondant à une défaillance ou une erreur de calcul.
  2. pure x - un seul élément (de la classe Applicative ).
  3. Opération <*> , organisation de calculs séquentiels.
  4. Opération <|> , organisation de calculs alternatifs.
  5. La fonction many consiste à répéter les calculs zéro ou plusieurs fois.

Tout cela est très similaire à la construction d'une langue régulière, non? Peut-être que le foncteur alternatif est presque ce dont nous avons besoin, la seule chose qui manque est la primitive correspondant au caractère littéral.


Toute personne nouvelle dans la classe Alternative peut trouver une bonne introduction à Typeclassopedia . Mais dans le cadre de notre article, cette classe représente simplement un «double monoïde» avec deux façons de combiner <*> et <|> , qui, en un sens, peuvent être comparées aux opérations * et + pour les nombres. En général, pour déterminer un foncteur alternatif, les cinq points ci-dessus et quelques lois supplémentaires de commutativité et de distributivité sont suffisants.


Note du traducteur (ennuyeux)

Pour être précis, l'auteur s'est un peu excité avec le «double monoïde». La classe Alternative étend le foncteur applicatif, qui (sous certaines restrictions) est un semigroupe, à un semiring, où l'opération d'addition <|> avec l'élément neutre empty joue le rôle d'un monoïde commutatif. Opérateur d'application


 (<*>) :: Applicative f => f (a -> b) -> fa -> fb 

ne peut pas agir comme un analogue de l'opération de multiplication dans un semirage, car il ne forme même pas de magma . Cependant, avec l'opérateur <*> , les opérateurs "unilatéraux" *> et <* définis dans le package Control.Applicative . Chacun d'eux ignore le résultat de l'opérande que le "coin" n'affiche pas:


 (<*) :: Applicative f => fa -> fb -> fa (*>) :: Applicative f => fa -> fb -> fb 

Si les types a et b coïncident, alors avec ces opérations on obtient un semi-groupe (l'associativité découle des propriétés de la composition). On peut vérifier que pour un foncteur alternatif, la multiplication est distributive par rapport à l'addition, à droite comme à gauche, et, de plus, l'élément neutre à additionner (analogique de zéro) est un élément absorbant pour l'opération de multiplication.


Les semi-anneaux forment également des nombres, des ensembles, des matrices de demi-anneaux, des types algébriques et ... des expressions régulières, donc, vraiment, nous parlons de la même structure algébrique.


Ainsi, nous pouvons considérer les expressions régulières comme un foncteur alternatif, plus une primitive pour un caractère littéral. Mais, il y a une autre façon de les regarder, et cela nous conduit directement à des structures libres. Au lieu du "foncteur alternatif avec des littéraux", nous pouvons transformer le littéral en une instance de la classe Alternative .


La liberté


Écrivons comme ça. Type pour littéral primitif:


 data Prim a = Prim Char a deriving Functor 

Notez que puisque nous travaillons avec des foncteurs (applicatifs, alternatifs), alors avec toutes nos expressions régulières un certain «résultat» sera associé. En effet, lors de la définition d'une instance pour les Functor , Applicative et Alternative , nous devons avoir un type de paramètre.


D'une part, vous pouvez ignorer ce type, mais d'autre part, vous devez utiliser cette valeur en raison de la correspondance avec une expression régulière, comme cela se fait dans les applications industrielles qui fonctionnent avec des expressions régulières.


Dans notre cas, Prim 'a' 1 :: Prim Int représentera une primitive qui correspond au caractère 'a' et est immédiatement interprétée, résultant en une unité.


Eh bien, maintenant ... donnons à notre primitive la structure mathématique souhaitée en utilisant le foncteur alternatif gratuit de la bibliothèque free :


 import Control.Alternative.Free type RegExp = Alt Prim 

C'est tout! C'est notre type complet pour les expressions régulières! Après avoir déclaré le type Alt une instance de la classe Functor , nous avons obtenu toutes les opérations des classes Applicative et Alternative , car dans ce cas, il existe des instances d' Applicative (Alt f) et Alternative (Alt f) . Maintenant, nous avons:


  • Jeu vide trivial - empty de la classe Alternative
  • Chaîne vide - pure de la classe Applicative
  • Caractère littéral - Basic Prim
  • Concaténation - <*> de la classe Applicative
  • Alternative - <|> de la classe Alternative
  • Kleene Star - many de la classe Alternative

Et tout cela, nous avons été complètement "gratuits", c'est-à-dire "gratuitement"!


Essentiellement, une structure libre ne nous fournit automatiquement qu'une abstraction pour le type de base et rien de plus. Mais les expressions régulières, en elles-mêmes, ne représentent également qu'une structure: des éléments de base et un ensemble d'opérations, ni plus ni moins, de sorte que le foncteur alternatif gratuit nous fournit exactement ce dont nous avons besoin. Pas plus, mais pas moins.


Après avoir ajouté quelques fonctions pratiques de wrapper ... le travail sur le type est terminé!


 -- | charAs:   ,    charAs :: Char -> a -> RegExp a charAs cx = liftAlt (Prim cx) -- liftAlt :: fa -> Alt fa   --   Prim   RegExp -- | char:         char :: Char -> RegExp Char char c = charAs cc -- | string:         string :: String -> RegExp String string = traverse char -- , ? 

Des exemples


Eh bien, essayons? Construisons l'expression (a|b)(cd)*e , qui retourne, en cas de correspondance réussie, le type d'unité () :


 testRegExp_ :: RegExp () testRegExp_ = void $ (char 'a' <|> char 'b') *> many (string "cd") *> char 'e' 

La fonction void :: Functor f => fa -> f () du package Data.Functor supprime le résultat, nous l'utilisons, car nous ne sommes intéressés que par le succès de la comparaison. Mais les opérateurs <|> , *> et many sont utilisés par nous exactement comme cela est supposé lors de la concaténation ou du choix d'une des options.


Voici un exemple intéressant plus compliqué, définissons la même expression régulière, mais maintenant, à la suite de la correspondance, nous calculons le nombre de répétitions de la sous-chaîne cd .


 testRegExp :: RegExp Int testRegExp = (char 'a' <|> char 'b') *> (length <$> many (string "cd")) <* char 'e' 

Il y a une subtilité dans le fonctionnement des opérateurs *> et <* : les flèches indiquent le résultat à enregistrer. Et comme many (string "cd") :: RegExp [String] renvoie une liste d'éléments répétitifs, nous pouvons, en restant à l'intérieur du foncteur, calculer la longueur de cette liste en obtenant le nombre de répétitions.


De plus, l' -XApplicativeDo compilateur GHC -XApplicativeDo permet d'écrire notre expression en utilisant la notation do, ce qui est probablement plus facile à comprendre:


 testRegExpDo :: RegExp Int testRegExpDo = do char 'a' <|> char 'b' cds <- many (string "cd") char 'e' pure (length cds) 

Tout cela est quelque peu similaire à la façon dont nous «capturons» le résultat de l'analyse d'une chaîne à l'aide d'une expression régulière, pour y accéder. Voici un exemple en Ruby:


 irb> /(a|b)((cd)*)e/.match("acdcdcdcde")[2] => "cdcdcdcd" 

la seule différence est que nous avons ajouté du post-traitement pour calculer le nombre de répétitions.


Voici un autre \d qui correspond à un nombre de 0 à 9 et renvoie un nombre:


 digit :: RegExp Int digit = asum [ charAs (intToDigit i) i | i <- [0..9] ] 

Ici, la fonction asum du package Control.Applicative.Alternative représente une sélection parmi les éléments de la liste asum [x,y,z] = x <|> y <|> z , et la fonction intToDigit définie dans le package Data.Char . Et, encore une fois, nous pouvons créer des choses assez élégantes, par exemple, l'expression \[\d\] , correspondant au nombre entre crochets:


 bracketDigit :: RegExp Int bracketDigit = char '[' *> digit <* char ']' 

Analyse


Eh bien, tout ce que nous avons fait était de décrire le type de données pour un littéral avec concaténation, sélection et répétition. Super! Mais ce dont nous avons vraiment besoin, c'est de faire correspondre une chaîne avec une expression régulière, non? Comment un foncteur alternatif gratuit peut-il nous aider? En fait, cela aidera beaucoup. Examinons deux façons de procéder!


Déchargez le foncteur alternatif


Qu'est-ce que la liberté?


La manière canonique d'utiliser une structure libre consiste à la plier en une structure en béton à l'aide d'une algèbre appropriée. Par exemple, la transformation foldMap transforme un monoïde libre (liste) en valeur de n'importe quelle instance de la classe Monoid :


 foldMap :: Monoid m => (a -> m) -> ([a] -> m) 

La fonction foldMap transforme la transformation a -> m en transformation [a] -> m (ou FreeMonoid a -> m ), avec un monoïde spécifique m . L'idée générale est que l'utilisation d'une structure libre vous permet de reporter son utilisation spécifique "pour plus tard", en séparant le moment de la création et le temps d'utilisation de la structure.


Par exemple, nous pouvons construire un monoïde gratuit à partir de nombres:


 -- |  "" `Int`     `Int`,  `liftAlt`. liftFM :: Int -> [Int] liftFM x = [x] myMon :: [Int] myMon = liftFM 1 <> liftFM 2 <> liftFM 3 <> liftFM 4 

Et maintenant, nous pouvons décider comment nous voulons interpréter l'opération <> :
Peut-être cet ajout?


 ghci> foldMap Sum myMon Sum 10 -- 1 + 2 + 3 + 4 

Ou la multiplication?


 ghci> foldMap Product myMon Product 24 -- 1 * 2 * 3 * 4 

Ou peut-être le calcul du nombre maximum?


 ghci> foldMap Max myMon Max 4 -- 1 `max` 2 `max` 3 `max` 4 

L'idée est de reporter la sélection d'un monoïde particulier en créant d'abord une collection gratuite de numéros 1, 2, 3 et 4. Un monoïde libre sur les nombres détermine la structure au-dessus d'eux, ni plus ni moins. Pour utiliser foldMap nous foldMap "comment percevoir le type de base" en passant l'opérateur <> à un monoïde particulier.


Interprétation dans un fonctionnaire d' State


En pratique, obtenir un résultat à partir d'une structure libre consiste à trouver (ou créer) un foncteur adapté qui nous fournira le comportement souhaité. Dans notre cas, nous avons de la chance qu'il existe une implémentation spécifique de la classe Alternative qui fonctionne exactement comme nous en avons besoin: StateT String Maybe .


Le produit <*> de ce foncteur consiste à organiser une séquence de changements d'état. Dans notre cas, sous l'état, nous considérerons le reste de la chaîne analysée, de sorte que l'analyse séquentielle soit la meilleure correspondance pour l'opération <*> .


Et sa somme <|> fonctionne comme un retour en arrière, une recherche avec retour à l'alternative en cas d'échec. Il conserve son état depuis la dernière exécution réussie de l'analyse et y revient si l'alternative est sélectionnée sans succès. C'est exactement le comportement que l'on attend de l'expression R|S


Enfin, une transformation naturelle pour un foncteur alternatif gratuit est appelée runAlt :


 runAlt :: Alternative f => (forall b. pb -> fb) -> Alt pa -> fa 

Ou, pour le type RegExp:


 runAlt :: Alternative f => (forall b. Prim b -> fb) -> RegExp a -> fa 

Si vous n'êtes pas familier avec les types RankN (avec forall b. Construction), vous pouvez voir une bonne introduction ici . Le point ici est que vous devez fournir une fonction runAlt qui fonctionne avec Prim b pour absolument n'importe quel b , et pas de type particulier, comme Int et Bool , par exemple. Autrement dit, comme avec foldMap nous n'avons qu'à spécifier ce qu'il faut faire avec le type de base. Dans notre cas, répondez à la question: "Que faut-il faire avec le type Prim ?"


 processPrim :: Prim a -> StateT String Maybe a processPrim (Prim cx) = do d:ds <- get guard (c == d) put ds pure x 

Il s'agit d'une interprétation de Prim comme une action dans le contexte de la StateT String Maybe - StateT String Maybe , où l'état est une chaîne StateT String Maybe . Permettez-moi de vous rappeler que Prim contient des informations sur le caractère correspondant c et son interprétation sous la forme d'une valeur de x . Prim traitement Prim compose des étapes suivantes:


  • En utilisant get état (pas encore une partie analysée de la chaîne) et imprimons immédiatement son premier caractère et le reste. Si la ligne est vide, une alternative reviendra. ( Le transformateur StateT agit à l'intérieur du foncteur Maybe, et s'il est impossible de faire correspondre le motif sur le côté droit de l'opérateur <- à l'intérieur du bloc do, les calculs se termineront avec le résultat empty , c'est-à-dire Nothing . Env. Trans. ).
  • Nous utilisons l'expression de garde pour faire correspondre le caractère actuel avec le caractère donné. En cas d'échec, le empty retourné, et nous passons à l'option alternative.
  • Nous changeons l'état en remplaçant la chaîne analysée par sa «queue», car à ce moment le caractère actuel peut déjà être considéré comme analysé avec succès.
  • Nous retournons ce que la primitive Prim devrait retourner.

Vous pouvez déjà utiliser cette fonction pour mapper RegEx à un préfixe de chaîne. Pour ce faire, vous devez démarrer les calculs à l'aide de runAlt et runStateT , en passant la chaîne analysée à la dernière fonction comme argument:


 matchPrefix :: RegExp a -> String -> Maybe a matchPrefix re = evalStateT (runAlt processPrim re) 

C'est tout! Voyons comment fonctionne notre première solution:


 ghci> matchPrefix testRegexp_ "acdcdcde" Just () ghci> matchPrefix testRegexp_ "acdcdcdx" Nothing ghci> matchPrefix testRegexp "acdcdcde" Just 3 ghci> matchPrefix testRegexp "bcdcdcdcdcdcdcde" Just 7 ghci> matchPrefix digit "9" Just 9 ghci> matchPrefix bracketDigit "[2]" Just 2 ghci> matchPrefix (many bracketDigit) "[2][3][4][5]" Just [2,3,4,5] ghci> matchPrefix (sum <$> many bracketDigit) "[2][3][4][5]" Just 14 

Attendez, c'était quoi?


Il semble que tout s'est passé un peu plus vite que prévu. Il y a une minute, nous avons écrit notre primitive, et puis encore! et l'analyseur de travail est prêt. Voici, en fait, tout le code clé, quelques lignes en Haskell:


 import Control.Monad.Trans.State (evalStateT, put, get) import Control.Alternative.Free (runAlt, Alt) import Control.Applicative import Control.Monad (guard) data Prim a = Prim Char a deriving Functor type RegExp = Alt Prim matchPrefix :: RegExp a -> String -> Maybe a matchPrefix re = evalStateT (runAlt processPrim re) where processPrim (Prim cx) = do d:ds <- get guard (c == d) put ds pure x 

Et avons-nous un analyseur d'expressions régulières entièrement fonctionnel? Qu'est-il arrivé?


Rappelons qu'à un niveau d'abstraction élevé, Alt Prim contient déjà pure , empty , Prim , <*> , <|> , et many dans sa structure (il y a une subtilité désagréable avec cet opérateur, mais plus sur cela plus tard). runAlt utilise le comportement d'un foncteur alternatif particulier (dans notre cas, StateT String Maybe ) pour contrôler le comportement des opérateurs pure , empty , <*> , <|> et de many opérateurs. Cependant, StateT n'a pas d'opérateur intégré similaire à Prim , et pour cela, nous avions besoin d'écrire processPrim .


Ainsi, pour le type Prim , la fonction runAlt utilise runAlt , et pour pure , empty , <*> , <|> et many , une instance appropriée de la classe Alternative est utilisée. Ainsi, 83% du travail est effectué pour nous par le foncteur StateT , et les 17% restants sont effectués par StateT . En vérité, c'est quelque peu décevant. On peut se demander: pourquoi était-il même nécessaire de commencer avec le wrapper Alt ? Pourquoi ne pas définir immédiatement le type RegExp = StateT String Maybe et le char :: Char -> StateT String Maybe Char primitif approprié char :: Char -> StateT String Maybe Char ? Si tout est fait dans le StateT StateT, alors pourquoi s'embêter avec Alt - un foncteur alternatif gratuit?


Alt principal avantage d' Alt sur StateT est que StateT est ... un outil assez puissant. Mais en fait, il est puissant, au point d'absurdité. Il peut être utilisé pour représenter un grand nombre des calculs et des structures les plus divers, et, désagréablement, il est facile d'imaginer quelque chose qui n'est pas une expression régulière. Disons quelque chose de basique comme put "hello" :: StateT String Maybe () ne correspond à aucune expression régulière valide, mais il est du même type que RegExp () . Ainsi, alors que nous disons que Alt Prim correspond à une expression régulière, ni plus, ni moins, nous ne pouvons pas en dire StateT String Maybe avec StateT String Maybe . Le type Alt Prim est la représentation parfaite d'une expression régulière. Tout ce qui peut être exprimé avec son aide est une expression régulière, mais tout ce qui n'est pas une telle expression ne peut pas être exprimé avec son aide. Ici, cependant, il y a aussi quelques subtilités désagréables associées à la paresse de Haskell, plus à ce sujet plus tard.


Ici, nous ne pouvons considérer StateT que comme un contexte utilisé pour un
interprétations d'expressions régulières - en tant qu'analyseur. Mais vous pouvez imaginer d'autres façons d'utiliser RegExp . Par exemple, nous pouvons avoir besoin de sa représentation textuelle, ce que StateT ne permettra pas.


Nous ne pouvons pas dire que StateT String Maybe est une expression régulière, mais seulement que ce foncteur peut représenter un analyseur basé sur des grammaires régulières. Mais à propos d' Alt Prim nous pouvons dire avec certitude qu'il s'agit d'une expression régulière ( comme disent les mathématiciens, ils sont égaux à l'isomorphisme, environ Trans. ).


Utilisation directe de la structure libre


Tout cela, bien sûr, est très bon, mais que se passe-t-il si nous ne voulons pas déplacer 83% du travail en code pour un type qui a été écrit par quelqu'un pour nous. Est-il possible d'utiliser directement la structure Alt gratuite pour écrire un analyseur? Cette question est similaire à celle-ci: comment écrire une fonction qui traite les listes (en faisant correspondre les constructeurs (:) et [] ) au lieu d'utiliser uniquement foldMap ? Comment opérer directement avec cette structure au lieu de déléguer le travail à un monoïde spécifique?


Heureux que vous ayez demandé! Jetons un coup d'œil à la définition d'un foncteur alternatif gratuit:


 newtype Alt fa = Alt { alternatives :: [AltF fa] } data AltF fa = forall r. Ap (fr) (Alt f (r -> a)) | Pure a 

Il s'agit d'un type inhabituel défini par récursivité mutuelle, il peut donc sembler très déroutant. Une façon de le comprendre est d'imaginer que Alt xs contient une chaîne d'alternatives formée en utilisant l'opérateur <|> . AltF , f , <*> ( ).


AltF fa [fr] , r . Ap (:) , fr , Pure[] . forall r. -XExistentialQuantification .


, Alt f , . , ( ) <*> <|> , , [a] <> .


, :


  • () — , <> :
     [a,b,c,d] = [a]<>[b]<>[c]<>[d] 
  • () — , + , — , * :
     a*(b+c)+d*(x+y+z)*h 
  • (Alt f) — , <|> , — , <*> :
     fa <*> (fb <|> fc) <|> fd <*> (fx <|> fy <|> fz) <*> fh 

, RegExp a -> String -> Maybe a , , . : .


, Alt . , , .


 matchAlts :: RegExp a -> String -> Maybe a matchAlts (Alt res) xs = asum [ matchChain re xs | re <- res ] 

asum :: [Maybe a] -> Maybe a , Just . ( , Maybe a AlternativeNothing , <|> . . . )


. AltF , Ap Pure :


 matchChain :: AltF Prim a -> String -> Maybe a matchChain (Ap (Prim cx) next) cs = _ matchChain (Pure x) cs = _ 

" ": GHC "", , , . ( Haskell "" (holes) , _ , . . . ) :


 matchChain :: AltF Prim a -> String -> Maybe a matchChain (Ap (Prim cx) next) cs = case cs of [] -> Nothing d:ds | c == d -> matchAlts (($ x) <$> next) ds | otherwise -> Nothing matchChain (Pure x) _ = Just x 

Ap ( (:) ), , - . , . Prim r , , next :: RegExp (r -> a) . , next . , "" , Nothing . , Pure x ( [] ), , , .


, , . , , " " Ap , Pure , AltF .., .


:


 ghci> matchAlts testRegexp_ "acdcdcde" Just () ghci> matchAlts testRegexp_ "acdcdcdx" Nothing ghci> matchAlts testRegexp "acdcdcde" Just 3 ghci> matchAlts testRegexp "bcdcdcdcdcdcdcde" Just 7 ghci> matchAlts digit "9" Just 9 ghci> matchAlts bracketDigit "[2]" Just 2 ghci> matchAlts (many bracketDigit) "[2][3][4][5]" Just [2,3,4,5] ghci> matchAlts (sum <$> many bracketDigit) "[2][3][4][5]" Just 14 

. , () , . , , .


?


foldMap . , , foldMap, , , , ! , — , — (:) [] .


, , : , , , (:) , [] . , . , [1,2,3] <> [4] , [1] <> [2,3] <> [4] . , , .


. , :


 data RegExp a = Empty | Pure a | Prim Char a | forall r. Seq (RegExp r) (RegExp (r -> a)) | Union (RegExp a) (RegExp a) | Many (RegExp a) 

RegExp , . . , RegExp :


 -- | a|(b|c) abc1 :: RegExp Int abc1 = Prim 'a' 1 `Union` (Prim 'b' 2 `Union` Prim 'c' 3) -- | (a|b)|c abc2 :: RegExp Int abc2 = (Prim 'a' 1 `Union` Prim 'b' 2) `Union` Prim 'c' 3 

, .


Alt Prim , , , . , matchAlts , . (a|b)|c a|(b|c) . Alt . , , .


, , (a|b)|c , (a|b)|c , , , RegExp . Alt , .


, , Alt , Alt Prim . , Alt Prim a|a a . , Alt f f . , : . , , , .



, . , , . RegExp , , — .


, Haskell. , - [a] . ( , - , "" , - "" . . trans. )


: a|aa|aaa|aaaa|aaaaa|aaaaaa|... , ( , , , . . . ). , Haskell . , Alt many . , a* a|aa|aaa|aaaa|aaaaa|aaaaaa|... , . - many (char 'a') , . Haskell Alt , .


, , , , (), . , many .


, ! "" Alt , Control.Alternative.Free.Final , many (, , ).


, , runAlt . , Alternative , many ( RE regex-applicative ) . , Haskell , , , many .


, . ( ), ( , , . . ). matchPrefix , , , . , , , , . , GHC .



, , tails ( ) mapMaybe ( ). , , listToMaybe .


 matches :: RegExp a -> String -> [a] matches re = mapMaybe (matchPrefix re) . tails firstMatch :: RegExp a -> String -> Maybe a firstMatch re = listToMaybe . matches re 

, , matchPrefix Nothing , listToMaybe , Nothing ( , . . . ).


, . , , — , . , . , , , .


Alt Prim , : , , , .


? . :


 data Prim a = Only Char a --    | Letter a --     | Digit (Int -> a) --    , | Wildcard (Char -> a) --    , | Satisfy (Char -> Maybe a) --    , --   

, . .


, , . runAlt Alt .


(). , , , , . | . ( , . . . ). , - . , MonadPlus - , , . , .


, , . , !

Source: https://habr.com/ru/post/fr448644/


All Articles