[英]Composing function composition: How does (.).(.) work?
(.)
接受兩個函數,它們接受一個值並返回一個值:
(.) :: (b -> c) -> (a -> b) -> a -> c
因為(.)
有兩個參數,我覺得(.).(.)
應該是無效的,但它完全沒問題:
(.).(.) :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c
這里發生了什么? 我意識到這個問題措辭嚴厲......所有的功能都只是因為討論而采取了一個論點。 也許更好的方式來說它是類型不匹配。
讓我們首先使用typechecker進行機械校樣。 我將描述一種直觀的思考方式。
我想將(.)
應用於(.)
,然后我將(.)
應用於結果。 第一個應用程序幫助我們定義一些變量的等價物。
((.) :: (b -> c) -> (a -> b) -> a -> c)
((.) :: (b' -> c') -> (a' -> b') -> a' -> c')
((.) :: (b'' -> c'') -> (a'' -> b'') -> a'' -> c'')
let b = (b' -> c')
c = (a' -> b') -> a' -> c'
((.) (.) :: (a -> b) -> a -> c)
((.) :: (b'' -> c'') -> (a'' -> b'') -> a'' -> c'')
然后我們開始第二次,但很快陷入困境......
let a = (b'' -> c'')
這是關鍵:我們想讓let b = (a'' -> b'') -> a'' -> c''
,但我們已經定義了b
,所以我們必須嘗試統一 ---以匹配我們最好的兩個定義。 幸運的是,他們確實匹配
UNIFY b = (b' -> c') =:= (a'' -> b'') -> a'' -> c''
which implies
b' = a'' -> b''
c' = a'' -> c''
通過這些定義/統一,我們可以繼續申請
((.) (.) (.) :: (b'' -> c'') -> (a' -> b') -> (a' -> c'))
然后擴大
((.) (.) (.) :: (b'' -> c'') -> (a' -> a'' -> b'') -> (a' -> a'' -> c''))
並清理它
substitute b'' -> b
c'' -> c
a' -> a
a'' -> a1
(.).(.) :: (b -> c) -> (a -> a1 -> b) -> (a -> a1 -> c)
說實話,這有點違反直覺。
這是直覺。 首先來看看fmap
fmap :: (a -> b) -> (f a -> f b)
它將一個功能“提升”為一個Functor
。 我們可以反復應用它
fmap.fmap.fmap :: (Functor f, Functor g, Functor h)
=> (a -> b) -> (f (g (h a)) -> f (g (h b)))
允許我們將函數提升到更深層次的Functors
層。
事實證明,數據類型(r ->)
是一個Functor
。
instance Functor ((->) r) where
fmap = (.)
這應該看起來很熟悉。 這意味着fmap.fmap
轉換為(.).(.)
。 因此, (.).(.)
只是讓我們轉換(r ->)
Functor
的更深層次的參數類型。 (r ->)
Functor
實際上是Reader
Monad
,因此分層Reader
就像擁有多種獨立的全局不可變狀態。
或者喜歡有多個不受fmap
影響的輸入參數。 類似於在(> 1)arity函數的“只是結果”上組成新的延續函數。
讓我們暫時忽略類型,只使用lambda演算。
Desugar中綴符號:
(.) (.) (.)
ETA-擴大:
(\\ ab -> (.) ab) (\\ cd -> (.) cd) (\\ ef -> (.) ef)
內聯(.)
的定義:
(\\ abx -> a (bx)) (\\ cdy -> c (dy)) (\\ efz -> e (fz))
替換a
:
(\\ bx -> (\\ cdy -> c (dy)) (bx)) (\\ efz -> e (fz))
替代品b
:
(\\ x -> (\\ cdy -> c (dy)) ((\\ efz -> e (fz)) x))
替代e
:
(\\ x -> (\\ cdy -> c (dy)) (\\ fz -> x (fz)))
替代c
:
(\\ x -> (\\ dy -> (\\ fz -> x (fz)) (dy)))
替代f
:
(\\ x -> (\\ dy -> (\\ z -> x (dyz))))
Resugar lambda表示法:
\\ xdyz -> x (dyz)
如果你問GHCi,你會發現它有預期的類型。 為什么? 因為函數箭頭是右關聯的以支持currying:類型(b -> c) -> (a -> b) -> a -> c
實際上意味着(b -> c) -> ((a -> b) -> (a -> c))
。 同時,類型變量b
可以代表任何類型, 包括函數類型 。 看到連接?
以下是同一現象的一個更簡單的例子:
id :: a -> a
id x = x
id的類型表示id應該采用一個參數。 事實上,我們可以用一個參數來稱呼它:
> id "hello"
"hello"
但事實證明我們也可以用兩個參數來稱呼它:
> id not True
False
甚至:
> id id "hello"
"hello"
到底是怎么回事? 理解id not True
的關鍵是先看看id not
。 顯然,這是允許的,因為它將id應用於一個參數。 not
Bool -> Bool
的類型Bool -> Bool
,所以我們知道來自id類型的a
應該是Bool -> Bool
,所以我們知道這種id的出現有類型:
id :: (Bool -> Bool) -> (Bool -> Bool)
或者,括號較少:
id :: (Bool -> Bool) -> Bool -> Bool
所以這種id的出現實際上需要兩個參數 。
同樣的推理也適用於id id "hello"
和(.) . (.)
(.) . (.)
。
這是一個巧妙的案例,我認為首先掌握更一般的案例更簡單,然后考慮具體案例。 那么讓我們考慮一下仿函數。 我們知道仿函數提供了一種在結構上映射函數的方法 -
class Functor f where
fmap :: (a -> b) -> f a -> f b
但是如果我們有兩層仿函數呢? 例如,列表列表? 在這種情況下,我們可以使用兩層fmap
>>> let xs = [[1,2,3], [4,5,6]]
>>> fmap (fmap (+10)) xs
[[11,12,13],[14,15,16]]
但是這種模式f (gx)
是完全一樣的(f . g) x
,所以我們可以寫
>>> (fmap . fmap) (+10) xs
[[11,12,13],[14,15,16]]
什么是fmap . fmap
的類型fmap . fmap
fmap . fmap
?
>>> :t fmap.fmap
:: (Functor g, Functor f) => (a -> b) -> f (g a) -> f (g b)
我們看到它根據我們的需要映射了兩層仿函數。 但是現在記住(->) r
是一個仿函數(來自r
的函數類型,您可能更喜歡將其讀作(r ->)
)並且它的仿函數實例是
instance Functor ((->) r) where
fmap f g = f . g
對於一個函數, fmap
只是函數組合! 當我們fmap
兩個fmap
我們映射函數functor的兩個級別。 我們最初有一些類型(->) s ((->) ra)
,這相當於s -> r -> a
,我們最終得到類型為s -> r -> b
,所以類型(.).(.)
必須是
(.).(.) :: (a -> b) -> (s -> r -> a) -> (s -> r -> b)
它接受第一個函數,並用它來轉換第二個(雙參數)函數的輸出。 例如,函數((.).(.)) show (+)
是兩個參數的函數,首先將其參數加在一起,然后使用show
將結果轉換為String
:
>>> ((.).(.)) show (+) 11 22
"33"
例如,考慮更長的fmap
鏈fmap
了自然的概括
fmap.fmap.fmap ::
(Functor f, Functor g, Functor h) => (a -> b) -> f (g (h a)) -> f (g (h b))
它映射三層仿函數,相當於用三個參數的函數組合:
(.).(.).(.) :: (a -> b) -> (r -> s -> t -> a) -> (r -> s -> t -> b)
例如
>>> import Data.Map
>>> ((.).(.).(.)) show insert 1 True empty
"fromList [(1,True)]"
它將值True
插入到帶有鍵1
的空映射中,然后將輸出轉換為帶有show
的字符串。
這些函數通常很有用,因此您有時會將它們定義為
(.:) :: (a -> b) -> (r -> s -> a) -> (r -> s -> b)
(.:) = (.).(.)
這樣你就可以寫了
>>> let f = show .: (+)
>>> f 10 20
"30"
當然,可以給出一個更簡單,有點的(.:)
定義
(.:) :: (a -> b) -> (r -> s -> a) -> (r -> s -> b)
(f .: g) x y = f (g x y)
這可能有助於神秘化(.).(.)
。
你是對的, (.)
只有兩個參數。 你似乎只對haskell的語法感到困惑。 在表達式(.).(.)
,它實際上是中間的點,它將另外兩個點作為參數,就像在表達式100 + 200
,可以寫成(+) 100 200
。
(.).(.) === (number the dots)
(1.)2.(3.) === (rewrite using just syntax rules)
(2.)(1.)(3.) === (unnumber and put spaces)
(.) (.) (.) ===
並且應該更清楚地從(.) (.) (.)
得到第一個(.)
將第二個(.)
和第三個(.)
作為它的參數。
是的,這是由於currying。 (.)
因為Haskell中的所有函數只接受一個參數。 你正在編寫的是對每個相應的組合(.)
的第一次部分調用,它采用了它的第一個參數(組合的第一個函數)。
想象一下,你有一個簡單的功能:它加起來2個數字然后否定結果。 我們稱之為foo
:
foo a b = negate (a + b)
現在讓我們一步一步地讓它無點,看看我們最終得到了什么:
foo a b = negate $ a + b
foo a b = negate $ (+) a b
foo a b = negate $ (+) a $ b
foo a b = negate . (+) a $ b
foo a = negate . (+) a -- f x = g x is equivalent to f = g
foo a = (.) negate ((+) a) -- any infix operator is just a function
foo a = (negate.) ((+) a) -- (2+) is the same as ((+) 2)
foo a = (negate.) $ (+) a
foo a = (negate.) . (+) $ a
foo = (negate.) . (+)
foo = ((.) negate) . (+)
foo = (.) ((.) negate) (+) -- move dot in the middle in prefix position
foo = ((.) ((.) negate)) (+) -- add extra parentheses
現在讓我們更仔細地分析表達式(.) ((.) negate)
。 它是(.)
函數的部分應用,其第一個參數是((.) negate)
。 我們可以進一步改造它嗎? 我們可以:
(.) ((.) negate)
(.) . (.) $ negate -- because f (f x) is the same as (f . f) x
(.)(.)(.) $ negate
((.)(.)(.)) negate
(.).(.)
相當於(.)(.)(.)
,因為在第1個表達式中,中間的點可以在前綴位置移動並用括號括起來,這就產生了第二個表達式。
現在我們可以重寫我們的foo
函數:
foo = ((.).(.)) negate (+)
foo = ((.)(.)(.)) negate (+) -- same as previous one
foo = negate .: (+)
where (.:) = (.).(.)
現在你知道(.).(.)
相當於(\\fgxy -> f (gxy))
:
(\f g x y -> f (g x y)) negate (+) 2 3 -- returns -5
((.).(.)) negate (+) 2 3 -- returns -5
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.