當你不知道自己在做什麼時,多形性是危險的。 fmap
和(*)
都是多態函數,並且盲目使用它們會導致非常混亂(並且可能不正確)的代碼。我之前已經回答過類似的問題:
What is happening when I compose * with + in Haskell?
在這種情況下,我認爲,看類型的值可以幫助你找出你要去哪裏出錯以及如何解決問題。讓我們先從fmap
類型簽名:
fmap :: Functor f => (a -> b) -> f a -> f b
|______| |________|
| |
domain codomain
的fmap
類型簽名是很容易理解。它將函數從a
提升到b
到函子的上下文中,無論函子是什麼(例如,列表,也許,等等)。
「domain」和「codomain」分別表示「輸入」和「輸出」。無論如何,讓我們看看會發生什麼,當我們申請fmap
到fmap
:
fmap :: Functor f => (a -> b) -> f a -> f b
|______|
|
fmap :: Functor g => (x -> y) -> g x -> g y
|______| |________|
| |
a -> b
正如你所看到的,a := x -> y
和b := g x -> g y
。另外,還添加了約束條件Functor g
。這給我們的fmap fmap
類型簽名:
fmap fmap :: (Functor f, Functor g) => f (x -> y) -> f (g x -> g y)
那麼,是什麼fmap fmap
辦?第一個fmap
已將第二個fmap
提升到函子f
的上下文中。假設f
是Maybe
。因此,在專業:
fmap fmap :: Functor g => Maybe (x -> y) -> Maybe (g x -> g y)
因此fmap fmap
必須施加到一個Maybe
值與它裏面的功能。 fmap fmap
的作用是將Maybe
值內的函數提升到另一個函子g
的上下文中。假設g
是[]
。因此,對專業:
fmap fmap :: Maybe (x -> y) -> Maybe ([x] -> [y])
如果我們將fmap fmap
到Nothing
然後我們得到Nothing
。但是,如果我們將它應用於Just (+1)
,那麼我們得到一個函數,該函數遞增列表中的每個數字,並將其包裝在一個Just
構造函數中(即我們獲得Just (fmap (+1))
)。
但是,fmap fmap
更一般。實際上,它看起來像一個函數f
(無論f
可能是什麼),並將f
中的函數提升到另一個函子g
的上下文中。
到目前爲止這麼好。所以有什麼問題?問題是當您將fmap fmap
應用於(*3)
時。這是愚蠢而危險的,就像酒後駕車一樣。讓我告訴你爲什麼這是愚蠢和危險的。看看的(*3)
類型簽名:
(*3) :: Num a => a -> a
當您申請fmap fmap
到(*3)
則函子f
是專門到(->) r
(即函數)。函數是一個有效的函子。 (->) r
的fmap
函數只是函數組合。因此,中fmap fmap
對專業類型是:
fmap fmap :: Functor g => (r -> x -> y) -> r -> g x -> g y
-- or
(.) fmap :: Functor g => (r -> x -> y) -> r -> g x -> g y
|___________|
|
(*3) :: Num a => a -> a
| |
| ------
| | |
r -> x -> y
你明白爲什麼這是愚蠢和危險的?
- ,因爲你申請其預計輸入功能有兩個參數(
r -> x -> y
)的函數只有一個參數,(*3) :: Num a => a -> a
功能這是愚蠢的。
- 這很危險,因爲
(*3)
的輸出是多態的。因此,編譯器不會告訴你,你正在做一些愚蠢的事情。幸運的是,由於輸出是有界的,你會得到一個類型約束Num (x -> y)
,這應該表明你在某處出錯了。
計算類型r := a := x -> y
。因此,我們可以得到以下類型簽名:
fmap . (*3) :: (Num (x -> y), Functor g) => (x -> y) -> g x -> g y
讓我告訴你爲什麼這是錯的使用值:
fmap . (*3)
= \x -> fmap (x * 3)
|_____|
|
+--> You are trying to lift a number into the context of a functor!
你真正想要做的是應用fmap fmap
於(*)
,這是一個二元函數:
(.) fmap :: Functor g => (r -> x -> y) -> r -> g x -> g y
|___________|
|
(*) :: Num a => a -> a -> a
| | |
r -> x -> y
因此,r := x := y := a
。這使您的類型簽名:
fmap . (*)
= \x -> fmap (x *)
因此,fmap fmap (*) 3
簡直是fmap (3*)
:
fmap . (*) :: (Num a, Functor g) => a -> g a -> g a
當你看到這個值就更有道理了。
最後,你有你的test
功能相同的問題:
test :: Functor f => f (a -> b) -> Bool
在專門仿函數f
到(->) r
我們得到:
test :: (r -> a -> b) -> Bool
|___________|
|
(*3) :: Num x => x -> x
| |
| ------
| | |
r -> a -> b
因此,r := x := a -> b
。因此,我們得到的類型簽名:
test (*3) :: Num (a -> b) => Bool
由於既不a
也不b
出現在輸出類型,約束Num (a -> b)
必須立即解決。如果a
或b
出現在輸出類型中,那麼它們可以是專用的,並且可以選擇不同的Num (a -> b)
實例。但是,因爲它們不出現在輸出類型中,編譯器必須決定立即選擇哪個實例Num (a -> b)
;並且因爲Num (a -> b)
是一個愚蠢的約束,它沒有任何實例,編譯器會引發錯誤。
如果您嘗試test (*)
,那麼您將不會收到任何錯誤,這與上面提到的相同。
'Num a => a - > a'和'f(x - > y)'不能很好地對齊,最後會出現'f〜( - >)a'和'a〜x - > y' ,因此'Num(x - > y)'。更有趣的可能是'fmap($ [1,2,3])$ fmap fmap $ fmap(+)$只是10',它返回'Just [11,12,13]'。一個更有用的'Functor'組合器可能是'fmap fmap fmap',它可以讓你通過兩個不同的'Functor'列出一個函數:'(。:) = fmap fmap fmap'; '(10 *)。:[只有1,沒有,只有3] == [只有10,沒有,只有30]' – bheklilr 2015-03-03 06:59:59
請注意,'fmap fmap fmap'等同於'fmap。因爲外部仿函數被強制爲'( - >)(a - > b)'(這就是爲什麼要求fmap fmap fmap'只在其約束中指定兩個仿函數的原因)。 – 2015-03-03 07:51:11
但爲什麼'(fmap fmap(* 3))'typecheck?我想我只是用ghci差異來處理兩個函數的參數是相同類型的('(fmap fmap(* 3))'和'test(* 3)') – egdmitry 2015-03-03 08:18:30