۲۳ - ۶اَلتِرنِتیو یا Alternative

فرض کنیم یه پارسر برای اعداد و یه پارسر برای نوشته‌های الفباعددی داشته باشیم:

Prelude> import Text.Trifecta
Prelude> parseString (some letter) mempty "blah"
Success "blah"
Prelude> parseString integer mempty "123"
Success 123

اگه یه تایپی داشتیم که می‌تونست هم ‏‎Integer‎‏ باشه هم ‏‎String‎‏، چطور؟

module AltParsing where

import Control.Applicative
import Text.Trifecta

type NumberOrString =
  Either Integer String

a = "blah"
b = "123"
c = "123blah789"

parseNos :: Parser NumberOrString
parseNos =
      (Left <$> integer)
  <|> (Right <$> some letter)

main = do
  let p f i =
        parseString f mempty i
  print $ p (some letter) a
  print $ p integer b
  print $ p parseNos a
  print $ p parseNos b
  print $ p (many parseNos) c
  print $ p (some parseNos) c

عملگر ِ ‏‎<|>‎‏ رو میشه "یا یا فصلِ منطقی بین دو پارسر خوند؛ ‏‎many‎‏ معادلِ صفر یا بیشتر، و ‏‎some‎‏ معادل یکی یا بیشتره.

Prelude> parseString (some integer) mempty "123"
Success [123]
Prelude> parseString (many integer) mempty "123"
Success [123]
Prelude> parseString (many integer) mempty ""
Success []
Prelude> parseString (some integer) mempty ""
Failure (interactive):1:1: error: unexpected
    EOF, expected: integer
<EOF>
^

توابع ‏‎some‎‏، ‏‎many‎‏ و ‏‎(<|>)‎‏ که اینجا استفاده کردیم از تایپکلاسی به اسمِ ‏‎Alternative‎‏ (م. به معنای "جایگزین") هستن:

class Applicative f => Alternative f where
  -- | <|> همانی برای
  empty :: f a

  -- | یه اوپراتور باینری و شرکت‌پذیر
  (<|>) :: f a -> f a -> f b

  -- | .یکی یا بیشتر
  some :: f a -> f [a]
  some v = some_v
    where
      many_v = some_v <|> pure []
      some_v = (fmap (:) v) <*> many_v

  -- | .صفر یا بیشتر
  many :: f a -> f [a]
  many v = many_v
    where
      many_v = some_v <|> pure []
      some_v = (fmap (:) v) <*> many_v

اگه بعد از وارد کردنِ ‏‎Text.Trifecta‎‏ یا بارگذاری ِ ماژول ِ بالا در REPL از دستورِ ‏‎:info‎‏ استفاده کنین، می‌بینین که ‏‎some‎‏ و ‏‎many‎‏ در ‏‎GHC.Base‎‏ تعریف شدن، چون این تایپکلاس مختصِ هیچ پارسر ِ بخصوص یا کتابخونه ِ ‏‎parsers‎‏، یا اصلاً این مسئله‌ی خاص نیست.

اگه بخوایم ملزم کنیم هر مقدار با یه خطِ‌جدید از هم جدا شده باشه چطور؟ با ‏‎QuasiQuotes‎‏ میشه نوشته‌های چندخطی رو بدونِ نوشتنِ کاراکترِ خطِ‌جدید درست کرد و به عنوانِ یه آرگومان ازشون استفاده کرد:

{-# LANGUAGE QuasiQuotes #-}

module AltParsing where

import Control.Applicative
import Text.RawString.QQ
import Text.Trifecta

type NumberOrString =
  Either Integer String

eitherOr :: String
eitherOr = [r|
123
abc
456
def
|]

‏‎QuasiQuotes‎‏

در کُدِ بالا، اون ‏‎[r|‎‏ یه بخشِ نیم‌نَقلی* رو با نیم‌نقل‌کننده‌ای به اسمِ ‏‎r‎‏ شروع می‌کنه. دقت کنین که برای استفاده از این گرامر باید توسعه‌ی زبانی ِ ‏‎QuasiQuotes‎‏ رو فعال می‌کردیم. در زمان نوشتار این کتاب، ‏‎r‎‏ در ‏‎raw-strings-qq‎‏ نسخه‌ی ۱٫۱ به صورتِ زیر تعریف شده:

r :: QuasiQuoter
r = QuasiQuoter {
    -- dead-simple-json استخراج شده از
    quoteExp  =
      return . LitE . StringL
             . normaliseNewlines,
    -- پیغام‌های خطا رو ننوشتیم
    quotePat  =
      \_ -> fail "some error message"
    quoteType =
      \_ -> fail "some error message"
    quoteDec  =
      \_ -> fail "some error message"
*

صفحه‌ی wiki ِ خوبی با مثال در این آدرس هست.

این در واقع یه روش برای نوشتنِ متونِ دلخواه داخل یه بلوکی که با |r] شروع و با [| تموم میشه‌ست. این نیم‌نقل‌کننده ِ بخصوص برای نوشتنِ متونِ چندخطی، بدونِ نیاز به دستی نوشتنِ کاراکترِ ‏‎\n‎‏ کاربرد داره. نیم‌نقل‌کننده‌ای که بالاتر نوشتیم چنین نوشته‌ای ایجاد می‌کنه:

"\n\
\123\n
\abc\n
\456\n
\def\n"

به خوشگلیِ قبل‌ش نیست، نه؟ اگه بخواین خروجیِ نیم‌نقل‌کننده یا Template Haskell رو ببینین می‌تونین پرچم ِ ‏‎-ddump-splices‎‏ رو فعال کنین. یه مثالِ ساده:

{-# LANGUAGE QuasiQuotes #-}

module Quasimodo where

import Text.RawString.QQ

eitherOr :: String
eitherOr = [r|
123
abc
456 
|]

بعد اگه تو GHCi اون پرچم رو با ‏‎:set‎‏ روشن کنیم، میشه چیزی که نیم‌نقل‌کننده ایجاد می‌کنه رو ببینیم:

Prelude> :set -ddump-splices
Prelude> :l code/quasi.hs
[1 of 1] Compiling Quasimodo
code/quasi.hs:(8,12)-(12,2): Splicing expression
    "\n\
    \123\n\
    \abc\n\
    \456\n"
  ======>
    "\n\
    \123\n\
    \abc\n\
    \456\n"

خیلی خوب، برگردیم به پارسری که می‌خواستیم بنویسیم!

بازگشت به ‏‎Alternative‎‏

برمی‌گردیم به ماژول ِ ‏‎AltParsing‎‏. می‌خوایم از این تابعِ فوق‌العاده استفاده کنیم:

parseNos :: Parser NumberOrString
parseNos =
      (Left <$> integer)
  <|> (Right <$> some letter)

و ‏‎main‎‏ رو طوری بازنویسی کنیم که اون رو به مقدارِ ‏‎eitherOr‎‏ اعمال کنه:

main = do
  let p f i = parseString f mempty i
  print $ p parseNos eitherOr

دقت کنین که ‏‎Left‎‏ و ‏‎Right‎‏ رو از روی آرگومان‌هاشون لیفت کردیم. دلیل‌ش اینه که بین چیزی که اون داده‌ساز‌ها انتظار دارن و مقداری که (احتمالاً) از اجرای پارسر بدست میاد، ساختار ِ ‏‎Parser‎‏ وجود داره. یه مقدار با تایپِ ‏‎Parser Char‎‏ پارسِر ایه که اگه بهش ورودی‌ای داده بشه که منجر به شکست‌ِش نشه، یه مقدارِ ‏‎Char‎‏ تولید می‌کنه. تایپِ ‏‎some letter‎‏ از این قراره:

Prelude> import Text.Trifecta
Prelude> :t some letter
some letter :: CharParsing f => f [Char]

ولی برای ما کفایت می‌کنه که بگیم تایپ‌ش همون تایپِ ‏‎Parser‎‏ ِ مختص به ‏‎trifecta‎‏ ست.

λ> let someLetter = some letter :: Parser [Char]
λ> let someLetter = some letter :: Parser String

اگه بخوایم مثل یه بچه‌ای که با اسباب‌بازی‌هاش بازی می‌کنه، پارسر ِ نوشته رو به زور به یه داده‌سازی که انتظارِ ‏‎String‎‏ داره بدیم، خطای تایپ می‌گیریم:

λ> data MyName = MyName String deriving Show
λ> MyName someLetter

Couldn't match type ‘Parser String’ with ‘[Char]’
Expected type: String
  Actual type: Parser String
In the first argument of ‘MyName’,
  namely ‘someLetter’
In the expression: MyName someLetter

مگر اینکه از روی ساختار ِ ‏‎Parser‎‏ لیفت‌ِش کنیم، چون ‏‎Parser‎‏ یه ‏‎Functor‎‏ ِه!

λ> :info Parser
{... خلاصه کردیم ...}
instance Monad Parser
instance Functor Parser
instance Applicative Parser
instance Monoid a => Monoid (Parser a)

instance Errable Parser
instance DeltaParsing Parser
instance TokenParsing Parser
instance CharParsing Parser

با یه ‏‎fmap‎‏ درست میشه، قبوله؟

λ> :t MyName <$> someLetter
MyName <$> someLetter :: Parser MyName
λ> :t MyName `fmap` someLetter
MyName `fmap` someLetter :: Parser MyName

بعد با اجرای هرکدوم از اونها:

λ> parseString someLetter mempty "Chris"
Success "Chris"
λ> let mynameParser = MyName <$> someLetter
λ> parseString mynameParser mempty "Chris"
Success (MyName "Chris")

خیلی هم خوب.

برگردیم به کُدِ اصلی‌مون که خطا میده:

λ> main
Failure (interactive):1:1: error: expected: integer,
    letter

اگه به نوشته ِ تست‌مون نگاه کنیم، راحت‌تر میشه:

λ> eitherOr
"\n123\nabc\n456\ndef\n"

یه راهش اینه که نوشته ِ نیم‌نقل‌شده رو درست کنیم:

eitherOr :: String
eitherOr = [r|123
abc
456
def
|]

ولی اگه می‌خواستیم اجازه‌ی وجودِ یه خطِ‌جدید قبل از شروع به پارس ِ نوشته‌ها یا اعداد رو بدیم چطور؟

eitherOr :: String
eitherOr = [r|
123
abc
456
def
|]

parseNos :: Parser NumberOrString
parseNos =
  skipMany (oneOf "\n")
  >>
      (Left <$> integer)
  <|> (Right <$> some letter)

main = do
  let p f i = parseString f mempty i
  print $ p parseNos eitherOr

Prelude> main
Success (Left 123)

اوکِی، ولی می‌خوایم بعد از هر خط به پارس‌کردن ادامه بدیم. اگه چاره‌ی واضح رو انتخاب کنیم و از ‏‎some‎‏ استفاده کنیم تا یک-یا-بیشتر جواب بخوایم، یه خطای نسبتاً عجیب می‌گیریم:

λ> parseString (some parseNos) mempty eitherOr
Failure (interactive):6:1: error: unexpected
    EOF, expected integer, letter
<EOF>
^

اینجا با ‏‎skipMany‎‏ کاراکترِ ‏‎'\n'‎‏ رو، صفر بار یا بیشتر رَد می‌کنیم، پس معنی‌ش اینه که اجرای بعدیِ پارسر رو، قبل از رسیدن به EOF شروع می‌کنیم. یعنی بعد از "def" انتظارِ یه عدد صحیح یا تعدادی حروف رو داره. میشه ورودی رو درست کنیم:

eitherOr :: String
eitherOr = [r|
123
abc
456
def|]

حالا کُدِ قبلی کار می‌کنه:

λ> parseString (some parseNos) mempty eitherOr
Success [Left 123,Right "abc",Left 456,Right "def"]

اگه با این کار راضی نمی‌شیم، دو راه داریم تا پارسر رو با خطِ‌جدید‌های پایانی سازگار کنیم. یکی‌ش اینه که یه ‏‎skipMany‎‏ ِ دیگه بعد از پارس اضافه کنیم:

parseNos :: Parser NumberOrString
parseNos = do
  skipMany (oneOf "\n")
  v <-     (Left <$> integer)
       <|> (Right <$> some letter)
  skipMany (oneOf "\n")
  return v

یه راهِ دیگه اینه که نسخه‌ی قبلیِ پارسر ِمون (که خط‌جدید‌های احتمالیِ اولِ نوشته رو رَد می‌کنه) رو نگه داریم:

parseNos :: Parser NumberOrString
parseNos =
  skipMany (oneOf "\n")
  >>
        (Left <$> integer)
    <|> (Right <$> some letter)

ولی بعد با رفتارِ پیش‌فرضِ ‏‎token‎‏، نشانه‌گذاری‌ش کنیم:

> parseString some (token parseNos)) mempty eitherOr
uccess [Left 123,Right "abc",Left 456,Right "def"]

به زودی توکِن رو توضیح میدیم، ولی باید حواسمون رو جمع کنیم چون پارسرهای توکن و پارسرهای کاراکتر چیزهای متفاوتی‌اند. کاری که اینجا اعمالِ ‏‎token‎‏ به ‏‎parseNos‎‏ برامون انجام داد این بود که در صورت وجودِ فاصله‌ی سفیدِ انتهایی اونها رو هم مصرف کنه؛ خطِ‌جدید هم یکی از فاصله‌های سفید هست.

تمرین: ‏‎try‎‏ امتحان کنین

با پارسرِ کسری که داشتیم، به علاوه‌ی یه پارسر ِ جدید برای اعدادِ ده‌دهی، یه پارسری درست کنین که بتونه هم اعداد ده‌دهی و هم اعداد کسری رو پارس کنه. عملگر ِ ‏‎<|>‎‏ از ‏‎Alternative‎‏ رو لازم دارین تا اون دوتا پارسر ِ جایگزین همدیگه رو با هم ترکیب کنین. اگه به نظرتون خیلی سخت اومد، یه پارسر درست کنین که بتونه هم اعدادِ صحیح و هم اعدادِ کسری‌ای که سَرراست هستن رو پارس کنه. یه نوع‌داده درست کنین که شامل اعداد صحیح یا گویا باشه و ازش به عنوان جوابِ پارسر استفاده کنین. یا از ‏‎Either‎‏ استفاده کنین. خودتون مختارین.

راهنمایی: هنوز توضیح ندادیم، ولی شاید بخواین ‏‎try‎‏ رو امتحان کنین.