هاسڪل ۾ ’مونادڪ پارسينگ‘ جو ٻيهر جائزو وٺڻ

4 فيبروري 2018 تي پوسٽ ڪيلٽيگ: haskell ، پروگرامن ، monads

هاسڪل ۾ ’مونادڪ پارسينگ‘ هڪ نن aو مقالو آهي ، جيڪو پارسيڪ ۽ اٽيپرسيڪ وانگر لائبريرين لاءِ بنيادي بنياد رکي ٿو. جيتوڻيڪ اهو 1998 ۾ شايع ٿيو هو (تقريبن 20 سال اڳ!) اهو عمر خشڪ نموني سان ڳري چڪو آهي ۽ ڪوڊ جا نمونا تقريباً ڪنهن به تبديلي آڻڻ سان هلندو. جيتوڻيڪ ، هن وقت کان وٺي اسٽيٽ آف اسٽيٽ ترقي ڪئي آهي ۽ منهنجو خيال آهي ته جديد هاسڪل جو استعمال انهي مواد کي پيروي ۽ لاڳو ڪرڻ ۾ آسان بڻائي سگهي ٿو.

هاسيل ۾ مونادڪ پارسنگ اها آهي جيڪا مون ٽنهي تي وڪڻي. Haskell کان اڳ تصريف سان منهنجي تجربا lexers ۽ جيان wrangling اوزار لاء شامل بگي regexes هئا bison۽ flex، ۽ باقي مون ٻڌو هوندي ته Haskell تصريف لاء سٺو هو مون کي ڏسي نه سگهيو هن اهو کڻي ڪيئن به ٿي سگهي ٿو جڏهن مون کي ڪنهن به مضبوط regex نه ڏسي سگهي لائبريريون! ڪجهه دستاويزن ۾ هڪ طرف مون کي Attoparsec ڏانهن اشارو ڪيو ۽ جڏهن مون ڏٺو ته RFC2616 پارسر مثال طور اهو جادو ٽڪري وانگر لڳي رهيو آهي. اهو ڪيترو نن smallڙو ٿي سگهي ٿو؟ ڪجهه هفتن ان جي ڪوشش ڪرڻ کان پوءِ آئون پاڻ مڃي ويو هوس ۽ مون ڪڏهن به پوئتي نه ڏٺو آهي. مونادن جي اها پهرين درخواست هئي مون سان ملاقات ڪئي جيڪا اصل ۾ منهنجي زندگي کي آسان بڻائي ٿي ، ۽ مون اهو محسوس ڪرڻ شروع ڪيو ته مونگ کان وڌيڪ مونجهات هئي ۽ نئين ايندڙن تائين ناقابل رسائي ٿيڻ جي ڪري.

پهرين تبديلي جيڪا آئون ڪرڻ ٿي چاهيو اها قسم جي تعريف آهي. ڪاغذ استعمال ڪندو آهي قسم

newtype Parser a = Parser (String -> [(a,String)])

۽ جيتوڻيڪ اها ڪافي مشهور تعريف آهي ته ان جي پنهنجي شاعريءَ آهي ، منهنجي خيال ۾ فهرستن جي لچڪ هتي ضايع ٿي وئي آهي. ليکڪ ان کي استعمال نه ڪندا آهن ، ۽ بدران هڪ ‘تعصب پسند انتخاب’ آپريٽر جي تعريف (+++)ڪندا آهن جيڪي وڌ ۾ وڌ هڪ نتيجو ڏيندو آهي ۽ انهي جي بدران هر جاءِ تي استعمال ڪندو آهي. هاسيل ۾ اڳ ۾ ئي هڪ مڪمل طور تي بهتر ڊيٽا ڊيٽابيس موجود آهي ، گهڻن ئي عنصرن جي فهرستن جي لاءِ Maybe، تنهنڪري آئون ان بدران استعمال ڪندس []:

newtype Parser a = Parser (String -> Maybe (a, String))

اسان رکجي ته Stringڪرڻ s۽ Maybeڪرڻ m، هڪ کان وڌيڪ دلچسپ طرز وحي ڪيو ويندو آھي:

newtype Parser s m a = Parser (s -> m (a, s))

هي آهي StateT! هن نموني کي سڃاڻڻ مثال جي وضاحت ڏا easierي آسان ڪري ٿي ، انهي حقيقت ۾ تمام گهڻو آسان آهي ته GHC اهو اسان سان ڪري سگهي ٿو پاڻ سان -XGeneralizedNewtypeDeriving! مڪمليت لاءِ مان ان ڪم جي لالچ ۾ مزاحمت ڪندس ، پر توهان انهي سان پاڻ آزمائي سگهو ٿا

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
newtype Parser a = Parser (StateT String Maybe a) deriving (Functor, Applicative, Alternative, Monad)

ٻي تبديلي مڪمليت لاءِ پڻ آهي: ليکڪ سڌي طرح Monadمثال ۾ گهڙي ڌار ٿا سمجهن Functor۽ Applicativeپهرين. صحيح هجڻ جي ڪري ، Applicativeخلاصو اڃا تائين دريافت نه ڪيو ويو هو ، ۽ اهو پڻ سبب آهي جو ليکڪ عام طور تي وضاحت ڪن ٿا mzero۽ mplus(جنهن کي اهي سڏين ٿا (++)) بدران وڌيڪ عام Alternativeطريقن empty۽ (<|>). اسان جي Maybeتبديلي جي ، تعريف Alternativeجو مطلب آهي ته مون کي انهن سان تنگ ڪرڻ جي ضرورت نه هوندي (+++).

آخر ۾ ، آئون ڪوشش ڪندس جتي ممڪن نه هجي وڌيڪ اطلاق واري انداز جي حق ۾ ، جتي استعمال  <*>ڪري سگهجي ، (جنهن کي ظاهر ڪري سگهجي ٿو ’اسپلٽ‘ جيڪڏهن توهان وٽ هن لاءِ اڳ ئي نالو ناهي) ڇاڪاڻ ته انهن اڪثر گهڻن اهو ان جي ضرورت آهي.

اچو ته شروع ڪريون!

{-# LANGUAGE InstanceSigs #-}

import Control.Applicative (Alternative(..))
import Control.Monad.Trans.State.Strict
import Control.Monad (guard)
import Data.Char (isSpace, isDigit, ord)

سهولت لاءِ مان هڪ تعريف ڪئي آهي unParserجنهن Parser aکي پنهنجي هيٺاهين تائين نه پهچايو آهي StateT String Maybe a.

newtype Parser a = Parser { unParser :: StateT String Maybe a }
runParser = runStateT . unParser

fmapايترو ئي آسان آهي جيترو ڪتب آڻڻ Parser۽ هيٺيان استعمال ڪرڻ وارن کي استعمال ڪرڻ StateTآهي fmap.

instance Functor Parser where
    fmap :: (a -> b) -> Parser a -> Parser b
    fmap f p = Parser $ f <$> unParser p

لاءِ وڌيڪ ناپسنديده Applicative۽ Alternative.

هن Alternativetypeclass اسان کي هڪ parser يا ٻئي parser ڊوڙندو جو خيال ظاهر ڪرڻ لاء، پهرين ڪامياب ڪمپوز نتيجي ۾ اجازت ڏئي ٿو. emptyاهو معاملو سنڀاليندو آهي جتي ٻئي پيرز ناڪام ٿيا ، ۽ (<|>)(جنهن کي ‘alt ’ظاھر ڪري سگهجي ٿو) متبادل انجام ڏئي ٿو. هن پنهنجي تي آسان ڪافي آهي، پر Alternativeپڻ مهيا ڪري many۽ someجنهن کي انهيء چونڊجندڙ many۽ many1ڪاغذ کان:

-- many v = some v <|> pure []
-- some v = (:) <$> v <*> many v

پر صرف انهي []سان بدلي ڪرڻ کانپوءِ Maybeمون هتي ڪيو آهي جيڪو انهي (<|>)سان مطابقت رکي ٿو (+++).

instance Applicative Parser where
    pure :: a -> Parser a
    pure a  = Parser $ pure a
    (<*>) :: Parser (a -> b) -> Parser a -> Parser b
    f <*> a = Parser $ unParser f <*> unParser a

instance Alternative Parser where
    empty :: Parser a
    empty   = Parser empty
    (<|>) :: Parser a -> Parser a -> Parser a
    a <|> b = Parser $ unParser a <|> unParser b

هي Monadتعريف ڪجهه وڌيڪ دلچسپ آهي ، ڇاڪاڻ ته اسان کي هٿرادو طور ٺاهڻ گهرجي StateTقيمت ، پر اهو پڻ ٻيهر ختم ڪرڻ ۽ بي ترتيب ڪرڻ تي.

instance Monad Parser where
    (>>=) :: Parser a -> (a -> Parser b) -> Parser b
    a >>= f = Parser $ StateT $ \s -> do
        (a', s') <- runParser a s
        runParser (f a') s'

نوٽ اهو ته anyCharصرف اهو ئي ڪم آهي جيڪو دستي طور تي هڪ ٺاهيندو آهي Parser، ۽ satisfyصرف هڪ ئي آهي جيڪو Monadانٽرفيس جي ضرورت آهي .

anyChar :: Parser Char
anyChar = Parser . StateT $ \s -> case s of
    []     -> empty
    (c:cs) -> pure (c, cs)

satisfy :: (Char -> Bool) -> Parser Char
satisfy pred = do
    c <- anyChar
    guard $ pred c
    pure c

char :: Char -> Parser Char
char = satisfy . (==)

string :: String -> Parser String
string []     = pure []
string (c:cs) = (:) <$> char c <*> string cs

ٻيهر ، many۽ many1وضاحت ڪرڻ جي ضرورت نه آهي ڇاڪاڻ ته اهي مفت مهيا ڪيا ويا آهن!

sepBy :: Parser a -> Parser b -> Parser [a]
sepBy p sep = (p `sepBy1` sep) <|> pure []

sepBy1 :: Parser a -> Parser b -> Parser [a]
sepBy1 p sep = (:) <$> p <*> many (sep *> p)

اهي ڪاغذ ۾ ڏنل تعريفن وانگر تقريبن ساڳيون آهن. مون مڪمليت chainrلاءِ شامل ڪيو آهي.

chainl :: Parser a -> Parser (a -> a -> a) -> a -> Parser a
chainl p op a = (p `chainl1` op) <|> pure a

chainl1 :: Parser a -> Parser (a -> a -> a) -> Parser a
chainl1 p op = p >>= rest
    where 
        rest a = (do
            f <- op
            b <- p
            rest (f a b)) <|> pure a

chainr :: Parser a -> Parser (a -> a -> a) -> a -> Parser a
chainr p op a = (p `chainr1` op) <|> pure a

chainr1 :: Parser a -> Parser (a -> a -> a) -> Parser a
chainr1 p op = scan
    where
        scan   = p >>= rest
        rest a = (do
            f <- op
            b <- scan
            rest (f a b)) <|> pure a

هتي فقط فرق آهي متبادل (>>)سان (*>). اهي ساڳيا اثر آهن ، سواءِ ان جي ته اها بعد ۾ ڪم ڪري ٿي Applicative۽ هڪ هم منصب سان به اچي (<*). جڏهن پارڪر لکندو آهيان ته مون کي اهو خاص طور تي ڪارائتو ثابت ٿيندو آهي ڇاڪاڻ ته اهي مون کي گهڻن parsers کي گڏ ڪرڻ جي اجازت ڏيندا آهن جڏهن آئون صرف انهن مان هڪ جي پيداوار بابت پرواهه ڪندا  ignored *> ignored *> value <* ignored. ڪيلڪيولر مثال هن ۾ استعمال ڪندو آهي factor.

space :: Parser String
space = many (satisfy isSpace)

token :: Parser a -> Parser a
token p = p <* space

symbol :: String -> Parser String
symbol = token . string

apply :: Parser a -> String -> Maybe (a, String)
apply p = runParser (space *> p)

ڳڻپيوڪر جو مثال تقريباً اڻ ٽر آھي

expr, term, factor, digit :: Parser Int
expr   = term   `chainl1` addop
term   = factor `chainl1` mulop
factor = digit <|> (symbol "(" *> expr <* symbol ")")
digit  = subtract (ord '0') . ord <$> token (satisfy isDigit)

addop, mulop :: Parser (Int -> Int -> Int)
addop = (symbol "+" *> pure (+)) <|> (symbol "-" *> pure (-))
mulop = (symbol "*" *> pure (*)) <|> (symbol "/" *> pure (div))

آخرڪار ، معاوضي!

runParser expr "(1 + 2 * 4) / 3 + 5"
Just (8,"")

اسان 20 سالن ۾ ڇا حاصل ڪيو آهي؟ صرف نن changesن تبديلين سان ، ڪوڊ وڌيڪ ٺوس آهي ۽ بهتر ڪيل تجريدي استعمال ڪندو آهي. مثال طور، جيڪڏهن اسان کي هٽائي جي باري ۾ اسان جي ذهن تبديل []سان Maybe، اسان کي ان کي واپس صفا ڪري سگهن ٿا ۽ رڳو جي قسم جي دستخط تازه ڪاري ڪرڻ آهي ها apply:

apply :: Parser a -> String -> [(a, String)]
apply p = runParser (space *> p) -- the implementation stays the same!

جيڪڏهن اسان بهتر غلط پيغام چاهيون ٿا ، اسان هڪ قسم استعمال ڪري سگهون ٿا جهڙوڪ Either Stringجڳهن ۽ غلط پيغامن جي چڪاس لاءِ. هن yoctoparsecلائبريري اوھان وٽ رهڻ پنهنجو وهڪرو قسم چونڊيو کي به وڌيڪ هن لڳن ٿا،.

ٻيو وڏو فرق Applicativeافعال جو خانداني هوندو آهي ، جنهن کي اسين جڏهن به اڳئين ترتيب واري قيمت تي شاخ هڻڻ جي ضرورت نه هئڻ تي اثر وجھي سگھون ٿا (جيڪو حيران ڪندڙ گهڻو ڪري اچڻو پوي ٿو). مون کي x <$> y <*> z۽ وڏين پرستن جو مان ڏا fanو پرستار آهيان ignored *> value <* ignored۽ منهنجو خيال آهي ته اهو ضروري آهي ته اسان ان طريقي سان پار ڪيو وڃي.

ٻي صورت ۾ ، ڪوڊ گهڻو ڪري ساڳيو آهي ۽ منهنجو خيال آهي ته اهو ڪافي ناقابل يقين آهي ته 20 سالن ۾ تمام گهٽ تبديل ٿي چڪو آهي! اهو ڪوڊ آئي هاسٽل نوٽ بڪ جي طور تي دستياب آهي جيڪڏهن توهان پنهنجي پاڻ سان تجربا ڪرڻ چاهيندا.

ايڊٽ ڪيو: مون صرف ڳولي لڌو آهي ’مانوائيڊس کان وٺي ويجهي سيمرنگس تائين: جوهر MonadPlus۽ Alternative‘ جيڪو اهو ظاهر ڪري ٿو ته منهنجي Maybeبنياد تي ڀاڙيندڙ Alternativeقانونن تي سختي سان عمل نٿو ڪري . ذهن ۾ رکڻ لاءِ ڪا شي جيڪڏهن توهان ان کي استعمال ڪرڻ جو منصوبو ٺاهيو يا ان بابت ڪجهه هو!

آلن او ڊونيل ، اينڊري موخوف ، ايني چرڪيف ، جوليا ايوانز ، ۽ نوح لک ايسسٽرل مهرباني ڪري رايا ۽ راءِ ڏيڻ جي مهرباني .