2015-03-03 62 views
4

我想了解fmap fmap如何適用於像say這樣的函數。fmap fmap如何應用於函數(作爲參數)?

類型的fmap fmap

(fmap fmap):: (Functor f1, Functor f) => f (a -> b) -> f (f1 a -> f1 b) 

(*3)類型:

(*3) :: Num a => a -> a 

這意味着簽名a -> a對應f (a -> b),對不對?

Prelude> :t (fmap fmap (*3)) 
(fmap fmap (*3)):: (Num (a -> b), Functor f) => (a -> b) -> f a -> f b 

我試圖創建一個簡單的測試:

test :: (Functor f) => f (a -> b) -> Bool 
test f = True 

和餵養(*3)進去,但我得到:

*Main> :t (test (*3)) 

<interactive>:1:8: 
    No instance for (Num (a0 -> b0)) arising from a use of ‘*’ 
    In the first argument of ‘test’, namely ‘(* 3)’ 
    In the expression: (test (* 3)) 

這是爲什麼發生?

+3

'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

+0

請注意,'fmap fmap fmap'等同於'fmap。因爲外部仿函數被強制爲'( - >)(a - > b)'(這就是爲什麼要求fmap fmap fmap'只在其約束中指定兩個仿函數的原因)。 – 2015-03-03 07:51:11

+0

但爲什麼'(fmap fmap(* 3))'typecheck?我想我只是用ghci差異來處理兩個函數的參數是相同類型的('(fmap fmap(* 3))'和'test(* 3)') – egdmitry 2015-03-03 08:18:30

回答

3

當你不知道自己在做什麼時,多形性是危險的。 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」分別表示「輸入」和「輸出」。無論如何,讓我們看看會發生什麼,當我們申請fmapfmap

fmap :: Functor f => (a -> b) -> f a -> f b 
        |______| 
         | 
         fmap :: Functor g => (x -> y) -> g x -> g y 
              |______| |________| 
               |   | 
               a ->  b 

正如你所看到的,a := x -> yb := 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的上下文中。假設fMaybe。因此,在專業:

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 fmapNothing然後我們得到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(即函數)。函數是一個有效的函子。 (->) rfmap函數只是函數組合。因此,中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 

你明白爲什麼這是愚蠢和危險的?

  1. ,因爲你申請其預計輸入功能有兩個參數(r -> x -> y)的函數只有一個參數,(*3) :: Num a => a -> a功能這是愚蠢的。
  2. 這很危險,因爲(*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)必須立即解決。如果ab出現在輸出類型中,那麼它們可以是專用的,並且可以選擇不同的Num (a -> b)實例。但是,因爲它們不出現在輸出類型中,編譯器必須決定立即選擇哪個實例Num (a -> b);並且因爲Num (a -> b)是一個愚蠢的約束,它沒有任何實例,編譯器會引發錯誤。

如果您嘗試test (*),那麼您將不會收到任何錯誤,這與上面提到的相同。

+0

偉大的圖表! – luqui 2015-08-24 23:17:30