「Text.Read」Day 3. - 30天Hackage之旅

Text.Read 做的事情和 Text.Show 刚好相反,是把字符串反序列化为对应的数据类型。反序列化,自然就和语法解析有关,也因此 Text.Read 要比 Text.Show 相对复杂许多。

初步使用

继续使用 Tree 数据结构为例:

import Text.Read

infixr 5 :^:
data Tree a =  Leaf a  |  Tree a :^: Tree a
   deriving (Show, Read)

main = print (read "Leaf 1 :^: (Leaf 2 :^: Leaf 3)" :: Tree Int)

可以看到,Read 的用法十分简单,就是直接用小写的 read 读取一个字符串,并返回希望得到的类型,需要注意的是,必须注明想要得到的类型,如此处的 Tree Int

如果不适用 deriving Read 而选择自己去定义 Read 实例的话,官方的示例是这么写的:

instance (Read a) => Read (Tree a) where
        readsPrec d r =  readParen (d > app_prec)
                         (\r -> [(Leaf m,t) |
                                 ("Leaf",s) <- lex r,
                                 (m,t) <- readsPrec (app_prec+1) s]) r

                      ++ readParen (d > up_prec)
                         (\r -> [(u:^:v,w) |
                                 (u,s) <- readsPrec (up_prec+1) r,
                                 (":^:",t) <- lex s,
                                 (v,w) <- readsPrec (up_prec+1) t]) r
          where app_prec = 10
                up_prec = 5

上半段代码尝试读取 Leaf 的形式,下半段尝试读取 :^: 的形式,app_prec 表示函数调用的优先级是 10, up_prec 表示 :^: 的优先级是 5。看到这里,可能就要问了,readsPrec 到底是在干什么?以及它和我们真正使用的 read 有什么关系?

初探源码

先来看看 read 是怎么定义的?

read :: Read a => String -> a
read s = either errorWithoutStackTrace id (readEither s)

readEither :: Read a => String -> Either String a
readEither s =
  case [ x | (x,"") <- readPrec_to_S read' minPrec s ] of
    [x] -> Right x
    []  -> Left "Prelude.read: no parse"
    _   -> Left "Prelude.read: ambiguous parse"
 where
  read' =
    do x <- readPrec
       lift P.skipSpaces
       return x

readPrec     = readS_to_Prec readsPrec

readS_to_Prec f = P (\n -> readS_to_P (f n))

不要被上面的代码吓到,以上是截取了多个模块中的代码,来展示 readreadsPrec 之间的联系。可以看到,这里绕的弯还不少,大概有这么几个函数需要我们摸清楚门道:

read ==> readEither ==> readPrec ==> readsPrec

注意,这里 readPrecreadsPrec 是两个不同的函数,有的时候真得是要吐槽下 Haskell 函数库中命名的不拘小节,命名的时候如此小的差别,确实不利于阅读源码。
为了讲清楚这些函数,就必须先分别来看一下它们的类型签名。

read :: Read a => String -> a
readEither :: Read a => String -> Either String a
readPrec     :: ReadPrec a
readsPrec :: Int -> ReadS a

哇偶!好多不认识的面孔!这里有一个类型类:Read,有两个陌生的类型:ReadPrecReadS ,都是什么鬼?别着急,慢慢来看:

-- 类型
newtype ReadPrec a = P (Prec -> ReadP a)

newtype ReadP a = R (forall b . (a -> P b) -> P b)
data P a
  = Get (Char -> P a)
  | Look (String -> P a)
  | Fail
  | Result a (P a)
  | Final [(a,String)]
  deriving Functor

type ReadS a = String -> [(a,String)]

-- 类型类
class Read a where
  {-# MINIMAL readsPrec | readPrec #-}
  readsPrec    :: Int -> ReadS a
  readList     :: ReadS [a]

  readPrec     :: ReadPrec a
  readListPrec :: ReadPrec [a]
  • ReadS a

    • 这是一个写 Parser 时一个非常基础的类型,就是 String -> [(a,String)] 的别名,其含义就是从一个字符串,获得一个数组的元组。元组中包含一个类型为 a 的最终数据,以及剩余字符串。这就是 readsPrec 最终会返回的类型,很明显,这个类型很方便我们获取反序列化的结果,直接套上 fst . head 就可能拿到结果。
  • data P a

    • 这是一个有趣的类型,在这个类型中存储的其实就是一个 Parser 的工作状态:

      • Get (Char -> P a) ,表示需要获取一个字符,然后返回一个新的 Parser 状态
      • Look (String -> P a),表示需要往后多看一个字符串,然后返回一个新的 Parser 状态
      • Fail,字面意思,表示 Parser 失败了
      • Result a (P a),表示获得了一个可能的解,然后返回一个新的 Parser 状态,继续尝试其他可能
      • Final [(a,String)],表示完成 parse 后得到的结果
    • 这样的数据结构,反映了一个很好的思路,就是将复杂运算 (此处就是 Parse)转化为各个不同的状态。只要能描述清楚状态,以及状态之间的转移,那逻辑就已将完成了一半。剩下就是把实际的复杂运算,藏到各个类型类的实例中去,比如 Monad。

    • 这里,P a 中的 Get 和 Look 天然具备了 (a -> P b) -> P b 的类型签名,可以知道,肯定要使用 CPS 了。

  • ReadP a

    • 一个 CPS 结构,这里的类型变量 a 就表示想让 Parser 收到的数据类型。在 Text.ParserCombinators.ReadP 中,显示声明了各种常见类型类的实例 Functor, Monad,之后还写了以 ReadP 为基础的一系列 Parser Combinators。
    • 笔者猜测,ReadP aReadS a 应该是同构的。对应的,在 ReadP 的模块中定义了 ReadPReadS 互转的两个函数。基于这个猜想,我们可以认为,ReadP 更适用于做实际 parse 工作,可以很好地进行 combinator 组合,最终再把结果转回 ReadS,方便直观地获取最终 parse 结果。

-- Text.ParserCombinators.ReadP 源码中,对 ReadS 有如下注释

-- Note that this kind of backtracking parser is very inefficient;
-- reading a large structure may be quite slow (cf 'ReadP').


- ReadPrec a
- 可以将 `ReadPrec a` 看作是 `ReadP a` 外面又套了一层 Reader Monad,传入的环境就是当前所处的操作优先级 Precedence。稍微看一下 ReadPrec 的源代码就会发现,其实就是把 ReadP 的很多操作加上了 lift。

- class Read a
- 两组函数,每组两个,一组使用 ReadS,另一组使用 ReadPrec,只要实现 `readPrec` 和 `readsPrec` 中的一个就可以工作。其中, Haskell 2010 Report 中规定要实现的是 `readsPrec`,而`readPrec` 是 GHC 才有的方法,也是 GHC 更推荐使用的方法。事实上,在绝大部分的 Read 实例中,Text.Read 也都是选择去实现 `readPrec`.


## Text.Read.Lex

说了那么多 Read 内部的类型和类型类,再来回看我们在 `instance Read (Tree a)` 中的实现,你会发现其实真正用到的生面孔只有两个函数,即 `readParen` 和 `lex`。没错,我们刚刚说的所有内容,实际上都藏在了这两个函数下。而 `readParen` 又是完全依赖于 `lex` 实现的用于匹配括号的辅助函数,所以接下来,就让我们把焦点放在 “lex” 上。

``` Haskell
lex :: ReadS String             -- As defined by H2010
lex s  = readP_to_S L.hsLex s

可以看到,lex 其实就是简单将 L.hsLex 从 ReadP 转成了 ReadS。所以真正实现都在 L 中,也就是 Text.Read.Lex 中。Text.Read.Lex 模块在 Hackage 上的描述是:

The cut-down Haskell lexer, used by Text.Read

由此可知,Text.Read.Lex 就是一个建议的 Haskell 语法解析器,且是专门写给 Text.Read 用的。翻阅一下它的代码,也可以发现,Lex 就是使用 ReadP 提供的 Parser Combinator 来对 Haskell 语法 (针对由 show 输出的文本)进行解析。

-- ^ Haskell lexemes.
data Lexeme
  = Char   Char         -- ^ Character literal
  | String String       -- ^ String literal, with escapes interpreted
  | Punc   String       -- ^ Punctuation or reserved symbol, e.g. @(@, @::@
  | Ident  String       -- ^ Haskell identifier, e.g. @foo@, @Baz@
  | Symbol String       -- ^ Haskell symbol, e.g. @>>@, @:%@
  | Number Number       -- ^ @since 4.6.0.0
  | EOF
 deriving (Eq, Show)

可以看到,Lex 定义了一些语言的基础元素,不过其中并没有涉及表达式和语句,这是因为 show 输出内容其实就只可能是一个表达式,所以只要好好描述表达式中可能包含的元素就够了。源代码中,有较大篇幅涉及数字和各种字符的解析,虽然值得阅读,但因为和 read 关系不大,所以就不再深入了。

最后再列一下 Text.Read 所有涉及的代码模块:

Text.Read
Text.Read.Lex
GHC.Read
Text.ParserCombinators.ReadP
Text.ParserCombinators.ReadPrec

总结

以上即笔者变读变总结的 Read 类型类的源码分析,希望对于大家理解 Read 能有所帮助。

对于 Read 类型类,大部分时候我们都会直接 deriving 它的实例,最多要实现,也只是简单得套用 readParenlex 就可以搞定了。但对于笔者而言,这里真正有趣的是理解 Read 的实现机制。这里特别有启发的就是两点:

  • 将状态描述为数据类型,然后以此来构建状态转移的情况,多数情况需要用到 Monad 和 Alternative 的实例来描述多状态组合时的效果。在 Text.Read 中即为 P a 类型。
  • 使用 CPS 搭配上面的状态类型,描述状态转移本身。在 Text.Read 中即为 newtype ReadP a 类型。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容