第二天,让我们回归基础,来看看 Text.Show
这个 base
中非常基本的函数库。
初步使用
对 Text.Show 的最基础使用,莫过于自动推导 Show 的实例,可以分别使用 deriving
的两种形式来完成这个功能:
-- Option 1: deriving 子句
data MyType = ThisType Int | ThatType MyType
deriving (Show)
-------------------------------------------------------
-- Option 2: 独立的 deriving 声明
-- 需要开启这个扩展,才能使用独立 deriving
{-# LANGUAGE StandaloneDeriving #-}
data MyType = ThisType Int | ThatType MyType
-- 故意隔开一行,表示不是子句
deriving instance Show MyType
在 GHCi 中简单调用 show 来看看输出的结果:
Prelude> let a = ThatType $ ThatType $ ThisType 10
Prelude> print a
-- 输出结果为:
ThatType (ThatType (ThisType 10))
可以看到推导出的 MyType
的 Show
实例,非常聪明的给我们的数据加上了括号和空格,这样看上去就完全和它的实际意义吻合了。
初探源码
让我们来看一下 Text.Show
内部都是怎么实现的,Show
这个 type class 都定义了什么接口方法?
class Show a where
{-# MINIMAL showsPrec | show #-}
showsPrec :: Int -- ^ the operator precedence of the enclosing
-- context (a number from @0@ to @11@).
-- Function application has precedence @10@.
-> a -- ^ the value to be converted to a 'String'
-> ShowS
-- | A specialised variant of 'showsPrec', using precedence context
-- zero, and returning an ordinary 'String'.
show :: a -> String
showList :: [a] -> ShowS
showsPrec _ x s = show x ++ s
show x = shows x ""
showList ls s = showList__ shows ls s
从以上代码可已看出以下几点:
- 有一个叫
Shows
的类型出现了两次,那是什么鬼? -
Show
这个类型类的最小实现是show
或者showsPrec
- 除此之外的方法,就只有
showList
,为什么需要showList
呢?
Shows 类型
看一下 Shows 类型的定义:
type ShowS = String -> String
这种定义方法十分有趣,我们可以直接把 Shows
看作是字符串类型,只不过两个字符串的链接,变得不太一样:
-- 普通字符串
str1 = "foo"
str2 = "bar"
str3 = str1 ++ str2
-- Shows
str1 = (++) "foo"
str2 = (++) "bar"
str3 = str1 . str2
使用 Shows
最大的好处是,获得 O(1) 的拼接效率,这特别适合像 Show 这样需要拼接众多小字符串的场景。事实上,像 Shows
这样表示字符串或数组的方式,被称作 Difference List ,即把一个数组表示为 [a] -> [a]
。在 Hackage 上还专门有一个库,实现了一个叫 DList 的类型,来对应这个概念,其中包含了一部分非常常用的数组操作函数。
show 函数
在 Text.Show
中,我们平时用的做多的肯定就是 show
,而实际上,默认的 show
的定义非常简单:
show x = showsPrec 0 x ""
其实就是 precedence = 0 的情况下,最终拼接一个空字符串,获得返回结果。
showsPrec 函数
我们可以拆开 showsPrec
的名字,来理解它的存在价值,即 'shows' + 'Prec(edence)' ,也就是说 showsPrec
是一个最终返回 Shows
类型的,会考虑代码组合优先级的函数。
Shows
的部分已经讲过了,现在来看下 Precedence 是干嘛用的。简单说就是:
- 大目标:当我们需要输出一个自定义类型的 toString 字符串,其实我们是希望能够一眼看出它在计算机内部实际代表的内容
- 小目标:在输出一个表达式时,toString 字符串需要能表达出操作符优先级,有必要的话,加上括号以作区隔。
下面我们以 Text.Show
源代码中的示例为参考:
infixr 5 :^:
data Tree a = Leaf a | Tree a :^: Tree a
-- the derived instance of 'Show' is equivalent to
instance (Show a) => Show (Tree a) where
showsPrec d (Leaf m) = showParen (d > app_prec) $
showString "Leaf " . showsPrec (app_prec+1) m
where app_prec = 10
showsPrec d (u :^: v) = showParen (d > up_prec) $
showsPrec (up_prec+1) u .
showString " :^: " .
showsPrec (up_prec+1) v
where up_prec = 5
-- Note that right-associativity of @:^:@ is ignored. For example,
--
-- * @'show' (Leaf 1 :^: Leaf 2 :^: Leaf 3)@ produces the string
-- @\"Leaf 1 :^: (Leaf 2 :^: Leaf 3)\"@.
上面的例子可以看到,这个 Tree a
的 Show 实例定义,就是在转为字符串时,显示地把右结合的那组括号打印出来,即原来的 Leaf 1 :^: Leaf 2 :^: Leaf 3
输出后变成了 "Leaf 1 :^: (Leaf 2 :^: Leaf 3)"
。
-
第一个 pattern match 匹配了
Leaf m
的情况,表示只有在当前优先级大于 10 的情况下,也就是函数调用 (function application) 的优先级时,才在Leaf m
外围增加括号。且对于更深一步的内容,采用优先级 11 进行处理。 -
第二个 pattern match 匹配了
Tree a :^: Tree a
的情况,当优先级的要求超过 5 (也就是:^:
所定义的优先级) 时,才需要在外面加上括号。且对于更深一步的内容,采用优先级 6 进行处理。
到这里,我们已经可以看出, showsPrec
的第一个参数 precedence,其实表达的就是,需要我内部最外层的操作符达到什么样的优先级,才不需要在外围加括号。
到这里,当我们再看到 Data.Set
的 Show
实例,应该就会很好理解了:
instance Show a => Show (Set a) where
showsPrec p xs = showParen (p > 10) $
showString "fromList " . shows (toList xs)
showList 函数
众所周知,String
就是 [Char]
,即内部类型为 Char 的数组,而打印一个 String
的结果也和打印其他数组不同:
Prelude> print [1,2,3]
--- 结果为:
[1,2,3]
Prelude> print ['a','b','c']
--- 结果为:
“abc"
其原因是, instance Show Char
中定义了特殊的 showList
:
instance Show Char where
showsPrec _ '\'' = showString "'\\''"
showsPrec _ c = showChar '\'' . showLitChar c . showChar '\''
showList cs = showChar '"' . showLitString cs . showChar '"'
看着很不错,但这就是为什么一定要有 showList
的原因吗?为什么不能直接给 [Char]
定义 Show
实例呢?类似如下:
instance Show [Char] where
...
其实这里使用了一个 trick,并被国外的某位仁兄定义为 「The “extra-method” trick pattern」。其初衷就是为了避免 overlapping instances 。来看下 Text.Show
源代码的这部分定义:
instance Show a => Show [a] where
showsPrec _ = showList
在有这个通用 Show [a]
存在的情况下,如果还要定义 instance Show [Char]
,就会造成 overlapping instances。当然也可以这么继续写,前提是开启 OverlappingInstances
扩展 (这是一个会造成 instance 小混乱的开关,不开为妙)。这时候,trick 就来了,即使把新加一个 showList
方法,并让它在 class 中的默认实现与 instance Show Char
中的实现不同,实现多态,这是可以的!!
这就是产生 showList
的原因。
总结
可以看到,看似简单的 Text.Show
,其背后还是有非常多值得借鉴和理解的知识点的。下一次我们继续来看一个基础模块 Read ~
网友评论