PyParsec 是 Python 3 的 parsec 移植。这个项目原本是一个非常微型的练手作品,是有一次 archmmit 会场里,我和老友赖勇浩聊天的时候,随手写出来的。因为 Python 是动态类型语言,很多 Haskell 里基于静态类型构建的类型约束也就失去了作用——当然,另一方面说,动态语言项目,很多静态语言的技术要点也不用考虑。
后面有很长一段时间,我都没太去想这个项目,对于我来说,验证了 python 可以作类似的东西,也就达成了目的。但是近期我加入到 csdn 的 ai 团队,有一些开发工作需要编写一些微型的解释器,或者复杂文本的分析程序。于是,我就将它又翻了出来。
除了缺少一些我在工作中用到的算子,pyparsec 还有一些比较突出的不足,比如算子都是很朴素的函数或者可调用对象,这导致复杂的算子写起来可读性会受影响,例如这个不太复杂的例子:
p = many(ch('i'))
显然写成
p = ch('i').many
更友善一些。
没有第一时间这样做,是因为 many 是一个组合子,它是基于 parsec 规范定义的算子,不应该成为一个地位超然的东西。基本法定下来,大家都要遵守,要么将来有办法给Parsec算子任意挂载新的扩展方法,要么大家都不能直接出现在 Parsec 的方法列表里,都不要有钦点的意思。
最终,在 scala 版本中,这些组合子以 typeclass 的风格成为了算子的扩展方法,它们可以通过 import 对应的隐式类型(在 Scala 3中是 extension)而任意扩展,外部扩展和内置算子地位一致,并不会破坏 parsec 的设计。
而在 python 中,我做了一些折衷。首先,为了解环形引用,也为了将来用于自己实现新的扩展方法方便,我定义了一个 combinator 类型:
class Combinator:
def __init__(self, parsec):
self.parsec = parsec
def __call__(self, st):
return self.parsec(st)
def bind(self, continuation):
return self.parsec.bind(continuation)
def then(self, p):
return self.parsec.then(p)
def over(self, p):
return self.parsec.over(p)
需要注意的是这个类型虽然接口与 Parsec 类型一致,但是实现并不相同。
然后再实现一个内置组合子的修饰器类型:
class BuiltIn(Combinator):
def __init__(self, parsec):
super().__init__(parsec)
self.parsec = parsec
@property
def head(self):
from .combinator import ahead
return ahead(self.parsec)
@property
def attempt(self):
from .combinator import attempt
return attempt(self.parsec)
@property
def many(self):
from .combinator import many
return many(self.parsec)
@property
def many1(self):
from .combinator import many1
return many1(self.parsec)
@property
def skip(self):
from .combinator import skip
return skip(self.parsec)
@property
def skip1(self):
from .combinator import skip1
return skip1(self.parsec)
def sepBy(self, by):
from .combinator import sep
return sep(self.parsec, by)
def sepBy1(self, by):
from .combinator import sep1
return sep1(self.parsec, by)
def otherwise(self, other):
from .combinator import choice
return choice(self.parsec.attempt, other)
def manyTill(self, end):
from .combinator import manyTill
return manyTill(self.parsec, end)
这个类型可以直接挂在其它类型或函数的定义上,成为一个修饰器(decorator),而这里还是给了它一点特权,Parsec 类型其实是继承自 Builtin:
#!/usr/bin/env python3
# coding:utf-8
from .builtin import BuiltIn
class Parsec(BuiltIn):
def __init__(self, parsec):
super().__init__(parsec)
self.parsec = BuiltIn(parsec)
def __call__(self, st):
return self.parsec(st)
def bind(self, continuation):
def bind(st):
binder = continuation(self.parsec(st))
return binder(st)
return Parsec(bind)
def then(self, p):
def then(st):
self.parsec(st)
return p(st)
return Parsec(then)
def over(self, p):
def over(st):
re = self.parsec(st)
p(st)
return re
return Parsec(over)
虽然我花了一点儿时间想办法解决了环形引用,但是这样只需要一个修饰器,就自动拥有了 builtin 方法,如同 python 的 builtins ,也不需要额外 import 。
而外部定义的修饰器扩展,需要和 Parsec 修饰器一起使用,组合修饰在算子定义上。
当然,更理想的办法是让任意的扩展方法可以自动挂载,但是 Python 既没有 typclass,也没有很方便的reopen能力,meta class也非常容易被破坏。修饰器是一个不够简洁,但是相对简单的方案。最终 pyparsec 选择了这个路线。