9 ความเป็นFunctor (Draft)
ในตอนนี้คุณได้รู้ว่าfunctorคืออะไรและได้เห็นตัวอย่างต่างๆของมัน เรามาดูวิธีในการสร้างfunctorที่ใหญ่กว่าจากตัวเล็กๆ มันนั้นน่าสนใจโดยเฉพาะเช่นการที่จะเห็นconstructorของtype(ที่คือการโยงระหว่างวัตถุในcategory) สามารถถูกเสริมเติมให้เป็นfunctor(ที่รวมไปภึงการโยงระหว่างmorphism)
9.1 Bifunctors
เนื่องด้วยว่าfunctorsคือmorphismใน\(\textbf{Cat}\)(categoryของcategory)ความเข้าใจหลายๆอย่างที่เกี่ยวกับmorphism (และfunctionโดยเฉพาะ) ก็สามารถถูกใช้ได้กับfunctorsเช่นเดียวกัน ตัวอย่างเช่นเดียวกันกับการที่คุณมีfunctionที่มีสองargument คุณสามารถที่จะมีfunctorที่มีสองargumentหรือbifunctor ถ้าพูดถึงการกระทำบนวัตถุ bifunctorนั้นโยงทุกๆคู่ของวัตถุอันหนึ่งจากcategory\(\textbf{C}\)และวัตถุจากcategory\(\textbf{D}\) ไปยังวัตถุในcategory\(\textbf{E}\) สังเกตได้ว่าสิ่งนี้ก็แค่คือการโยงจากการคูณแบบCartesian ของcategoryอย่าง\(\textbf{C}\times\textbf{D}\)ไปยัง\(\textbf{E}\)
สิ่งนี้ค่อนข้างตรงไปตรงมา แต่ความเป็นfunctorหมายความว่า bifunctorต้องทำการโยงmorphismเช่นเดียวกัน แต่ในตอนนี้มันต้องโยงคู่ของmorphismที่ตัวหนึ่งมาจาก\(\textbf{C}\) และตัวหนึ่งมาจาก\(\textbf{D}\)ไปยังmorphismใน\(\textbf{E}\)
อีกครั้งว่าคู่ของmorphismคือแค่morphismเดี่ยวในproduct category \(\textbf{C}\times\textbf{D}\)ไปยัง\(\textbf{E}\) เรานิยามmorphismในการคูณแบบCartesianของcategoryต่างๆในฐานะคู่ของmorphismที่เริ่มจากคู่แรกของวัตถุไปยังอีกคู่หนึ่งของวัตถุ คู่morphismเหล่านี้สามารถถูกประกอบในวิธีที่ตรงไปตรงมา
\[ (f, g) \circ (f', g') = (f \circ f', g \circ g') \]
การประกอบแบบนี้นั้นมีคุณสมบัติการเปลี่ยนหมู่และมันมีidentity(คู่ของidentity morphismอย่าง \((\operatorname{id}, \operatorname{id})\)) ดังนั้นคูณแบบCartesianของcategoryก็เป็นcategory
วิธีที่ง่ายกว่าในการคิดเกี่ยวกับbifunctorก็คงจะเป็นการพิจารณามันในแต่ละargumentแยกออกไป ดังนั้นแทนที่จะแปลฏทางfunctor (การรักษาสมบัติการเปลี่ยนหมู่และidentity) จากfunctorไปยังbifunctor มันดีพอที่จะทดสอบมันโดยการแยกออกเป็นที่ละargument แต่โดยทั่วไปแล้ว ความเป็นfunctorแบบแยกๆไม่สามารถที่จะพิสูจน์ความเป็นfunctorทั้งหมด categoryที่ไม่ความเป็นfunctorแบบทั้งหมด จะถูกเรียกว่าpremonoidal
เรามานิยามbifunctorในHaskell ในกรณีนี้ทั้งสามcategoryนั้นเหมือนกันนั้นก็คือcategoryของtypeในHaskell bifuntorคือconstructorของtypeที่นำสองargumentของtypeเข้ามา นี่คือนิยามของBifunctor
typeclassที่เอามาโดยตรงจากlibaryControl.Bifunctor
class Bifunctor f where
bimap :: (a -> c) -> (b -> d) -> f a b -> f c d
= first g . second h
bimap g h first :: (a -> c) -> f a b -> f c b
= bimap g id
first g second :: (b -> d) -> f a b -> f a d
= bimap id second
ต้วแปรของtypef
แทนbifunctionนี้ คุณสามารถที่จะเห็นได้ว่าในtype signatureต่างๆนี้ถูกนำไปใช้กับสองargumentของtype ในtype signatureแรกนิยามbimap
ที่คือการโยงสองfunctionในเวลาเดียวกัน ผลที่ตามมาคือfunctionที่ถูกliftf a -> f c d
ที่กระทำบนtypeที่ถูกสร้างโดยconstructorของbifunction type ได้มีการเขียนของbimap
ในรูปแบบของfirst
และsecond
(เป็นดั่งที่กล่าวก่อนหน้านี้ว่ามันไม่ได้ใช้ได้ตลอดเพราะว่าการโยงทั้งสองอาจจะไม่สามารถสลับที่ได้ นั่นก็คือfirst g . second h
อาจจะไม่เหมือนกับsecond h . first g
)
ในtype signatureทั้งสองที่เหลือfirst
และsecond
คือfmaps
ทั้งสองเป็นตัวแทนของความเป็นfunctorของf
ในargumentตัวแรกและตัวที่สองตามลำดับ


นิยามของtype classนี้ให้การเขียนมาอยู่แล้วของทั้งสอง(first
และsecond
)ในรูปแบบของbimap
ในตอนประกาศinstanceของBifunctor
คุณมีทางเลือกของการเขียนbimap
และยอบรับกับการเขียนที่ให้มาอยู่แล้วของสำหรับfirst
และsecond
หรือการเขียนทั้งfirst
และsecond
และยอมรับบการเขียนที่ให้มาอยู่แล้วของสำหรับbimap
(แน่นอนว่าคุณอาจจะเขียนทั้งสามตัวหมดแต่มันก็ขึ้นอยู่กับคุณที่จะทำให้แน่ใจว่าพวกมันมีความเกี่ยวข้องในแบบที่จำเป็น)
9.2 BifunctorแบบProductและCoproduct
ตัวอย่างที่ดีที่สุดของbifunctorคือproductแบบcategory (productของสองวัตถุที่ถูกนิยามโดยการสร้างแบบสากล) ถ้าproductนั้นมีอยู่สำหรับทุกๆคู่ของวัตถุ การโยงจากวัตถุเหล่านี้ไปยังproductนั้นก็มีความเป็นbifunctor มันเป็นจริงโดยทั่วไปและโดยเฉพาะHaskell ในที่นี้instanceBifunctor
สำหรับconstructorแบบpair ที่เป็นtypeแบบproductที่เรียบง่ายที่สุด
instance Bifunctor (,) where
= (f x, g y) bimap f g (x, y)
มันไม่มีตัวเลือกอื่นๆbimap
นั้นแค่นำfunctionแรกไปใช้กับส่วนแรกและfunctionที่สองไปใช้กับส่วนที่สอง โค้ดของสิ่งนี้เขียนตัวมันเองอยู่แล้วถ้าให้typesต่างๆกับมัน
bimap :: (a -> c) -> (b -> d) -> (a, b) -> (c, d)
การกระทำของbifunctorในที่นี้คือการทำpairของtypeตัวอย่างเช่น
= (a, b) (,) a b
โดยduality coproductถ้ามันสามารถถูกนิยามสำหรับทุกๆคู่ของวัตถุในcategoryก็คือbifucntorเช่นกัน ในHaskellนี้ถูกแสดงโดยconstructorของtypeEither
ซึ่งก็เป็นinstanceของBifunctor
instance Bifunctor Either where
Left x) = Left (f x)
bimap f _ (Right y) = Right (g y) bimap _ g (
โค้ดของสิ่งนี้ก็เขียนตัวมันเองเช่นกัน
ในตอนนี้เรายังจำตอนที่เราพูดเกี่ยวกับcategoryที่เป็นแบบmonoidได้หรือเปล่า? categoryแบบmonoidนิยามoperatorที่เป็นbinaryในการกระทำกับคู่ของวัตถุกับวัตถุที่เป็นunit ผมได้เอ่ยถึงว่า\(\textbf{Set}\)นั้นเป็นcategoryแบบmonoidเมื่อมีการคูณแบบCartesianคู่กับsetที่มีสมาชิกเดี่ยวที่มีฐานะเป็นunit สิ่งที่ผมไม่ได้กล่าวถึงคือว่าหนึ่งในเงื่อนไขสำหรับoperator binaryคือว่ามันต้องเป็นbifunctor นี่คือเงื่อนไขที่สำคัญ เราต้องการproductแบบmonoidให้เข้ากันได้กับโครงสร้างของcategoryที่ที่ถูกนิยามโดยmorphism เรานั้นเข้าใกล้ถึงนิยามเต็มของcategoryแบบmonoidไปอีกขั้น (เรายังต้องเราเกี่ยวกับnaturality(ความเป็นธรรมชาติ)ก่อนที่จะไปถึงจุดนั้น)
9.3 typeข้อมูลแบบพีชคณิตที่มีความเป็นfunctor (Functorial Algebraic Data Types)
เราได้เห็นตัวอย่างหลายๆตัวของtypeข้อมูลที่ถูกparameterizedที่กลายมาเป็นfunctor เราสามารถที่จะนิยามfmap
สำหรับมัน typeข้อมูลที่มีความชับช้อนนั้นถูกสร้างจากtypeข้อมูลที่ง่ายกว่า โดยเฉพาะtypeข้อมูลแบบพีชคณิต(algebraic data types (ADTs))ที่ถูกสร้างโดยการใช้sumและproduct เราได้เห็นมาแล้วว่าsumและproductนั้นมีความเป็นfunctor และเราก็รู้ว่าfunctorสามารถประกอบกันได้ ดังนั้นถ้าเราสามารถที่จะแสดงได้ว่าส่วนประกอบของADTsนั้นมีความเป็นfunctor เราก็จะรู้ได้ว่าADTที่ถูกparameterizedนั้นก็มีความเป็นfunctorเช่นกัน
ดังนั้นอะไรคือส่วนประกอบของADTsที่ถูกparameterized? อย่างแรกได้มีส่วนที่ไม่ขึ้นกับparameterของtypeของfunctorอย่าง Nothing
ในMaybe
หรือNil
ในList
พวกมันมีความเท่ากันกับfunctorConst
โดยที่functorConst
นั้นไม่สนใจparameterของtype (จริงๆแล้วparameterของtypeที่สองคือเป็นสิ่งที่เราสนใจ และตัวแรกก็ถูกเก็บไว้ให้คงที่)
ดังนั้นมันก็จะมีส่วนที่แค่ครอบ(encapsulate)parameterของtypeในมันเองเหมือนกับJust
ในMaybe
มันนั้นเท่ากันกับfunctorที่เป็นidentity ผมได้กล่าวถึงfunctorที่เป็นidentityไปก่อนหน้านี้ในฐานะmorphismที่เป็นidentityใน\(\textbf{Cat}\)แต่ไม่ได้ให้นิยามของมันในHaskellให้ชัดเจน นี่คือนิยามของมัน
data Identity a = Identity a
instance Functor Identity where
fmap f (Identity x) = Identity (f x)
คุณสามารถที่จะคิดถึงIdentity
ในฐานะcontainerที่เรียบง่ายที่สุดที่ก็จะเก็บแค่ค่า(ที่ไม่สามารถเปลี่ยนได้)ของtypea
ทุกๆอย่างที่่เหลือในdata structureแบบพีชคณิตนั้นสร้างมาจากพื้นฐานทั้งสองโดนการใช้productกับsum
ด้วยความรู้ใหม่นี้เรามามองในมุมมองใหม่ของconstructorของtypeMaybe
data Maybe a = Nothing | Just a
มันคือsumของtypeสองแบบและเราได้รู้ว่าsumนั้นมีความเป็นfunctorในส่วนแรกNothing
สามารถที่จะถูกแสดงในรูปแบบของConst ()
ที่กระทำบนa
(parameterของtypeแรกของConst
ที่ถูกกำหนดให้เป็นunit หลังจากนี้เราจะเห็นการใช้Const
ที่น่าสนใจ) ในส่วนที่สองก็คือแค่ชื่อที่ต่างไปของidentity functor เราอาจจะนิยามMaybe
จนถึงความisomorphismว่า
type Maybe a = Either (Const () a) (Identity a)
ดังนั้นMaybe
คือการประกอบกันของbifunctorEither
คู่กับสองfunctorอย่างConst ()
และIdentity
(Const
นั้นจริงๆแล้วเป็นbifunctorแต่ในที่นี้เราได้ใช้มันแค่บางส่วนตลอดเวลา)
เราได้เห็นแล้วว่าfunctorที่ประกอบกันคือfunctor เราสามารถที่จะโน้มน้าวเราเองง่ายๆว่าbifunctorก็เป็นเช่นเดียวกัน ทั้งหมดที่เราต้องมีคือการหาวิธีที่bifunctorที่ถูกประกอบคู่กับfunctorสองตัวจะทำงานบนmorphism ถ้ามีmorphismอยู่สองตัวเราแค่liftตัวหนึ่งด้วยfunctorตัวหนึ่งและliftอีกตัวด้วยfunctorอีกตัวหนึ่ง เราจึงliftคู่ของmorphismที่ถูกliftที่เป็นผลมาจากก่อนหน้านี้ด้วยbifunctor
เราสามารถที่จะแสดงการประกอบกันในHaskell เรามานิยามtypeของdataที่ถูกparameterizedโดยbifunctorbf
(มันคือตัวแปรแบบtypeที่ก็คือconstructorของtypeที่นำสองtypeมาในฐานะargument) functorทั้งสองอย่างfu
และgu
(constructorของtypeที่นำตัวแปรแบบtypeมาแต่ละอย่าง) และtypeทั่วๆไปa
และb
เราใช้fu
ไปกับa
และgu
ไปกับb
แล้วก็ใช้bf
กับผลลัพธ์ที่เป็นทั้งสองtype
newtype BiComp bf fu gu a b = BiComp (bf (fu a) (gu b))
นั่้นคือการประกอบกันของวัตถุหรือtype สังเกตว่าในHaskellเราใช้constructorของtypeกับtypeต่างๆ เหมือนกับการที่เราใช้functionไปกับargumentต่างๆ ที่มีsyntaxเหมือนกัน
ถ้าคุณสับสนเล็กน้อยลองใข้BiComp
ไปยังEither
,Const ()
,Identity
,a
และb
ในลำดับนี้ คุณจะได้อีกรูปที่พื้นฐานแบบหนึ่งของMaybe b
กลับมา(a
ก็ถูกเมินเฉย)
typeข้อมูลอันใหม่BiComp
คือbifunctorบนa
และb
แต่แค่bf
นั้นคือBifunctor
เองและfu
และgu
คือFunctor
complierต้องรู้ว่าได้มีนิยามของbimap
อยู่สำหรับbf
และนิยามของfmap
ในfu
และgu
ในHaskellนี่สามารถถูกแสดงในฐานะเงื่อนไขเบื้องต้นในการประกาศของinstance นั้นก็คือชุดของความต้องการของclass (class constraints)ตามด้วยลูกศรคู่
instance (Bifunctor bf, Functor fu, Functor gu) =>
Bifunctor (BiComp bf fu gu) where
BiComp x) = BiComp (bimap (fmap f1) (fmap f2) x) bimap f1 f2 (
ในการเขียนของbimap
สำหรับBiComp
ที่มาในรูปแบบของbimap
สำหรับbf
และfmaps
ทั้งสองสำหรับfu
และgu
complierนั้นอนุมานโดยอัตโนมัติสำหรับtypesทั้งหมดและเลือกfunctionที่ถูกoverloadได้อย่างถูกต้องในทุกๆตอนที่BiComp
ถูกใช้
x
ในนิยามของbimap
นั้นมีtypeเป็นแบบนี้
bf (fu a) (gu b)
ที่ที่ค่อนข้างมีขนาดใหญ่ bimap
รอบนอกทะลุผ่านชั้นของbf
และfmap
ทั้งสองที่อยู่ภายใต้fu
และgu
ตามลำดับ ถ้าtypeของf1
และf2
คือ
f1 :: a -> a'
f2 :: b -> b'
แล้วผลลัพธ์สุดท้ายของtype bf (fu a') (gu b')
คือ
bimap :: (fu a -> fu a') -> (gu b -> gu b')
-> bf (fu a) (gu b) -> bf (fu a') (gu b')
ถ้าคุณชอบปัญหาจิ๊กซอว์ การโยกย้ายtypeแบบนี้สามารถให้ความบันเทิงกับคุณเป็นชั่วโมงเลย
ดังนั้นมันกลับเป็นว่าเราไม่ต้องที่จะพิสูจน์ว่าMaybe
เป็นfunctor ความจริงนี้ตามมาจากวิธีการที่มันถูกสร้างในฐานะsumของพื้นฐานที่มีความเป็นfunctor
ผู้อ่านที่ช่างสังเกตอยากจะถามคำถามว่า ถ้าการคำนวณinstaceของFunctor
สำหรับtypeข้อมูลแบบพีชคณิตนั้นมีความเป็นกลไกอย่างมาก แล้าเราสามารถทำให้การทำแบบนี้เป็นอัตโนมัติและกระทำโดยcomplierได้หรือเปล่า แน่นอนว่ามันสามารถทำได้และมันทำอยู่ คุณต้องเปิดใช้งานextensionของHaskellบางตัวโดยการนำบรรทัดนี้เข้ามาข้างบนของไฟล์ของโค้ด(source file)
{-# LANGUAGE DeriveFunctor #-}
แล้วก็เพิ่มderiving Functor
ไปยังdata structureของคุณ
data Maybe a = Nothing | Just a deriving Functor
แล้วfmap
ที่สอดคล้องกันก็จะถูกเขียนให้คุณ
ความเป็นระเบียบของdata strctureแบบพีชคณิตทำให้มันเป็นไปได้ที่จะสร้างinstanceที่ไม่ใช่แค่สำหรับFunctor
แต่type classต่างๆอีกหลายตัวรวมไปถึงEq
type classที่ผมพูดถึงก่อนหน้านี้ มันได้มีทางเลือกในการสอนcomplierในการสร้างinstancesสำหรับtype classsของคุณแต่นั้นค่อนข้างขั้นสูงไปหน่อย แนวคิดจะเหมือนกันคือคุณให้พฤติกรรมสำหรับส่วนประกอบพื้นฐานและการsumและproductและให้complierคิดในส่วนที่เหลือ
9.4 FunctorในC++
ถ้าคุณเป็นคนที่เขียนC++แน่นอนว่าคุณนั้นอยู่ตัวคนเดียวสำหรับการเขียนfuntor แต่คุณควรที่สามารถที่จะมองเห็นtypeบางตัวของdata structureแบบพีชคณิตในC++ ถ้าdata structureแบบนั้นถูกสร้างให่้เป็นtemplateทั่วไป(generic template) คุณก็ควรที่จะสามารถเขียนfmap
สำหรับมัน
เรามาดูที่data structureแบบtreeที่เราต้องการที่จะนิยามในHaskellในฐานะtypeแบบsumและrecursive
data Tree a = Leaf a | Node (Tree a) (Tree a)
deriving Functor
เหมือนกับการที่ผมเอ่ยถึงก่อนหน้านี้ วิธีหนึ่งในการเขียนtypeแบบsumในC++ คือผ่านลำดับชั้นของclass มันอาจจะเป็นธรรมชาติในภาษาแบบobject-orientedในการเขียนfmap
ในฐานะfunctionแบบvirtualของclassพื้นฐานอย่างFunctor
และก็overrideมันในsubclassทั้งหมด น่าเสียดายว่าสิ่งนี้เป็นไปไม่ได้เพราะfmap
คือtemplate ที่parameterizedไม่ได้แค่โดยtypeของวัตถุที่มันกระทำต่อ (pointerของthis
)แต่ก็รวมไปถึงtypeที่returnของfunctionที่ถูกนำมาใช้กับมัน functionแบบvirtualไม่สามารถที่จะทำให้เป็นtemplateในC++ได้ เราจะเขียนfmap
ในฐานะgeneric free function และเราจะแทนที่การจับคู่รูปแบบด้วยdynamic_cast
classฐานต้องนิยามfunctionแบบvirtualหนี่งตัวเป็นอย่างน้อยเพื่อที่จะสนับสนุนการcastingแบบdynamics ดังนั้นเราจะทำdestructorเป็นvirtual (ที่ก็เป็นแนวคิดที่ดีในทุกๆกรณี)
template<class T>
struct Tree {
virtual ~Tree() {}
};
Left
ก็เป็นแค่functorIdentity
แบบแอบๆ
template<class T>
struct Leaf : public Tree<T> {
;
T _label(T l) : _label(l) {}
Leaf};
Node
เป็นtypeแบบproduct
template<class T>
struct Node : public Tree<T> {
<T> * _left;
Tree<T> * _right;
Tree(Tree<T> * l, Tree<T> * r) : _left(l), _right(r) {}
Node};
ในการเขียนfmap
เราได้ใช้การdispatchingแบบdynamicบนtypeของTree
ให้เป็นประโยชน์ ในกรณีของLeaf
ใช้รูปแบบIdentity
ของfmap
และในกรณีของfmap
ก็ถูกปฏิบัติเหมือนbifunctorที่ถูกประกอบคู่กับTree
functorสองตัว ในฐานะคนที่เขียนC++ คุณอาจจะไม่คุ้นเคยกับการวิเคราะห์โค้ดในแบบนี้แต่มันเป็นการฝึกที่ดีในการคิดแบบcategorical
template<class A, class B>
<B> * fmap(std::function<B(A)> f, Tree<A> * t) {
Tree<A> * pl = dynamic_cast <Leaf<A>*>(t);
Leafif (pl)
return new Leaf<B>(f (pl->_label));
<A> * pn = dynamic_cast<Node<A>*>(t);
Nodeif (pn)
return new Node<B>( fmap<A>(f, pn->_left)
, fmap<A>(f, pn->_right));
return nullptr;
}
เพิ่อความง่ายดาย ผมตัดสินใจที่จะไม่สนใจกับปัญหาของการจัดการmemoryและทรัพยากร แต่ในโด้ดที่ใช้ในproductionคุณอาจจะต้องใช้smart pointer(ที่เป็นเอกลักษณ์หรือใช้ร่วมกัน แล้วแต่นโยบายของคุณ)
นำมาเทียบกับการเขียนในHaskell
ของfmap
instance Functor Tree where
fmap f (Leaf a) = Leaf (f a)
fmap f (Node t t') = Node (fmap f t) (fmap f t')
การเขียนนี้ก็สามารถถูกสร้างโดยcomplierเช่นกัน
9.5 Writer Functor
ผมสัญญาว่าผมจะกลับมาที่categortyแบบkleisliที่ผมอธิบายก่อนหน้านี้ morphismในcategoryนั้นถูกแสดงโดยfunctionที่ผ่านการประดับแล้วที่return data structureแบบWriter
type Writer a = (a, String)
ผมได้พูดก่อนหน้านี้ว่าการประดับของfunctionนั้นมีความเกี่ยวข้องบางอย่างกับendofunctor และแน่นอนว่าconstructorของtypeอย่างWriter
นั้นมีความเป็นfunctorในa
เราไม่ต้องที่จะมีการเขียนของfmap
สำหรับมันเพราะว่ามันก็เป็นแค่typeแบบproduct
แต่อะไรคือความสัมพันธ์ระหว่างcategoryแบบKleisliและfunctorโดยทั่วไปละ? ในการที่categoryแบบKleisliเป็นcategory มันนั้นได้นิยามการประกอบกันและidentity ให้ผมได้เตือนความจำคุณว่าการประกอบกันนั้นถูกให้โดยfish operator
(>=>) :: (a -> Writer b) -> (b -> Writer c) -> (a -> Writer c)
>=> m2 = \x ->
m1 let (y, s1) = m1 x
= m2 y
(z, s2) in (z, s1 ++ s2)
และmorphismแบบidentityถูกให้โดยfunctionที่ถูกเรียกว่าreturn
return :: a -> Writer a
return x = (x, "")
มันกลายมาเป็นว่าถ้าคุณมองที่typeของfunctionทั้งสองมาพอ(และผมหมายถึงนานมากพอจริงๆ) คุณฏ้จะสามารถหาวิธีการในการรวมพวกมันเข้าเดียวกันในการสร้างfunctionที่มีtype signatureที่ถูกต้องเพื่อที่จะนำมาใช้ในฐานะfmap
ได้ดังต่อไปนี้
fmap f = id >=> (\x -> return (f x))
ในที่นี้fish operatorได้รวมสองfunction หนึ่งในนั้นคือid
ที่คุ้นเคยและอีกตัวหนึ่งคือlambdaที่ใช้return
ผลลัพธ์ของf
ที่ทำบนargumentของlambnda ส่วนที่ยากที่สุดที่จะทำความเข้าใจก็คงเป็นการใช้id
ไม่ไช่หรอที่argumentของfish operatorควรที่จะเป็นfunctionที่นำtypeแบบ”ธรรมดา”และreturn typeผ่านการประดับแล้ว? ก็ไม่ตลอก ไม่มีใครบอกว่าa
ในa -> Writer b
ต้องเป็นtypeแบบ”ธรรมดา” มันเป็นตัวแปรแบบtypeดังนั้นมันสามารถที่จะเป็นอะไรก็ได้ โดยเฉพาะที่มันสามารถที่จะเป็นtypeที่ผ่านการประดับแล้วอย่างWriter b
ดังนั้นid
จะรับWriter a
และแปลมันเป็นWriter a
fish operatorจะทำการดึงค่าของa
และส่งไปในฐานะx
ไปยังlambda ที่ในที่นี้คือf
จะกลายมาเป็นb
และกาreturn
จะประดับมันให้ได้มันมาเป็นWriter b
นำทั้งหมดเข้าด้วยกันเราก็จะได้functionที่นำWriter a
เขามาและส่งกลับมาเป็นWriter b
ในที่สุด ซึ่งเป็นสิ่งที่fmap
ควรที่จะสร้าง
สังเกตได้ว่าargumentนี้มีความทั่วไปเป็นอย่างมาก คุณสามมารถที่จะแทนที่Writer
กับconstructorของtypeแบบไหนก็ได้ ตราบเท่าที่มันรับรองfist operatorและreturn
คุณสามารถนิยามfmap
ได้เหมือนกัน ดังนั้นการประดับในcategoryแบบKleisliจึงเป็นfunctorโดยตลอด (แต่ไม่ไช่ทุกfunctorที่จะก่อให้เกิดเป็นcategoryแบบKleisi)
คุณอาจจะสงสัยว่าถ้าfmap
ที่เราพึ่งนิยามนั้นเหมือนกับfmap
ที่complierอาจจะสร้างผ่านderiving Functor
หรือเปล่า? น่าสนใจไม่น้อยที่มันเป็นแบบเดียวกัน นี่ก็เป็นเพราะวิธีการที่Haskellเขียนfunctionsแบบpolymorphic จะถูกเรียกว่าparametric polymorphism และมันเป็นที่มาของสิ่งที่เรียกว่า ทฤษฎีบทที่ได้มาแบบฟรีๆ(theorems for free) หนึ่งในทฤษฎีบทเหล่านี้พูดว่า ถ้าได้มีการเขียนของfmap
ที่เก็บรักษาidentityไว้สำหรับconstructorของtypeที่ให้มา แล้วนั้น ก็จะมีการเขียนเพียงแบบเดียว
9.6 FunctorแบบCovariantและContravariant
ในตอนนี้เราได้พิจารณาWriter Functorเรามากลับมาที่Reader Functor มันมาจากconstructorของtypeแบบลูกศรfunctionที่ถูกใช้บางส่วน
->) r (
เราสามารถที่จะเขียนใหม่ในtypeที่มีความหมายเดียวกัน
type Reader r a = r -> a
ที่instanceFunctor
ในที่ที่เราเห็นมาก่อนเขียนว่า
instance Functor (Reader r) where
fmap f g = f . g
แต่ก็เหมือนกับcostructorของtype pairหรือconstructorของtypeEither
constructorของtype functionนั้นนำargumentของtypeทั้งสองเข้ามา pairและEither
มีความเป็นfunctorในargumentทั้งสอง มันคือbifunctor แล้วconstructorของfunctionเป็นbifunctorด้วยหรือเปล่า?
เรามาลองที่จะทำมันให้มีความเป็นfunctorในargumentแรก เริ่มด้วยtypeที่มีความหมายเดียวกันมันแต่แค่Reader
แต่ถูกสลับargument
type Op r a = a -> r
ในตอนนี้เรายึดtypeที่returnr
ไว้แล้วเปลี่ยนtypeของargumenta
ไปมา เรามาดูว่าเราสามารถที่จะให้typeต่างๆนั้นลงตัว เพื่อที่จะสามารถเขียนfmap
ที่ควรจะเป็นไปตามsignatureของtypeดังต่อไปนี้
fmap :: (a -> b) -> (a -> r) -> (b -> r)
โดยมีfunctionสองตัวที่นำa
มาแล้วก็returnb
และr
กลับมาตามลำดับ แต่มันไม่มีวิธีไหนในการสร้างfunctionที่นำb
เข้ามาและreturnr
กลับมา แต่ผลอาจจะแตกต่างถ้าเราสามารถที่จะสลับ(invert)functionตัวแรกเพื่อที่ว่ามันนำb
เข้ามาและreturna
กลับมาแทน เราไม่สามารถสลับfunctionตามใจได้แต่เราสามารถไปยังcategoryตรงข้าม
สรุปอย่างคร่าวๆ สำหรับทุกๆcategory\(\textbf{C}\)ได้มีdual categoryอย่าง\(\textbf{C}^\text{op}\) ที่คือcategoryที่มีวัตถุเหมือนกับ\(\textbf{C}\)แต่ในทุกๆลูกศรอยู่ในทางตรงข้าม
ลองพิจารณาfunctorระหว่าง\(\textbf{C}^\text{op}\)และอีกcategory\(\textbf{D}\)ตัวหนึ่ง
\[ F::\textbf{C}^\text{op}\rightarrow\textbf{D} \]
functorแบบนี้โยงmorphism\(f^\text{op}::a\rightarrow b\)ใน\(\textbf{C}^\text{op}\) ไปยังmorphism\(f^\text{op}::Fa\rightarrow Fb\)ใน\(\textbf{D}\) แต่morphism\(f^\text{op}\)นั้นคู่กับmorphism\(f::b\rightarrow a\)อย่างลับๆในcategoryดังเดิม\(\textbf{C}\) โปรดสังเกตการกลับทาง
ในตอนนี้\(F\)เป็นfunctorธรรมดาแต่ได้มีการโยงที่เรานิยามโดยมีฐานมาจาก\(F\)ที่ไม่ใช่functor เรามาเรียกมันว่า\(G\)ที่โยงจาก\(\textbf{C}\)ไปยัง\(\textbf{D}\) มันโยงวัตถุในแบบเดียวกันกับ\(F\) แต่ในตอนที่มันโยงmorphismมันจะทำการกลับทางพวกมัน มันนำmorphism\(f::b\rightarrow b\)ใน\(\textbf{C}\)โยงไปเป็นmorphismตรงข้าม\(f^\text{op}::a\rightarrow b\) แล้วก็ใช้functor\(F\)กับมันเพื่อที่จะได้\(Ff^\text{op}:Fa\rightarrow Fb\)
ถ้าเราพิจารณาว่า\(Fa\)นั้นเหมือนกับ\(Ga\)และ\(Fb\)เหมือนกับ\(Gb\) ทั้งหมดสามารถที่จะถูกอธิบายอย่าง \(Gf::(b\rightarrow a)\rightarrow(Ga\rightarrow Gb)\) มันคือ”functorที่มีการแปลงเล็กน้อย” การโยงระหว่างcategoryต่างๆท่มีการกลับทิศทางของmorphismแบบนี้ จะถูกเรียกว่าfunctorแบบcontravariant สังเกตว่าfunctorแบบcontravariantนั้นก็เป็นแค่functorธรรมดาจากcategoryตรงข้าม อนึ่งfunctorธรรมดา(เป็นแบบที่เราได้ทำการศึกษาจนถึงจุดนี้) จะถูกเรียกว่าfunctorแบบcovariant
ในที่นี้typeclassที่นิยามfunctorแบบcontravariant (จริงๆก็คือendofunctorแบบcontravariant) ในHaskellที่ก็คือ
class Contravariant f where
contramap :: (b -> a) -> (f a -> f b)
construnctorของtypeOp
คือinstanceของมัน
instance Contravariant (Op r) where
-- (b -> a) -> Op r a -> Op r b
= g . f contramap f g
สังเกตว่าfunctionf
ใส่ตัวเองเข้าก่อน(นั่นก็คืออยู่ทางด้านขวาของ) เนื้อหาของOp
นั้นก็คือfunctiong
นิยามของcontramap
สำหรับOp
สามารถทำให้กระชับมากขึ้นถ้าคุณสังเกตว่ามันคือแค่operatorในการประกอบfunctionที่argumentถูกสลับ ได้มีfunctionพิเศษสำหรับการสลับargument เรียกว่าflip
flip :: (a -> b -> c) -> (b -> a -> c)
flip f y x = f x y
ในแบบนี้เราก็จะได้
= flip (.) contramap
9.7 Profunctors
เราได้เห็นมาแล้วว่าoperatorของลูกศรfunctionนั้นเป็นแบบcontravariantในargumentตัวแรก และเป็นแบบcovariantในในargumentตัวที่สอง ได้มีชื่อสำหรับสิ่ง(ที่ดูน่ากลัวนี้)นี้หรือเปล่า? กลับเป็นว่าถ้าcategoryปลายทางเป็น\(\textbf{Set}\)แล้วสิ่งที่ดูน่ากลัวนี้จะถูกเรียกว่าprofunctor เพราะว่าfunctorแบบcontravariantนั้นเท่ากับfunctorแบบcovariantจากcategoryตรงข้าม profunctorนั้นจะถูกนิยามว่า
\[ \textbf{C}^\text{op}\times\textbf{D}\rightarrow\textbf{Set} \]
เนื่องว่าในการประมาณคร่าวๆว่าtypeของHaskellคือset เราได้ใช้ชื่อของProfunctor
ไปยังconstructorสองargumentของtypep
ที่มีความเป็นcontra-functorในargumentแรกและความเป็นfunctorในargumentที่สอง นี่คือtypeclassที่ถูกต้องที่มาจากlibraryData.Profunctor
อย่าง
class Profunctor p where
dimap :: (a -> b) -> (c -> d) -> p b c -> p a d
= lmap f . rmap g
dimap f g lmap :: (a -> b) -> p b c -> p a c
= dimap f id
lmap f rmap :: (b -> c) -> p a b -> p a c
= dimap id rmap
functionทั้งสามมาคู่กับค่าเริ่มต้น เหมือนกับBifunctor
ในการประกาศinstanceของProfunctor
คุณต้องมีทางเลือกของการเขียนdimap
และทำการยอมรับค่าเริ่มต้นสำหรับlmap
และrmap
หรือเขียนทั้งlmap
และrmap
และยอมรับค่าเริ่มต้นสำหรับdimap
ในตอนนี้เราสามารถยืนยันว่าoperatorแบบลูกศรfunctionคือinstanceของProfunctor
instance Profunctor (->) where
= cd . bc . ab
dimap ab cd bc = flip (.)
lmap = (.) rmap
Profunctorถูกใช้ในlibaryของlensในHaskell เราจะเห็นพวกมันในตอนที่เราพูดเกี่ยวกับendsและcoends
9.8 FunctorของHom
ในตัวอย่างข้างบนคือภาพสะท้อนของstatementที่ว่าการโยง ที่นำคู่ของวัตถุ\(a\)และ\(b\)และกำหนดsetของmorphismระหว่างมัน(hom-set\(\textbf{C}(a,b)\)) คือfunctor มันคือfunctorจากcategoryแบบproduct\(\textbf{C}^\text{op}\times\textbf{C}\)ไปยังcategoryของsetอย่าง\(\textbf{Set}\)
เรามานิยามการกระทำของมันบนmorphism morphismใน\(\textbf{C}^\text{op}\times\textbf{C}\) คือคู่ของmorphismจาก\(\textbf{C}\)
\[ \begin{gather*} f :: a' \to a \\ g :: b \to b' \end{gather*} \]
การliftของคู่นี้ต้องเป็นmorphism(function)จากset \(\textbf{C}(a,b)\)ไปยัง\(\textbf{C}(a',b')\) เราแค่ต้องเลือกสมาชิก\(h\)ของ\(\textbf{C}(a,b)\) (มันคือmorphismจาก\(a\)ไปยัง\(b\)) และกำหนดมันไปยัง
\[ g\circ h\circ f \]
ที่ก็คือสมาชิกของ\(\textbf{C}(a',b')\)
คุณได้เห็นแล้วว่าhom-functorเป็นกรณีพิเศษของprofunctor
9.9 โจทย์ท้าทาย
- ลองแสดงดูว่าtypeของข้อมูล
data Pair a b = Pair a b
คือbifunctor สำหรับคะแนนเพิ่มให้ลองเขียนทั้งสามmethod ของBifunctor
และใช้การให้เหตุผลทางสมการ ในการแสดงว่าการนิยามพวกนี้นั้นเข้ากันได้กลับค่าเริ่มต้นในทุกๆตอนที่มันถูกใช้
- ลองแสดงisomorphismระหว่างนิยามมาตรฐานของ
Maybe
และนิยามที่ตรงไปตรงมาอย่าง(desugaring)
type Maybe' a = Either (Const () a) (Identity a)
คำใบ้:นิยามการโยงสองตัวระหว่างการเขียนทั้งสอง สำหรับคะแนนเพิ่มลองแสดงว่าพวกมันคือinverseระหว่างกันโดยการใช้เหตุผลทางสมการ
- เรามาลองdata structureอีกอัน เราเรียกมันว่า
PreList
เพราะว่ามันคือจุดเริ่มของList
มันแทนที่recursionด้วยparameterแบบtypeb
data PreList a b = Nil | Cons a b
เราอาจจะกู้นิยามก่อนหน้านี้ของList
โดยการใช้งานPreList
ไปกับตัวเอง (เราจะเห็นวิธีการในการทำมันในตอนที่เราพูดเกี่ยวกับfixed points) และลองแสดงว่าPreList
คือinstanceของBifunctor
- แสดงว่าtypeแบบข้อมูลนิยามbifunctorใน
a
และb
data K2 c a b = K2 c
data Fst a b = Fst a
data Snd a b = Snd b
สำหรับคะแนนเพิ่มลองตรวจสอบคำตอบของคุณกับpaperของConor McBrideอย่างClowns to the Left of me, Jokers to the Right(ตัวตลกอยู่ด้านช้าย โจ๊กเกอร์อยู่ด้านชวา)1
นิยามbifunctorในภาษาที่มีมากกว่าHaskell ลองเขียน
bimap
สำหรับpairแบบทั่วไปในภาษานั้นstd::map
ควรที่จะถูกพิจารณาเป็นbifunctorหรือprofunctor ในargumentของtemplateKey
และT
? แล้วคุณจะทำการออกแบบtypeแบบข้อมูลใหม่ที่จะทำให้เป็นแบบนั้นหรือเปล่า?