Parsers Aplikatif Haskell


Motivasi


Ketika saya pertama kali mulai mempelajari Haskell, saya sangat terganggu dengan meluasnya penggunaan abstraksi kompleks daripada solusi spesifik apa pun. Tampak bagi saya bahwa jauh lebih baik untuk selalu mengikuti prinsip KISS dan menulis sepeda menggunakan konstruksi bahasa dasar daripada memahami semua kelas jenis ini untuk menulis satu konstruksi yang seharusnya nyaman di suatu tempat.


Saya tidak memiliki contoh yang baik di mana upaya yang dihabiskan untuk pengembangan "material" akan membuahkan hasil. Bagi saya, salah satu contoh yang paling sukses adalah parser. Sekarang saya sering berbicara tentang mereka ketika mereka bertanya kepada saya tentang tugas umum apa yang dapat Anda gunakan dengan indah dari Haskell.


Saya ingin menawarkan pemula juga dengan cara ini dan membuat basis kecil fungsi dari awal untuk implementasi parser yang nyaman, dan kemudian menggunakannya untuk menulis parser mereka sendiri, kode yang secara harfiah akan mengulangi tata bahasa yang digunakan untuk parsing.


Saya harap ini membantu seseorang untuk mengatasi ketakutan akan abstraksi dan mengajari mereka cara menggunakannya secara tepat (ya, saya masih berpikir bahwa kadang-kadang lebih efisien untuk menulis sepeda).


Saya tidak memiliki tujuan dan keinginan untuk membuat kursus Haskell dari awal dari sebuah artikel, jadi saya berasumsi bahwa pembaca sudah terbiasa dengan sintaksis dan secara mandiri mengembangkan program-program sederhana. Untuk berjaga-jaga, saya akan secara singkat berbicara tentang kelas tipe sebelum beralih ke deskripsi implementasi.


Bagi mereka yang belum pernah menulis ke Haskell, tetapi ingin memahami apa yang terjadi di sini, saya sarankan Anda melihat halaman yang sesuai pada Learn X dalam Y menit . Sebagai buku berbahasa Rusia yang sangat baik untuk pemula, saya menyarankan "Tentang Haskell sebagai Manusia" oleh Denis Shevchenko.


Saya akan mencoba menggunakan konstruksi bahasa yang paling sederhana yang bisa dipahami oleh pemula. Di akhir artikel, tautan diberikan ke repositori sumber, di mana di beberapa bagian kode digunakan entri yang lebih nyaman dan lebih pendek, yang mungkin kurang jelas pada pandangan pertama.


Dan ya, tuan-tuan Haskellists, banyak hal dijelaskan dengan sangat sederhana dan canggung, untuk kasus-kasus khusus, tidak terlalu abstrak, tanpa menggunakan istilah dari teori kategori dan kata-kata menakutkan lainnya. Saya senang Anda mengenal mereka dan tentu saja mereka dengan mudah menguasainya. Saya juga mengenal mereka, tetapi saya rasa tidak perlu membuang informasi sebanyak itu dalam konteks ini pada pembaca yang tidak siap.


Ketik kelas


Kelas tipe Haskell tidak ada hubungannya dengan kelas di C ++ dan bahasa berorientasi objek lainnya. Jika kita menggambar analogi dengan OOP, maka kelas tipe lebih seperti kelebihan metode dan fungsi.


Kelas menentukan tindakan apa yang dapat dilakukan dengan objek dari tipe yang membentuk kelas. Misalnya, semua angka dapat dibandingkan untuk kesetaraan, tetapi semuanya dapat dipesan kecuali untuk yang kompleks, dan secara umum, fungsi tidak dapat dibandingkan sama sekali. Kelas jenis yang dapat dibandingkan disebut Eq , dipesan - Ord (jenis tidak harus numerik). Apa yang dapat dicetak dengan menerjemahkan ke dalam string milik kelas Show , ia memiliki kelas Read "berlawanan", yang menentukan bagaimana mengkonversi string ke objek dari tipe yang diinginkan.


Untuk sekumpulan kelas tipe standar (seperti Eq , Show , Read ...), Anda dapat meminta kompiler untuk mengimplementasikan fungsionalitas yang diinginkan dengan cara standar, menggunakan kata kunci deriving setelah menentukan jenisnya:


 data Point = Point { xCoord :: Float , yCoord :: Float } deriving (Eq, Show) 

Anda dapat mendefinisikan kelas tipe Anda sendiri:


 class PrettyPrint a where pPrint :: a -> String 

Di sini PrettyPrint adalah nama kelas, a adalah variabel tipe. Kata kunci where diikuti oleh daftar yang disebut metode kelas, yaitu fungsi yang dapat diterapkan ke objek tipe dari kelas ini.


Untuk menunjukkan kepemilikan tipe data ke kelas, konstruksi berikut digunakan:


 instance PrettyPrint Point where pPrint (Point xy) = "(" ++ show x ++ ", " ++ show y ++ ")" 

Bahasa ini memungkinkan Anda menentukan batasan pada kelas tipe yang harus dikaitkan dengan argumen fungsi:


 showVsPretty :: (Show a, PrettyPrint a) => a -> (String, String) showVsPretty x = (show x, pPrint x) 

Untuk setiap panggilan fungsi, kompilator memeriksa apakah persyaratan jenis ini dipenuhi, dan jika terjadi kegagalan, menampilkan kesalahan (tentu saja, ini terjadi pada tahap kompilasi).


 >>> showVsPretty (Point 2 3) ("Point {xCoord = 2.0, yCoord = 3.0}","(2.0, 3.0)") >>> showVsPretty "str" error: No instance for (PrettyPrint [Char]) arising from a use of 'showVsPretty' 

Implementasi


Parser menerima string input yang harus diurai sesuai dengan aturan yang telah ditentukan dan mendapatkan nilai dari jenis yang kita butuhkan (misalnya, integer). Dalam hal ini, jalur input mungkin tidak berakhir, dan sisanya akan berfungsi sebagai input untuk analisis lebih lanjut. Selain itu, parser kami pada umumnya akan bersifat non-deterministik, mis. akan mengembalikan beberapa kemungkinan hasil parsing sebagai daftar.


Sebuah tupel dari dua elemen (String, a) cocok untuk menggambarkan satu hasil operasi parser, di mana a adalah variabel tipe yang dapat menunjukkan tipe pengguna apa pun.


Karena parser mem-parsing string berdasarkan beberapa aturan, kami menggambarkannya sebagai fungsi yang mengambil string sebagai input dan mengembalikan daftar hasil:


 newtype Parser a = Parser { unParser :: String -> [(String, a)] } 

Kami akan menganggap parsing berhasil jika daftar hasil terdiri dari satu elemen dan string input telah sepenuhnya diproses. Kami menerapkan fungsi pembantu yang mencoba mengurai seluruh string secara unik:


 parseString :: String -> Parser a -> Maybe a parseString s (Parser p) = case (ps) of [("", val)] -> Just val _ -> Nothing 

Parser sederhana


Kami menerapkan beberapa parser sederhana, yang kemudian akan berguna dalam membangun kombinasi yang lebih kompleks.


Kita mulai dengan mem-parsing satu karakter yang harus memenuhi predikat. Jika string input kosong, maka hasil pekerjaan adalah daftar kosong. Jika tidak, kami memeriksa nilai predikat pada karakter pertama dari string. Jika True dikembalikan, hasil parsing adalah karakter ini; kembalikan dengan sisa string. Jika tidak, penguraian juga gagal.


 predP :: (Char -> Bool) -> Parser Char predP p = Parser f where f "" = [] f (c : cs) | pc = [(cs, c)] | otherwise = [] 

Sekarang kita bisa menulis parser yang mengambil karakter tertentu di awal baris. Untuk melakukan ini, gunakan predP baru saja ditulis dan berikan sebagai argumen fungsi yang membandingkan argumennya dengan karakter yang kita butuhkan:


 charP :: Char -> Parser Char charP char = predP (\c -> c == char) 

Kasus paling sederhana berikut: parser yang hanya menerima string tertentu secara keseluruhan. stringP saja stringP . Fungsi di dalam parser membandingkan garis input dengan yang diinginkan dan, jika garis-garisnya sama, mengembalikan daftar satu elemen: sepasang garis kosong (tidak ada yang tersisa di input) dan yang asli. Jika tidak, penguraian gagal, dan daftar hasil kosong dikembalikan.


 stringP :: String -> Parser String stringP s = Parser f where fs' | s == s' = [("", s)] | otherwise = [] 

Cukup sering, Anda perlu melewati karakter yang memiliki properti tertentu ketika mereka pergi ke awal baris (misalnya, karakter spasi putih). Selain itu, hasil analisis tidak penting bagi kami dan tidak akan berguna di masa depan. Kami menulis fungsi lewati yang melewatkan karakter awal string sementara nilai sebenarnya dari predikat dipertahankan. Sebagai hasil analisis, kami menggunakan tuple kosong.


 skip :: (Char -> Bool) -> Parser () skip p = Parser (\s -> [(dropWhile ps, ())]) 

Dua parser berikutnya sangat mirip satu sama lain. Keduanya memeriksa awalan jalur input, hanya yang pertama jika berhasil mengembalikan awalan ini, dan yang kedua mengembalikan tuple kosong, mis. memungkinkan Anda untuk melewati garis arbitrer di awal input. Implementasi menggunakan fungsi isPrefixOf didefinisikan dalam modul Data.List .


 prefixP :: String -> Parser String prefixP s = Parser f where f input = if s `isPrefixOf` input then [(drop (length s) input, s)] else [] skipString :: String -> Parser () skipString s = Parser f where f input = if s `isPrefixOf` input then [(drop (length s) input, ())] else [] 

Beberapa saat kemudian kami akan mempertimbangkan implementasi yang lebih sederhana dari fungsi yang terakhir dan menyingkirkan duplikasi kode.


Parser sebagai functor


Kami dapat membedakan seluruh kelas jenis wadah yang benar berikut ini: jika Anda tahu cara mengubah objek di dalam wadah, maka Anda dapat mengubah wadah itu sendiri. Contoh paling sederhana adalah daftar sebagai wadah dan fungsi map , yang tersedia di hampir semua bahasa tingkat tinggi. Memang, Anda dapat menelusuri semua elemen daftar tipe [a] , menerapkan fungsi a -> b untuk masing-masing, dan mendapatkan daftar tipe [b] .


Tipe kelas ini disebut Functor ; kelas ini memiliki satu metode fmap :


 class Functor f where fmap :: (a -> b) -> fa -> fb 

Misalkan kita sudah tahu bagaimana cara mem-parsing string ke objek tipe tertentu a , dan, di samping itu, kita tahu cara mengubah objek tipe a ke objek tipe b . Apakah mungkin untuk mengatakan bahwa kemudian ada parser untuk objek bertipe b ?


Jika dinyatakan dalam bentuk fungsi, maka akan memiliki jenis berikut:


 (a -> b) -> Parser a -> Parser b 

Tipe ini bertepatan dengan tipe fungsi fmap , jadi mari kita coba menjadikan parser sebagai functor. Mari kita membuat parser nilai tipe b dari awal, yang akan memanggil parser pertama (kita sudah memilikinya), dan kemudian menerapkan fungsi ke hasil parsingnya.


 instance Functor Parser where fmap :: (a -> b) -> Parser a -> Parser b fmap f (Parser p1) = Parser p2 where p2 :: String -> [(String, b)] p2 s = convert (p1 s) convert :: [(String, a)] -> [(String, b)] convert results = map (\(s, val) -> (s, f val)) results 

Fungsi fmap memiliki sinonim infiks yang nyaman: fmap fx == f <$> x .


Jika kita menggunakan fungsi sebagai argumen ke fmap yang hanya menggantikan argumen pertamanya dengan nilai baru, kita mendapatkan operasi lain yang bermanfaat yang sudah diterapkan untuk semua fungsi bahkan dalam dua salinan (mereka berbeda hanya dalam urutan argumen):


 (<$) :: Functor f => a -> fb -> fa ($>) :: Functor f => fa -> b -> fb 

Ingat parser yang melewatkan garis tertentu ( skipString )? Sekarang Anda dapat menerapkannya sebagai berikut:


 skipString :: String -> Parser () skipString s = () <$ prefixP s 

Kombinasi Parser


Di Haskell, semua fungsi dijalankan secara default dan sebagian dapat digunakan. Ini berarti bahwa fungsi argumen n sebenarnya adalah fungsi dari satu argumen, mengembalikan fungsi argumen n-1 :


 cons :: Int -> [Int] -> [Int] cons = (:) cons1 :: [Int] -> [Int] cons1 = cons 1 --  cons   

Kami menerapkan fungsi dari tiga argumen ke beberapa nilai di dalam parser menggunakan fmap . Jenisnya adalah sebagai berikut:


 f :: c -> a -> b p :: Parser c (fmap fp) :: Parser (a -> b) 

Fungsi parser ternyata ?! Tentu saja, sebuah situasi dimungkinkan ketika representasi fungsi benar-benar ada di baris input, tetapi saya ingin dapat menggunakan fungsi ini, atau lebih tepatnya menggabungkan Parser (a -> b) dan Parser a parser untuk mendapatkan Parser b :


 applyP :: Parser (a -> b) -> Parser a -> Parser b 

Jenis fungsi ini sangat mirip dengan tipe fmap , hanya fungsi itu sendiri yang perlu diterapkan juga dalam wadah. Ini memberikan pemahaman intuitif tentang bagaimana penerapan fungsi applyP akan terlihat seperti: dapatkan fungsi dari wadah (sebagai hasil dari penerapan parser pertama), dapatkan nilai-nilai yang harus diterapkan fungsi (hasil penerapan parser kedua), dan "bungkus" nilai-nilai yang dikonversi menggunakan fungsi ini kembali ke dalam wadah (buat parser baru). Dalam implementasinya, kami akan menggunakan daftar pemahaman:


 applyP :: Parser (a -> b) -> Parser a -> Parser b applyP (Parser p1) (Parser p2) = Parser f where fs = [ (sx, fx) | (sf, f) <- p1 s, -- p1     (sx, x) <- p2 sf] -- p2   ,     

Ada kelas Applicative yang memiliki metode dengan prototipe yang sama. Metode kedua dari kelas ini disebut pure dan digunakan untuk "membungkus" atau "mengangkat" nilai, termasuk nilai fungsional. Dalam kasus implementasi untuk parser, fungsi pure menambahkan argumennya ke hasil parser, tanpa mengubah string input.


 class Functor f => Applicative f where pure :: a -> fa (<*>) :: f (a -> b) -> fa -> fb instance Applicative Parser where pure x = Parser (\s -> [(s, x)]) pf <*> px = Parser (\s -> [ (sx, fx) | (sf, f) <- unParser pf $ s, (sx, x) <- unParser px $ sf]) 

Fungsi applyP adalah <*> dari kelas Applicative . Jenis yang termasuk dalam kelas ini disebut sebagai aplikator fungsi.


Untuk fungsi aplikatif, dua fungsi tambahan diterapkan yang akan berguna bagi kami:


 (*>) :: fa -> fb -> fb (<*) :: fa -> fb -> fa 

Fungsi-fungsi ini melakukan dua tindakan berurutan dan mengembalikan hasil hanya dari salah satunya. Untuk parser, mereka dapat digunakan, misalnya, untuk melewati spasi sebelum mengurai bagian dari string yang membawa beban semantik.


Dengan menggabungkan <$> dan <*> , Anda dapat membuat desain yang sangat nyaman. Pertimbangkan tipe data berikut:


 data MyStructType = MyStruct { field1 :: Type1 , field2 :: Type2 , field3 :: Type3 } 

Konstruktor nilai MyStruct juga merupakan fungsi, dalam hal ini adalah tipe Type1 -> Type2 -> Type3 -> MyStructType . Anda dapat bekerja dengan konstruktor seperti halnya dengan fungsi lainnya. Misalkan parser sudah ditulis untuk jenis bidang struktur:


 parser1 :: Parser Type1 parser2 :: Parser Type2 parser3 :: Parser Type3 

Menggunakan fungsi fmap , Anda dapat menerapkan sebagian MyStruct ke yang pertama dari parser ini:


 parserStruct' :: Parser (Type2 -> Type3 -> MyStructType) parserStruct' = MyStruct <$> parser1 

Mari kita coba terapkan fungsi yang sekarang "di dalam" parser. Untuk melakukan ini, Anda perlu menggunakan <*> :


 parserStruct'' :: Parser (Type3 -> MyStructType) parserStruct'' = parserStruct' <*> parser2 parserStruct :: Parser MyStructType parserStruct = parserStruct'' <*> parser3 

Sebagai hasilnya, kami mendapatkan parser untuk seluruh struktur (tentu saja, di sini kami menggunakan asumsi bahwa dalam baris asli representasi bidangnya berturut-turut). Hal yang sama dapat dilakukan dalam satu baris:


 parserStruct :: Parser MyStructType parserStruct = MyStruct <$> parser1 <*> parser2 <*> parser3 

Konstruksi seperti itu akan sering ditemui dalam kasus penggunaan.


Sekarang anggaplah kita sedang mencoba untuk menulis parser yang mem-parsing ekspresi aritmatika sederhana di mana bilangan bulat dan pengidentifikasi dapat hadir sebagai operan. Mari kita buat jenis Operand terpisah untuk mereka:


 data Operand = IntOp Int | IdentOp String 

Jika kita sudah tahu cara mem-parsing bilangan bulat dan pengidentifikasi (misalnya, seperti dalam C), maka kita memerlukan satu pengurai untuk operan yang dapat menguraikan satu atau yang lain. Parser ini merupakan alternatif dari dua parser lainnya, sehingga diperlukan suatu fungsi yang dapat menggabungkan parser sehingga hasil kerja mereka digabungkan. Hasil pengurai adalah daftar, dan menggabungkan daftar adalah gabungannya. Kami mengimplementasikan fungsi altP menggabungkan dua parser:


 altP :: Parser a -> Parser a -> Parser a altP (Parser p1) (Parser p2) = Parser (\s -> p1 s ++ p2 s) 

Kemudian operan parser dapat diimplementasikan menggunakan fungsi ini (di sini diasumsikan bahwa parserInt dan parserIdent sudah dijelaskan di suatu tempat:


 parserOperand :: Parser Operand parserOperand = altP parserIntOp parserIdentOp where parserIntOp = IntOp <$> parserInt parserIdentOp = IdentOp <$> parserIdent 

Tentu saja, untuk alternatif kami telah datang dengan kelas yang terpisah, yang disebut Alternative . Ini memiliki metode lain, empty , yang menggambarkan elemen netral untuk operasi alternatif. Dalam kasus kami, itu adalah parser yang tidak pernah mem-parsing apa pun, yaitu. selalu mengembalikan daftar hasil yang kosong. Untuk parser, implementasi metode kelas Alternative terlihat seperti ini:


 class Applicative f => Alternative f where empty :: fa (<|>) :: fa -> fa -> fa instance Alternative Parser where empty = Parser (const []) px <|> py = Parser (\s -> unParser px s ++ unParser py s) 

Operasi <|> adalah fungsi altP hanya dalam notasi infiks, yang lebih nyaman digunakan dengan menggabungkan beberapa parser dalam satu baris.


Untuk semua tipe dalam kelas ini, dua fungsi diimplementasikan, some dan many tipe fa -> f [a] . Masing-masing dapat diekspresikan melalui yang lain:


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

Dalam hal parser, fungsi-fungsi ini memungkinkan Anda untuk mengurai urutan data jika Anda tahu cara mengurai elemen data tunggal. Dalam hal menggunakan some urutannya harus kosong.


Contoh penggunaan


Sekarang kita siap untuk menulis parser Anda sendiri, misalnya, untuk ekspresi aritmatika sederhana dengan tata bahasa berikut:


  expr ::= constExpr | binOpExpr | negExpr const ::= int int ::= digit{digit} digit ::= '0' | ... | '9' binOpExpr ::= '(' expr ' ' binOp ' ' expr ')' binOp ::= '+' | '*' negExpr ::= '-' expr 

Ekspresi terdiri dari konstanta integer, minus unary, dan dua operasi biner infiks: penambahan dan perkalian. Kurung diperlukan di sekitar ekspresi dengan operasi biner, simbol operasi dipisahkan dari operan dengan tepat satu ruang, ruang terdepan dan gantung tidak diperbolehkan.


Contoh penulisan ekspresi yang benar:


 "123" "-(10 + 42)" "(1 + ((2 + 3) * (4 + 5)))" 

Contoh entri yang salah:


 " 666 " "2 + 3" "(10 * 10)" 

Kami mendeklarasikan tipe data yang diperlukan (ekspresi itu sendiri dan operasi biner):


 data Expr = ConstExpr Int | BinaryExpr Expr Operator Expr | NegateExpr Expr data Operator = Add | Mul 

Anda bisa mulai parsing! Ungkapan itu sendiri terdiri dari tiga alternatif. Jadi kami menulis:


 -- expr ::= constExpr | binOpExpr | negExpr exprParser :: Parser Expr exprParser = constParser <|> binParser <|> negParser 

Konstanta adalah bilangan bulat positif. Dalam tipe data kami, "dibungkus" dalam konstruktor, jadi kami tidak dapat menggunakan parser untuk integer secara langsung, tetapi kami dapat menggunakan fmap untuk mendapatkan nilai dari tipe yang diinginkan.


 -- const ::= int constParser :: Parser Expr constParser = ConstExpr <$> intParser 

Bilangan bulat, menurut tata bahasa, direpresentasikan sebagai urutan angka yang tidak kosong. Untuk mem-parse satu digit, kita menggunakan predP fungsi bantu dan predikat isDigit dari modul Data.Char . Sekarang, untuk membangun parser untuk parsing urutan angka, kita menggunakan fungsi some (tidak many , karena harus ada setidaknya satu digit). Hasil parser seperti itu mengembalikan daftar semua opsi parsing yang mungkin, dimulai dengan catatan terpanjang. Misalnya, jika string input adalah "123ab", daftar hasilnya adalah sebagai berikut: [("ab", "123"), ("3ab", "12"), ("23ab", "1")] . Kita perlu menguraikan urutan angka terpanjang dan mengubahnya menjadi tipe Int . Seluruh implementasi adalah sebagai berikut:


 -- int ::= digit{digit} -- digit ::= '0' | ... | '9' intParser :: Parser Int intParser = Parser $ \s -> let res = unParser (some digitParser) s in case res of [] -> [] ((rest, i) : xs) -> [(rest, read i)] where digitParser = predP isDigit 

Cara selanjutnya untuk menulis ekspresi adalah dengan menggunakan operasi biner. Menurut tata bahasa, braket pembuka harus terlebih dahulu menyertakan braket pembuka, operan pertama, spasi, simbol operasi, spasi lain, operan kedua, dan braket penutup. Untuk mem-parsing karakter individu (tanda kurung dan spasi) kami menggunakan fungsi charP . Operand adalah ekspresi, dan sudah ada parser ( exprParser ) untuk menguraikannya. Untuk mengurai simbol operasi biner, kami menjelaskan parser tambahan tepat di bawah. Masih untuk menggabungkan parser ini dengan rapi. Harus ada tanda kurung di awal dan di akhir ekspresi: Anda perlu memeriksa ini, tetapi buang hasilnya sendiri. Untuk melakukan ini, gunakan *> dan <* :


 binParser :: Parser Expr binParser = charP '(' *> ??? <* charP ')' 

Di antara parser ini untuk tanda kurung, ekspresi harus dibangun menggunakan konstruktor BinaryExpr dan parser untuk ekspresi dan operasi. Jangan lupa tentang spasi di sekitar simbol operasi, menggunakan metode yang sama seperti untuk tanda kurung. Bagian ini adalah sebagai berikut:


 BinaryExpr <$> exprParser --   <*> (charP ' ' *> binOpParser <* charP ' ') -- ,   <*> exprParser --   

Kami mengganti ungkapan ini alih-alih tanda tanya:


 -- binOpExpr ::= '(' expr ' ' binOp ' ' expr ')' binParser :: Parser Expr binParser = charP '(' *> (BinaryExpr <$> exprParser <*> (charP ' ' *> binOpParser <* charP ' ') <*> exprParser ) <* charP ')' 

Operasi biner adalah karakter + yang mem-parsing nilai Add , atau * yang mem-parsing Mul :


 -- binOp ::= '+' | '*' binOpParser :: Parser Operator binOpParser = plusParser <|> multParser where plusParser = charP '+' $> Add multParser = charP '*' $> Mul 

Bagian paling sederhana dari tata bahasa tetap, negasi dari ekspresi. Dengan simbol - melakukan hal yang sama seperti dengan tanda kurung dan spasi. Selanjutnya, terapkan konstruktor NegateExpr ke hasil parsing rekursif:


 -- negExpr ::= '-' expr negParser = charP '-' *> (NegateExpr <$> exprParser) 

Jadi, semua bagian pengurai diimplementasikan. Kode mirip dengan tata bahasa dan benar-benar bertepatan dengan strukturnya.


Kode sumber tersedia di GitLab: https://gitlab.com/fierce-katie/applicative-parsers-demo .


Ada lebih mudah untuk mengevaluasi volume dan tingkat ekspresifnya, karena ada jauh lebih sedikit komentar. Anda dapat mengkompilasi proyek dengan utilitas Stack dan menjalankan interpreter primitif menggunakan parser yang kami tulis:


 $ stack build $ stack exec demo-parser 

Bagi mereka yang ingin berlatih lebih lanjut sendiri, saya dapat menyarankan yang berikut:


  • Tata bahasa dapat ditingkatkan dengan segala cara, misalnya, untuk memungkinkan ruang terkemuka dan menggantung, menambah operasi baru, dll.
  • Parser menerjemahkan string ke representasi internal ekspresi. Ungkapan ini dapat dihitung dan interpreter dikonversi sehingga ia mencetak bukan hasil dari penguraian, tetapi hasil dari perhitungan.
  • Jelajahi kemungkinan parsec , attoparsec , applicative-parsec dan optparse-applicative applicative-parsec dan cobalah untuk menggunakannya.

Terima kasih atas perhatian anda!


Bahan yang berguna


  1. Pelajari Haskell dalam Y menit
  2. Denis Shevchenko. "Tentang Haskell sebagai manusia"
  3. Perpustakaan Parsec
  4. Perpustakaan Attoparsec
  5. Perpustakaan aplikatif-parsec
  6. Perpustakaan optparse-aplikatif

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


All Articles