<譯> 積與餘積

原文見 http://bartoszmilewski.com/20...c++

-> 上一篇:『Kleisli 範疇編程

沿着箭頭

古希臘劇做家 Euripides 曾說過:『每一個人都像他盡力維護的同伴』。咱們被咱們的人際關係所定義。在範疇論中更是這樣。若是想刻畫範疇中的一個對象,只能經過描述這個對象與其餘對象(或其自身)之間的關係模式來實現。segmentfault

範疇論中有一個常見的構造,叫作泛構造(Universal Construction),它就是經過對象之間的關係來定義對象,其方式之一就是拮取一個模式——由對象與態射構成的一種特殊的形狀,而後在範疇中觀察它的各個方面。若是這個模式很常見並且範疇也很大,你就會有大量的機會命中它。技巧是如何對這些命中機會進行排序,並選出最好的那個。網絡

這個過程就相似於咱們進行網絡搜索時所採用的方式。一次查詢就是一個模式。一次很是寬泛的查詢會召來大量的命中。其中有些多是你所關注的,其餘的則不是。爲了消減無關的命中,你會對查詢進行優化,來提升它的命中精度。最終,搜索引擎會對命中的結果劃分等級,頗有可能你想要的結果就位於命中結果等級序列的頂端。dom

初始對象

最簡單的形狀是單個的對象。顯然,這種形狀有許多實例,由於給定的範疇中有許多對象。可選對象過多。咱們須要將它們劃分等級,去尋找處於最上層的那個對象。這意味着咱們要根據態射來劃分對象的等級(譯註:不然範疇中還有什麼?)。若是將態射想象爲箭頭,它們可能會造成一張網,在這張網中,箭頭從範疇的一端流向另外一端。在有序的範疇中會出現這種狀況,例如偏序範疇。能夠將對象前後次序的概念推廣一下,即:若是說對象 a 比對象 b 更靠前,那麼必定存在一個箭頭(態射)是從 a 到 b 的。若是有一個對象,它發出的箭頭指向全部其餘的對象,那麼這個對象就叫作初始對象。顯然,對於一個給定的範疇,可能沒法保證其中初始對象的存在,這還好說,更大的問題是其中可能有不少個初始對象:召來的東西都挺不錯,可是精度不夠。不過,能夠從有序的範疇那裏獲得一些啓示——任意兩個對象之間,它們只容許最多存在 1 個箭頭:小於或等於另一個對象。通過有序範疇的引導,咱們這樣定義初始對象:ide

初始對象,這種對象有且僅有一個態射指向範疇中的任意一個對象。函數

初始對象

然而,即便這樣定義初始對象也沒法擔它的惟一性(若是它存在),可是這個定義能擔保最好的一件事是:在同構意義下,它具備惟一性。同構(Isomorphism)在範疇論中很是重要,過會兒再談它。如今,咱們只須要認定,初始對象的定義主要是爲了讓初始對象在同構意義下具有惟一性。性能

在此,給出幾個與初始對象有關的例子:偏序集(一般稱爲 poset)的初始對象是那個最小的對象。有些偏序集沒有初始對象——例如整數集。優化

在集合範疇中,初始對象是是空集。記住,在 Haskell 中,空集至關於 Void 類型(C++ 中沒有相對應的類型),並且從 Void 到任意類型的多態函數是惟一的,它叫 absurd搜索引擎

absurd :: Void -> a

它是一個態射族,由於它的存在,Void 纔會成爲類型範疇中的初始對象。

終端對象

繼續考察單對象模式,如今改變一下劃分對象等級的方式。若是有一個態射從對象 b 到對象 a,那麼認爲 a 比 b 更靠後。咱們要在範疇中尋找比任何其餘對象都靠後的那個對象,而且堅持認定它也具有着這樣的惟一性:

終端對象,這種對象有且僅有一個態射來自範疇中的任意對象。

終端對象

一樣,終端對象在同構意義下具備惟一性。同構是什麼鬼,後面會講。來看幾個例子。在偏序集內,若是存在着終端對象,那麼它是那個最大的對象。在集合範疇中,終端對象是一個單例。咱們已經討論過單例,它們至關於 C++ 中的 void 類型,也至關於 Haskell 中的 unit 類型 ()。單例就是隻有一個值的類型,在 C++ 中是隱匿的,在 Haskell 中是顯式的。咱們已經肯定,有且僅有一個純函數,它從任意類型到 unit 類型:

unit :: a -> ()
unit _ = ()

所以,對於單例而言,終端對象的全部條件都是可以知足的。

注意,上面示例中,惟一性的條件是很是重要的,由於有些集合(其實是除了空集的全部集合)會有來自其餘集合的態射。例如,有一個布爾類型的函數(謂詞),它的定義適合全部類型:

yes :: a -> Bool
yes _ = True

可是 Bool 並非一個終端對象,由於至少還存在一個一樣是適合全部類型的布爾類型的函數:

no :: a -> Bool
no _ = False

堅持惟一性可以獲得足夠的精度,能夠將終端對象的定義緊縮爲一種類型。

對偶

你確定已經注意到了初始對象與終端對象是對稱的。兩者之間惟一的區別是態射的方向。事實上,對於任意範疇 C,咱們總能定義一個相反的範疇 C',只需將 C 中的箭頭都反轉一下,而後再從新定義一下態射的複合方式,那麼相反的範疇就可以天然知足全部的範疇法則。若是原始態射 f::a->bg::b->ch=g∘f 複合爲 h::a->c,那麼逆向的態射 f'::b->ag'::c->b 可經過 h'=f'∘g' 複合爲 h'::c->a。恆等箭頭保持不變。

對偶是範疇的一個很是重要的性質,由於它可以讓數學家在範疇論中的工做事半功倍。你所提出的每一種構造,都有其對偶的構造;你所證實的任何一個定理,都會獲得它的一個免費版本。相反的範疇中的構造,一般冠以『co(餘)』,所以就有了 product(積)與coproduct(餘積),monad(單子) 與 comonad(餘單子),cone(錐)與 cocone(餘錐)等等。沒有 cocomonad(余余單子),由於箭頭反轉兩次又回到初始狀態。

一個範疇中的終端對象,在相反範疇中就是初始對象。

同構

做爲程序猿,咱們深知相等不是那麼容易定義。兩個對象相等是什麼意思?它們是佔據相同的位置(指針相等)麼?或者它們全部成員的值相等麼?兩個複數,若是其中一個用實部與虛部來表示,另外一個用模與幅角來表示,它們是相等的麼?你可能會認爲數學家可以描述出相等的意義,但實際上他們也作不到。他們不得不定義出來多種相等,有命題相等、內涵相等、外延相等,還有拓撲類型理論中的路徑相等之類。因而,就出現了一種弱化的相等概念——同構。

直覺上,同構的對象看上去是同樣的,也就是說它們有相同的形狀。這意味着一個對象的每個部分都能與另外一個對象的某一個部分造成一對一的映射,直到咱們的『儀器』可以檢測出這兩個對象是彼此的拷貝。在數學上,這意味着存在一個從對象 a 到對象 b 的映射,同時也存在着一個從對象 b 到對象 a 的映射,這兩個映射是互逆的。在範疇論中,咱們用態射取代了映射。一個同構是一個可逆的態射;或者是一對互逆的態射。

咱們能夠經過複合與恆等來理解互逆:若是態射 g 與態射 f 的複合結果是恆等態射,那麼 g 是 f 的逆。這體現爲如下兩個方程,由於兩個態射存在兩種複合形式:

f . g = id
g . f = id

前面我說過,初始(終端)對象在同構意義下具備惟一性,個人意思就是任意兩個初始(終端)對象都是同構的。這實際上很容易看出來。假設兩個初始對象 $i_1$ 與 $i_2$,由於 $i_1$ 是初始對象,所以有惟一的態射 f 從 $i_1$ 到 $i_2$,同理也有惟一的態射 g 從 $i_2$ 到 $i_1$。這兩個態射的複合結果是什麼?

同構

圖中全部的態射都具備惟一性

g∘f 確定是從 $i_1$ 到 $i_1$ 的態射,由於 $i_1$ 是初始對象,所以它只允許一個從 $i_1$ 到 $i_1$ 的態射的存在。由於咱們是在一個範疇中,咱們知道從 $i_1$ 到 $i_1$ 的態射就是一個恆等態射,所以 g∘f 等於恆等態射。同理,f∘g 的結果也是恆等態射。這樣就證實了 f 與 g 是互逆的,所以兩個初始對象就是同構的。

注意,在上述證實中,咱們使用的是從初始對象到它自己的態射的惟一性。若是沒有這個前提條件,咱們就沒法證實『同構意義下』這部分。可是,爲何須要 f 與 g 也是惟一的?由於初始對象不只在同構意義下具備惟一性,並且這個同構是惟一的。理論上,兩個對象之間可能存在不止一種同構關係,雖然它們未在這裏出現。這種『在同構意義下具備惟一性,並且這個同構是惟一的』是全部泛構造的一個重要性質。

還有一個泛構造,叫作積。咱們知道兩個集合的笛卡爾積:序對的集合。可是積集合與成分集合(譯註:參與積運算的集合),它們之間存在着什麼樣的鏈接模式?若是咱們能說清楚它,那麼就可以將積推廣到其餘範疇。

從積到每一個成分,存在兩個投影。在 Haskell 中,這兩個函數被稱爲 fstsnd,它們分別從序對中拮取第一與第二個成員:

fst :: (a, b) -> a
fst (x, y) = x

snd :: (a, b) -> b
snd (x, y) = y

在此,這兩個函數是採用參數的模板匹配來定義的,即對於任意序對 (x, y),模板匹配能夠從中抽取變量 xy

這些定義能夠用通配符做進一步簡化:

fst (x, _) = x
snd (_, y) = y

在 C++ 中,可使用模板函數來模擬,例如:

template<class A, class B>
A fst(pair<A, B> const &p) {
    return p.first;
}

藉助這看上去很是有限的知識,咱們試着去定義集合範疇中對象與態射的模式,這種模式能夠引導咱們去構造兩個集合 a 與 b 的積。這個模式由對象 c 與兩個態射 p 與 q 構成,p 與 q 將 c 分別連向 a 與 b:

p :: c -> a
q :: c -> b

積的模式

全部的適合這種模式的 c 都會被認爲是候選積,由於這樣的 c 可能有不少。

候選積

例如,從 Haskell 類型中選擇 IntBool,讓它們相乘,並將 IntBool 做爲候選積。

假設 Int 是候選積。Int 可以被認爲是 IntBool 相乘的候選積嗎?是的,它能,由於它具備如下投影:

p :: Int -> Int
p x = x

q ::  Int -> Bool
q _ = True

雖然至關無聊,但它符合候選積的條件。

還有一個 (Int, Int, Bool),這是個三元組。它也是個合法的候選積,由於存在:

p :: (Int, Int, Bool) -> Int
p (x, _, _) = x

q :: (Int, Int, Bool) -> Bool
q (_, _, b) = b

可能你會注意到,第一個候選積過小了——它只覆蓋了乘積的 Int 維,而第二個候選積又太大了——它複製了一個不必的 Int 維。

咱們尚未探索這個泛構造的其餘部分:等級劃分。咱們但願可以比較這種模式的兩個實例,也就是說對於候選積 c 與候選積 c',咱們想對它們作一些比較,以便做出 c 比 c'『更好』這樣的結論。若是有一個從 c' 到 c 的態射 m,雖然能夠基於這個態射認爲 c 比 c' 更好,可是這樣仍是太弱了。由於咱們還但願 c 伴隨的投影 p 與 q 比 c' 的 p' 與 q' 『更好』或『更泛』,這意味着能夠經過態射 m 從 q 與 q 分別構造出 p' 與 q':

p' = p . m
q' = q . m

積的等級

從另外一個角度來看,這些方程像是 m 因式化了 p' 與 q'。若將上面的這兩個方程想象爲天然數,將點符號想象爲相乘,那麼 m 就是 p' 與 q' 的公因式了。

上面只是爲了創建一些直覺,如今來看一下伴隨序對 (Int, Bool) 的兩個經典的投影,fstsnd,它們要比剛纔我給出的那兩個候選積的投影更好。

非積

第一個候選積的 m 是:

m :: Int -> (Int, Bool)
m x = (x, True)

這個候選積的兩個投影可重構爲:

p x = fst (m x) = x
q x = snd (m x) = True

對於第二個候選積而言,m 彷佛是惟一的:

m (x, _, b) = (x, b)

咱們說了 (Int, Bool) 要比前兩個候選積更好。如今給出咱們的理由。問一下本身,咱們能找到某個 m',它可以幫助咱們從伴隨候選積的 pq 重構出 fstsnd 麼?

fst = p . m'
snd = q . m'

對於第一個候選積,q 老是返回 True,而咱們知道存在第 2 個元素是 False 的序對,所以沒法從 q 重構 snd

第二個候選積就不一樣了,咱們可以在 pq 運行後保留足夠的信息,可是對於 fstsnd 而言,它們存在着多種因式化方式,由於 pq 會忽略三元組的第 2 個元素,這就意味着咱們的 m' 能夠在第 2 個元素的位置聽任意東西,例如:

m' (x, b) = (x, x, b)

或

m' (x, b) = (x, 42, b)

等等。

總而言之,對於給定的任意類型 c,它伴隨着兩個投影 pq,存在着惟一的一個 m 可將 c 映射爲笛卡爾積 (a, b),而這個 mpq 的因式化就是將 pq 組合成序對:

m :: c -> (a, b)
m x = (p x, q x)

這樣就決定了笛卡爾積 (a, b) 是最好的候選積,這意味着這種泛構造對於集合範疇是有效的,它涵蓋了任意兩個集合的積。

如今,咱們忘記集合,使用相同的泛構造來定義任意範疇中兩個對象的積。這樣的積並不是老是存在,可是一旦它存在,它就在同構意義下具備惟一性,並且這個同構是惟一的。

對象 a 與對象 b 的是伴隨兩個投影的對象 c。對於任何其餘伴隨兩個投影的對象 c' 而言,存在惟一的從 c' 到 c 的態射,這個態射能夠因式化這兩個投影。

一個高階函數可以生成因子 m,這個高階函數有時被稱爲因子生成器。對於本文的示例中,它是這樣的函數:

factorizer :: (c -> a) -> (c -> b) -> (c -> (a, b))
factorizer p q = \x -> (p x, q x)

餘積

同範疇論中每一個構造同樣,積有一個對偶,叫作餘積。將積的範式中的箭頭反轉,就能夠獲得一個對象 c,它伴隨兩個入射 ij——從 a 到 c 的態射與從 b 到 c 的態射。

i :: a -> c
j :: b -> c

餘積模式

等級也反轉了:對象 c 比 c' 『更好』的條件是,存在從 c 到 c' 的態射 m,它能夠因式化入射:

i' = m . i
j' = m . j

餘積等級

『最好』的對象就是,具備惟一的態射從其自己指向其餘模式,這種對象就叫作餘積,而且若是它存在,那麼它就在同構意義下具備惟一性,並且這個同構是惟一的。

兩個對象 a 與 b 的餘積是對象 c,當且僅當 c 伴隨着兩個入射,並且任何一個其餘的伴隨兩個入射的對象 c',只存在惟一的從 c 到 c' 的態射 m,而且 m 能夠因式化這些入射。

在集合的範疇中,餘積就是兩個集合的不相交求並運算。集合 a 與集合 b 的不相交求並結果中的一個元素,要麼是 a 中的元素,要麼是 b 中的元素。若是兩個集合有交集,那麼不相交求並的結果會包含兩個集合公共部分的兩份拷貝。你能夠將不相交求並運算結果的一個元素想象爲貼着它所屬集合的標籤的元素。

對於程序猿而言,餘積很容易理解,它不過是兩種類型的帶標籤的聯合。C++ 有聯合類型,只不過它們沒標籤。若是你在程序中想跟蹤哪一個聯合的成員有效,必須用枚舉類型自行定義標籤。例如,intchar const * 的一個標籤化聯合類型以下:

struct Contact {
    enum { isPhone, isEmail } tag;
    union { int phoneNum; char const * emailAddr; };
};

兩個射入能夠實現爲構造子或函數。例如,下面是第一個射入的函數實現:

Contact PhoneNum(int n) {
    Contact c;
    c.tag = isPhone;
    c.phoneNum = n;
    return c;
}

它將一個整型類型射入 Contact

帶標籤的聯合被稱爲變體(Variant),boost 庫裏實現了一個泛型的變體 boost::variant

在 Haskell 中,你能夠將任意數據類型組合爲帶標籤的聯合,只需用豎線隔開數據構造子便可。上面 C++ 的 Contact 的示例能夠翻譯爲:

data Contact = PhoneNum Int | EmailAddr String

這裏,PhoneNumEmailAddr 都是構造子(入射),也能夠做爲模式匹配(之後會講)時所用的標籤。例如,將電話號碼構造爲一個 Contact

helpdesk :: Contact;
helpdesk = PhoneNum 2222222

對於 Haskell 而言,與基於內建的序對而實現的『正統』積不一樣,『正統』的餘積的既有實現是標準庫中定義的 Either 數據類型:

Either a b = Left a | Right b

這個數據類型接受兩個參數 ab,它還擁有兩個構造子:Left 接受類型 a 的值,Right 接受類型 b 的值。

正如咱們剛纔所定義的積的因式生成器同樣,咱們也能夠爲餘積定義一個。對於給定的候選餘積 c 以及兩個候選入射 ij,爲 Either 生成因式函數的的因式生成器可定義爲:

factorizer :: (a -> c) -> (b -> c) -> Either a b -> c
factorizer i j (Left a)  = i a
factorizer i j (Right b) = j b

非對稱

咱們已經見識了兩種對偶結構:終端對象可由初始對象的箭頭反轉而得到,餘積可由積的箭頭的反轉而得到。不過,在集合的範疇中,初始對象與終端對象有着顯著的區別,餘積與積也有着顯著區別。之後會看到積的行爲像是乘法運算,終端對象扮演者 1 的角色,而餘積的行爲更像求和運算,初始對象扮演着 0 的角色。在特殊狀況下,對於有限集,積的尺寸就是各個集合的尺寸的積,而餘積的尺寸是各個集合的尺寸之和。

這一切都代表了集合的範疇不會隨箭頭的反轉而出現對稱性。

注意,空集能夠向任意一個集合發出惟一的態射(absurd 函數),可是它沒有其餘集合發來的態射。單例集合擁有任意集合發來的惟一的態射,但它也能向任一集合(除了空集)發出態射。由終端對象發出的態射在拮取其餘集合中的元素方面扮演了重要的角色(空集沒有元素,所以沒什麼東西可拮取的)。

單例做爲積與它做爲餘積有着天壤之別。要將單例 () 做爲品質低劣的候選積,須要給它配備兩個投影 pq。因爲積是泛構造,所以存在一個從 () 到積的態射 m。這個態射從積集合中選出一個元素——序對,它也因式化了兩個投影:

p = fst . m
q = snd . m

這兩個投影做用於單例的值 (),上面那兩個方程就變爲:

p () = fst (m ())
q () = snd (m ())

由於 m ()m 從積集合中拮取的元素,p 所拮取的是第一個參與積運算的集合中的元素,結果是 p (),而 q 所拮取的是第二各參與積運算的集合中的元素,結果是 q ()。這徹底符合咱們對積的理解,即參與積運算的集合中的元素造成積集合中的序對。

若單例做爲候選的餘積,就不會它做爲候選的積那樣簡單了。咱們能夠經過投影從單例中抽取元素,可是向單例入射就顯得沒有意義了,由於它們的『源』在入射時會丟失。從真正的餘積到做爲候選餘積的單例的態射也不是惟一的。集合的範疇,從初始對象的方向去看,與從終端對象的方向去看,是有顯著差別的。

這其實不是集合的性質,而是是咱們在 Set 中做爲態射使用的函數的性質。函數,一般是非對稱的,下面我來解釋一下。

函數是創建在它的定義域(Domain)上的(在編程中,稱之爲函數),它沒必要覆蓋餘定義域(Codomain,譯註:可能叫陪域更正式一些)。咱們已經看到了一些極端的例子(實際上,全部定義域是空集的函數都是極端的):定義域是單例的函數,意味着它只在餘定義域上選擇了一個元素。若定義域的尺度遠小於餘域的尺度,咱們一般認爲這樣的函數是將定義域嵌入餘定義域中了。例如,咱們能夠認爲,定義域是單例的函數,它將單例嵌入到了餘定義域中。我將這樣的函數稱爲嵌入函數,可是數學家給從相反的角度進行命名:覆蓋了餘定義域的函數稱爲滿射(Surjective)函數或映成(Onto)函數。

函數的非對稱性也表現爲,函數能夠將定義域中的許多元素映射爲餘定義域上的一個元素,也就是說函數坍縮了。一個極端的例子是函數使整個集合坍縮爲一個單例,unit 函數乾的就是這事。這種坍縮只能經過函數的複合進行混成。兩個坍縮函數的複合,其坍縮能力要強過兩者單兵做戰。數學家爲非坍縮函數取了個名字:內射(Injective)或一對一(One-to-one)映射。

固然,有許多函數即不是嵌入的,也不是坍縮的。它們被稱爲雙射(Bijection)函數,它們是徹底對稱的,由於它們是可逆的。在集合範疇中,同構就是雙射的。

$$ \cdot $$

挑戰

1. 證實終端對象在同構意義下具備惟一性,並且這個同構是惟一的。
2. 偏序集內的兩個對象的積是什麼?提示:使用泛構造。
3. 偏序集內的兩個對象的餘積是什麼?
4. 用你喜歡的語言(Haskell 除外),實現與 Haskell 的 Either 等價的泛型類型。
5. 證實 Either 是比 int 更好的餘積。int 伴隨的兩個入射是:

int i(int n) { return n; }
int j(bool b) { return b? 0: 1; }

提示:定義一個函數

int m(Either const & e);

用它因式化 ij

6. 繼續上一個問題,如何證實 int 不會比 Either 更好?

7. 依然繼續上述第 5 個問題:下面的入射怎麼樣?

int i(int n) { 
    if (n < 0) return n; 
    return n + 2;
}
int j(bool b) { return b? 0: 1; }

8. 針對 intbool,給出一個品質低劣的候選餘積,使之有多個態射抵達 Either

參考文獻

  1. The Catsters, Products and Coproducts video.

致謝

感謝 Gershom Bazerman 對這篇文檔的審閱以及針對性的討論。

-> 下一篇:『簡單的代數數據類型

相關文章
相關標籤/搜索