美文网首页程序员
「Text.Show」Day 2. - 30天Hackage之旅

「Text.Show」Day 2. - 30天Hackage之旅

作者: kdepp | 来源:发表于2016-11-22 23:41 被阅读68次

第二天,让我们回归基础,来看看 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))

可以看到推导出的 MyTypeShow 实例,非常聪明的给我们的数据加上了括号和空格,这样看上去就完全和它的实际意义吻合了。

初探源码

让我们来看一下 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.SetShow 实例,应该就会很好理解了:

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 ~

相关文章

网友评论

    本文标题:「Text.Show」Day 2. - 30天Hackage之旅

    本文链接:https://www.haomeiwen.com/subject/owrmpttx.html