Parsec 的遷移(二)Pyparsec 的 Monad化

前一篇我們討論了將 Parsec 移植到 Go 語言中遇到的一些問題: Parsec 的遷移(一) - 前路迢迢 - 知乎專欄 。

這裡有幾個問題,是幾乎向任何語言遷移 Parsec 庫都會遇到的。例如,如何將運算元的定義和Monad化的邏輯正交分解。因為每個運算元的邏輯各不相同,但是其Bind/Then行為應該是一致的。另一個是如何平衡錯誤處理的問題:如果要頻繁的處理錯誤狀態,將err作為結果返回比較好。如果要實現do,就要提供一個可以拋出異常的環境。更麻煩的是,有些語言並沒有異常處理能力。

Python 版的 Parsec 實現 Dwarfartisan/pyparsec · GitHub 中,我直接放棄了對於錯誤返回和異常拋出的妥協問題,統一用異常處理。得益於 Python 的 decorate 語法,Monad 封裝做的非常的簡單:

class Parsec(object):n def __init__(self, parsec):n self.parsec = parsecn def __call__(self, st):n return self.parsec(st)n def bind(self, continuation):n def bind(st):n return continuation(self.parsec(st))n return Parsec(bind)n def then(self, p):n def then(st):n self.parsec(st)n return p(st)n return Parsec(then)n def over(self, p):n def over(st):n re = self.parsec(st)n p(st)n return ren return Parsec(over)n

這樣,所有的運算元只要定義為函數,然後 decorate 一下就好。將原來的函數綁定到修飾後生成的新對象上,作為其 __call__ 方法的行為,bind等monad行為也實現為對應的方法調用。例如 eof :

@Parsecndef eof(state):n re = Nonen try:n re = state.next()n except ParsecEof:n return Nonen raise ParsecError(state, "Expect eof but got {0}".format(re))n

甚至組合子或生成運算元的邏輯也可以,比如eq:

def eq(data):n @Parsecn def call(st):n re = st.next()n if re == data:n return ren else:n raise ParsecError(st, "Expect {0} but got {1}".format(data, re))n return calln

和 choices :

def choices(*psc):n if len(psc)<2:n raise "choices need more args than one."n @Parsecn def call(st):n for p in psc[:-1]:n prev = st.indexn try:n return p(st)n except:n if st.index != prev:n raisen else:n return psc[-1](st)n return calln

得益於 Python 簡潔的語法和動態類型,即使不像 goparsec2 一樣區分 exception 和 error ,Python 的版本仍然遠比其它靜態語言的簡短易用,自從實現了這個版本,我經常是先用 python 寫出解析邏輯,確定測試 case ,再去編寫正式項目中的 go 版本邏輯。

推薦閱讀:

TAG:编程 | 解释器 | Python |