假設你想定義一個函數來計算右三角形的hypoteneuse的平方。以下定義中的任何一個都是有效的
hyp1 a b = a * a + b * b
hyp2(a,b) = a * a + b * b
但是,它們不是相同的功能!您可以通過在GHCI
看着自己的類型
>> :type hyp1
hyp1 :: Num a => a -> a -> a
>> :type hyp2
hyp2 :: Num a => (a, a) -> a
以hyp2
第一(現在忽略Num a =>
一部分)的類型告訴你的函數取一對(a, a)
並返回另一個a
(例如,它可能需要告訴一對整數並返回另一個整數或一對實數並返回另一個實數)。你這樣使用它
>> hyp2 (3,4)
25
請注意,括號在這裏不是可選的!他們確保論證是正確的類型,一對a
s。如果你不包含它們,你會得到一個錯誤(現在看起來可能會讓你感到困惑,但請放心,當你瞭解了類型類時它會有意義的)。
現在看hyp1
讀取類型a -> a -> a
的一種方法是需要兩件東西a
並返回a
類型的其他東西。您可以使用它像這樣
>> hyp1 3 4
25
現在你會得到一個錯誤,如果你做包括括號!
所以首先要注意的是,你使用函數的方式必須與你定義它的方式相匹配。如果用parens定義函數,則每次調用它時都必須使用parens。如果在定義函數時不使用parens,則在調用它時不能使用它們。
因此,似乎沒有理由選擇其中一個 - 這只是一個品味問題。但實際上,我認爲是是一個優先於另一個的好理由,並且您應該更喜歡沒有括號的樣式。有三個很好的理由:
它看起來更乾淨,讓您的代碼更容易閱讀,如果你沒有parens凌亂的頁面。
如果你在任何地方都使用parens,那麼你會受到性能影響,因爲每次使用該函數時都需要構造和解構一對(儘管編譯器可能會優化它 - 我不確定)。
你想要得到的好處討好,又名部分應用功能 *。
最後一點是微妙的。回想一下,我說過一種理解a -> a -> a
類型函數的方法是它需要兩件事a
類型,並返回另一個a
。但是還有另一種方法來讀取該類型,即a -> (a -> a)
。這意味着完全一樣的東西,因爲->
運算符在Haskell中是右關聯的。解釋是該函數只需要一個a
,並返回a -> a
類型的函數。這可以讓你只需要提供的第一個參數的功能,並應用第二個參數後,例如
>> let f = hyp1 3
>> f 4
25
這是在各種各樣的場合實用價值。例如,map
功能可讓您將一些功能列表中的每個元素 -
>> :type map
map :: (a -> b) -> [a] -> [b]
假設你有增加了一個爆炸任何String
功能(++ "!")
。但是你有Strings
的列表,你希望它們都以爆炸聲結束。沒問題!你只是部分應用map
功能
>> let bang = map (++ "!")
現在bang
是類型的函數**
>> :type bang
bang :: [String] -> [String]
,你可以用它像這樣
>> bang ["Ready", "Set", "Go"]
["Ready!", "Set!", "Go!"]
非常有用的!
我希望我已經說服你,在你的學校的教育材料中使用的慣例有一些相當堅實的理由被使用。作爲擁有數學背景的人,我可以看到使用更多「傳統」語法的吸引力,但我希望隨着您在編程之旅中的進步,您將能夠看到改變最初的某些東西的優勢對你不熟悉。
*對於書呆子的說明 - 我知道咖喱和部分應用並不完全一樣。
**實際上,GHCI會告訴您類型爲bang :: [[Char]] -> [[Char]]
,但由於String
是[Char]
的同義詞,所以這些意思是相同的。
在你的第一個筆記上:'不完全相同的東西'暗示着某種關係,而實際上沒有關係。 Currying只允許輕鬆部分應用,但我認爲原則上它們是非常不相關的。 – Xeo
@Xeo他們有些相關。 curried/tupled表單形成一個[adjunction](http://en.wikipedia.org/wiki/Adjoint_functors),其中F是'( - x B)',G是' -^B'。 – jozefg
@Xeo我想這是一個味道點。對於一個函數'(a,b,c) - > d',currying它給出'a - > b - > c - > d'而部分應用第一個參數給出a - >(b,c) - > D'。對於兩個參數的函數,currying和部分應用是相同的事情。在Haskell中,幾乎沒有區別,因爲默認情況下是currying(這是我希望避免用我的腳註做出的評論!) –