第二天稼钩,讓我們回歸基礎(chǔ)顾稀,來看看 Text.Show
這個 base
中非常基本的函數(shù)庫坝撑。
初步使用
對 Text.Show 的最基礎(chǔ)使用静秆,莫過于自動推導(dǎo) 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 中簡單調(diào)用 show 來看看輸出的結(jié)果:
Prelude> let a = ThatType $ ThatType $ ThisType 10
Prelude> print a
-- 輸出結(jié)果為:
ThatType (ThatType (ThisType 10))
可以看到推導(dǎo)出的 MyType
的 Show
實例,非常聰明的給我們的數(shù)據(jù)加上了括號和空格击儡,這樣看上去就完全和它的實際意義吻合了塔沃。
初探源碼
讓我們來看一下 Text.Show
內(nèi)部都是怎么實現(xiàn)的,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
的類型出現(xiàn)了兩次蛀柴,那是什么鬼螃概? -
Show
這個類型類的最小實現(xiàn)是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
這樣表示字符串或數(shù)組的方式儒飒,被稱作 Difference List 谬莹,即把一個數(shù)組表示為 [a] -> [a]
。在 Hackage 上還專門有一個庫桩了,實現(xiàn)了一個叫 DList 的類型附帽,來對應(yīng)這個概念,其中包含了一部分非常常用的數(shù)組操作函數(shù)井誉。
show 函數(shù)
在 Text.Show
中蕉扮,我們平時用的做多的肯定就是 show
,而實際上颗圣,默認(rèn)的 show
的定義非常簡單:
show x = showsPrec 0 x ""
其實就是 precedence = 0 的情況下喳钟,最終拼接一個空字符串,獲得返回結(jié)果欠啤。
showsPrec 函數(shù)
我們可以拆開 showsPrec
的名字荚藻,來理解它的存在價值,即 'shows' + 'Prec(edence)' 洁段,也就是說 showsPrec
是一個最終返回 Shows
類型的,會考慮代碼組合優(yōu)先級的函數(shù)共郭。
Shows
的部分已經(jīng)講過了祠丝,現(xiàn)在來看下 Precedence 是干嘛用的。簡單說就是:
- 大目標(biāo):當(dāng)我們需要輸出一個自定義類型的 toString 字符串除嘹,其實我們是希望能夠一眼看出它在計算機內(nèi)部實際代表的內(nèi)容
- 小目標(biāo):在輸出一個表達(dá)式時写半,toString 字符串需要能表達(dá)出操作符優(yōu)先級,有必要的話尉咕,加上括號以作區(qū)隔叠蝇。
下面我們以 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 實例定義年缎,就是在轉(zhuǎn)為字符串時悔捶,顯示地把右結(jié)合的那組括號打印出來铃慷,即原來的 Leaf 1 :^: Leaf 2 :^: Leaf 3
輸出后變成了 "Leaf 1 :^: (Leaf 2 :^: Leaf 3)"
。
第一個 pattern match 匹配了
Leaf m
的情況蜕该,表示只有在當(dāng)前優(yōu)先級大于 10 的情況下犁柜,也就是函數(shù)調(diào)用 (function application) 的優(yōu)先級時,才在Leaf m
外圍增加括號堂淡。且對于更深一步的內(nèi)容馋缅,采用優(yōu)先級 11 進行處理。第二個 pattern match 匹配了
Tree a :^: Tree a
的情況绢淀,當(dāng)優(yōu)先級的要求超過 5 (也就是:^:
所定義的優(yōu)先級) 時萤悴,才需要在外面加上括號。且對于更深一步的內(nèi)容皆的,采用優(yōu)先級 6 進行處理稚疹。
到這里,我們已經(jīng)可以看出祭务, showsPrec
的第一個參數(shù) precedence内狗,其實表達(dá)的就是,需要我內(nèi)部最外層的操作符達(dá)到什么樣的優(yōu)先級义锥,才不需要在外圍加括號柳沙。
到這里,當(dāng)我們再看到 Data.Set
的 Show
實例拌倍,應(yīng)該就會很好理解了:
instance Show a => Show (Set a) where
showsPrec p xs = showParen (p > 10) $
showString "fromList " . shows (toList xs)
showList 函數(shù)
眾所周知赂鲤,String
就是 [Char]
,即內(nèi)部類型為 Char 的數(shù)組柱恤,而打印一個 String
的結(jié)果也和打印其他數(shù)組不同:
Prelude> print [1,2,3]
--- 結(jié)果為:
[1,2,3]
Prelude> print ['a','b','c']
--- 結(jié)果為:
“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眼俊。當(dāng)然也可以這么繼續(xù)寫,前提是開啟 OverlappingInstances
擴展 (這是一個會造成 instance 小混亂的開關(guān)粟关,不開為妙)疮胖。這時候,trick 就來了,即使把新加一個 showList
方法澎灸,并讓它在 class 中的默認(rèn)實現(xiàn)與 instance Show Char
中的實現(xiàn)不同院塞,實現(xiàn)多態(tài),這是可以的;骱ⅰ迫悠!
這就是產(chǎn)生 showList
的原因。
總結(jié)
可以看到巩梢,看似簡單的 Text.Show
创泄,其背后還是有非常多值得借鑒和理解的知識點的。下一次我們繼續(xù)來看一個基礎(chǔ)模塊 Read ~