2010-11-16 66 views
53

我試圖學習箭頭的含義,但我不明白他們。什麼是箭頭,我該如何使用它們?

我使用了Wikibooks教程。我認爲維基百科的問題主要在於它似乎是爲那些已經理解這個主題的人寫的。

有人可以解釋箭頭是什麼以及我如何使用它們?

+6

相關:http://stackoverflow.com/questions/3154701/help-understanding-arrows-in-haskell – kennytm 2010-11-16 07:07:19

+0

@KennyTM:你的回答幾乎解釋了我想知道的大部分事情。現在我明白了一些原因。 – fuz 2010-11-17 06:35:57

+0

@FUZxxl我最近也想認真地摟住箭,並且遇到了同樣的挫折感。你有沒有一份你認爲對你的任務最有幫助的清單? – kizzx2 2011-08-27 13:51:04

回答

60

我不知道教程,但我認爲如果你看一些具體的例子,理解箭頭是最容易的。我學習如何使用箭頭的最大問題是沒有任何教程或示例實際上展示如何使用箭頭,以及如何組合它們。所以,考慮到這一點,這是我的迷你教程。我將檢查兩個不同的箭頭:函數和用戶定義的箭頭類型MyArr

-- type representing a computation 
data MyArr b c = MyArr (b -> (c,MyArr b c)) 

1)箭頭是從指定類型的輸入到指定類型的輸出的計算。箭頭類型類型有三個類型參數:箭頭類型,輸入類型和輸出類型。縱觀實例頭箭頭情況下,我們發現:

instance Arrow (->) b c where 
instance Arrow MyArr b c where 

箭(無論是(->)MyArr)是一個計算的抽象。

對於函數b -> c,b是輸入,而c是輸出。
對於MyArr b c,b是輸入,而c是輸出。

2)要實際運行箭頭計算,您需要使用特定於箭頭類型的函數。對於函數,只需將該函數應用於參數。對於其他箭頭,需要有一個單獨的功能(就像單個的runIdentity,runState等一樣)。

-- run a function arrow 
runF :: (b -> c) -> b -> c 
runF = id 

-- run a MyArr arrow, discarding the remaining computation 
runMyArr :: MyArr b c -> b -> c 
runMyArr (MyArr step) = fst . step 

3)箭頭通常用於處理輸入列表。對於這些功能可以並行進行,但是對於某些步驟中的某些箭頭輸出取決於先前的輸入(例如,保持輸入的總數)。

-- run a function arrow over multiple inputs 
runFList :: (b -> c) -> [b] -> [c] 
runFList f = map f 

-- run a MyArr over multiple inputs. 
-- Each step of the computation gives the next step to use 
runMyArrList :: MyArr b c -> [b] -> [c] 
runMyArrList _ [] = [] 
runMyArrList (MyArr step) (b:bs) = let (this, step') = step b 
            in this : runMyArrList step' bs 

這是箭頭有用的原因之一。他們提供了一個計算模型,可以隱式地使用狀態,而不會將該狀態暴露給程序員。程序員可以使用箭頭化的計算並將它們組合起來以創建複雜的系統。

這裏有一個myArr,該是保持輸入數量的計數已收到:

-- count the number of inputs received: 
count :: MyArr b Int 
count = count' 0 
    where 
    count' n = MyArr (\_ -> (n+1, count' (n+1))) 

現在的功能runMyArrList count將採取列表長度爲n作爲輸入,並從1返回INTS的列表爲n。

請注意,我們仍然沒有使用任何「箭頭」函數,即Arrow類方法或用它們編寫的函數。

4)上面的大部分代碼都是針對每個Arrow實例的[1]。在Control.Arrow(和Control.Category)中的一切都是關於組成箭頭來製作新箭頭。如果我們假裝類別是箭代替單獨的類的一部分:

-- combine two arrows in sequence 
>>> :: Arrow a => a b c -> a c d -> a b d 

-- the function arrow instance 
-- >>> :: (b -> c) -> (c -> d) -> (b -> d) 
-- this is just flip (.) 

-- MyArr instance 
-- >>> :: MyArr b c -> MyArr c d -> MyArr b d 

>>>函數有兩個箭頭和使用第一的輸出作爲輸入到第二。

這裏的另一家運營商,通常被稱爲「扇出」:

-- &&& applies two arrows to a single input in parallel 
&&& :: Arrow a => a b c -> a b c' -> a b (c,c') 

-- function instance type 
-- &&& :: (b -> c) -> (b -> c') -> (b -> (c,c')) 

-- MyArr instance type 
-- &&& :: MyArr b c -> MyArr b c' -> MyArr b (c,c') 

-- first and second omitted for brevity, see the accepted answer from KennyTM's link 
-- for further details. 

由於Control.Arrow提供計算相結合的手段,這裏有一個例子:

-- function that, given an input n, returns "n+1" and "n*2" 
calc1 :: Int -> (Int,Int) 
calc1 = (+1) &&& (*2) 

我經常發現功能,如calc1有用在複雜的摺疊中,或者對指針進行操作的函數。

Monad類型類爲我們提供了一種使用>>=函數將單點計算組合成單個新單子計算的方法。類似地,Arrow類爲我們提供了使用幾個原始函數(first,arr***,以及來自Control.Category的>>>id)將箭頭化的計算組合成單個新箭頭化計算的手段。與Monad類似,「箭頭是做什麼的?」這個問題。不能普遍回答。這取決於箭頭。

不幸的是,我不知道野外的箭頭實例的很多例子。功能和玻璃鋼似乎是最常見的應用。 HXT是唯一想到的其他重要用法。

[1]除了count。可以編寫一個計數函數,對ArrowLoop的任何實例執行相同的操作。

+0

啊,看看那個,它又是流換能器!這對你個人來說可能不是新聞(如果你是約翰,我認爲你是?),但是如果'MyArr'類型被擴展爲包含最終狀態和Kleisli箭頭,就像'data MyArr mab = Done |步驟(a - > m(b,MyArr mab)',加上對「運行」函數和「摺疊」構造函數的適當調整,結果是增量式左側摺疊流處理器的迭代精度非常接近。 「哦,枚舉可以像一個」箭頭「」可能對初學者沒有幫助... – 2010-11-16 17:18:09

+0

我實際上決定使用iteratees進行我的工作,因爲我需要一個流量傳感器,它的功率稍大一點。似乎很合適,這就是我最終在Hackage上建立一個小型圖書館的過程。 – 2010-11-16 19:19:51

28

從你的歷史記錄堆棧溢出一眼,我會假設你舒服一些其他的標準類型類別,特別是FunctorMonoid,並從這些簡短的比喻開始。

Functor上的單個操作是fmap,它用作列表上的一個通用版本map。這幾乎是類型類的全部目的;它定義了「你可以映射的東西」。因此,從某種意義上說,Functor代表列表的特定方面的概括。

Monoid的操作是空列表和(++)的通用版本,它定義了「可以關聯組合的事物,以及具有特定值的特定事物」。列表非常符合該描述的最簡單的事情,並且Monoid代表列表的該方面的概括。

以相同的方式如上述兩個,在Category型類的操作是的id(.)廣義的版本,並將其定義「的東西在特定方向上連接兩個類型,可連接頭 - 尾」。所以,這代表了函數那方面的概括。值得注意的是不包含在泛化中的是咖喱或功能應用。

Arrow type class由Category構建而成,但其基本概念相同:Arrow s是構成函數的東西,並且具有爲任何類型定義的「標識箭頭」。在Arrow類中定義的附加操作本身只是定義了一種將任意函數提升爲Arrow的方法,以及將兩個「並行」箭頭組合爲元組之間的單箭頭的方法。

因此,這裏首先要記住的是,表達式構建Arrow s基本上是精心設計的函數組合。像(***)(>>>)這樣的組合器用於編寫「無點」樣式,而proc表示法給出了一種在接線時將臨時名稱分配給輸入和輸出的方法。

這裏要注意的一個有用的事情是,即使Arrow s的有時被描述爲「下一步」,從Monad s時,真的不是有一個非常有意義的關係。對於任何Monad,您可以使用Kleisli箭頭,這些箭頭只是a -> m b之類的函數。 Control.Monad中的(<=<)運算符是這些的箭頭組合。另一方面,Arrow s不會給你一個Monad,除非你還包括ArrowApply類。所以沒有直接的聯繫。

這裏的關鍵區別在於,儘管Monad s可以用於序列計算並按部就班地進行,但在某種意義上,它與常規函數一樣是「永恆的」。它們可以包含額外的機器和功能,這些機器和功能被(.)拼接,但更像是建立管道,而不是積累行動。

其他相關類型類爲箭頭添加附加功能,例如能夠將箭頭與Either以及(,)組合在一起。


我最喜歡的一個Arrow的例子是狀態流傳感器,這是這個樣子:

data StreamTrans a b = StreamTrans (a -> (b, StreamTrans a b)) 

一個StreamTrans箭頭的輸入值轉換爲輸出的「更新」版本身;考慮這與有狀態Monad不同的方式。

編寫Arrow及其相關類型類的上述類型的實例可能是理解它們如何工作的很好練習!

我還寫了一個similar answer previously,您可能會發現有幫助。

24

我想補充一點,在Haskell中的箭頭比根據文獻可能出現的 簡單得多。它們只是功能的抽象。

要了解這實際上是多麼有用,請考慮一下您想要編寫的一些 函數,其中一些是純的,一些是單數的 。例如,f :: a -> b,g :: b -> m1 ch :: c -> m2 d

相知參與,我可以建立由手的組合物的類型,但 組合物的輸出類型必須反映中間 單子類型(在上述情況下,m1 (m2 d))。如果我只想將 的功能看作是a -> bb -> cc -> d?也就是, 我想抽象出monads的存在,並且僅爲 基礎類型提供理由。我可以使用箭頭來做到這一點。

這裏是一個抽象了IO在 IO單子存在下的功能,例如,我可以純函數構成它們不構成 代碼需要知道的是IO所涉及一個箭頭。我們首先定義一個 IOArrow來包裝IO功能:

data IOArrow a b = IOArrow { runIOArrow :: a -> IO b } 

instance Category IOArrow where 
    id = IOArrow return 
    IOArrow f . IOArrow g = IOArrow $ f <=< g 

instance Arrow IOArrow where 
    arr f = IOArrow $ return . f 
    first (IOArrow f) = IOArrow $ \(a, c) -> do 
    x <- f a 
    return (x, c) 

然後我做,我要撰寫一些簡單的功能:

foo :: Int -> String 
foo = show 

bar :: String -> IO Int 
bar = return . read 

,並利用它們:

main :: IO() 
main = do 
    let f = arr (++ "!") . arr foo . IOArrow bar . arr id 
    result <- runIOArrow f "123" 
    putStrLn result 

這裏我打電話給IOArrow和runIOArrow,但是如果我在這個多態函數庫中傳遞這些箭頭 ,他們只需要接受 類型爲「Arrow a => a b c」的參數。沒有一個庫代碼需要 意識到涉及monad。只有箭頭 的創建者和最終用戶需要知道。

泛化IOArrow的職能任何單子的工作被稱爲「Kleisli 箭頭」,並且已經有一個內置的箭頭,正是這樣做的:

main :: IO() 
main = do 
    let g = arr (++ "!") . arr foo . Kleisli bar . arr id 
    result <- runKleisli g "123" 
    putStrLn result 

你當然也可以使用箭頭組成運營商和PROC語法,以 使其更清晰一點是箭頭參與:

arrowUser :: Arrow a => a String String -> a String String 
arrowUser f = proc x -> do 
    y <- f -< x 
    returnA -< y 

main :: IO() 
main = do 
    let h =  arr (++ "!") 
      <<< arr foo 
      <<< Kleisli bar 
      <<< arr id 
    result <- runKleisli (arrowUser h) "123" 
    putStrLn result 

這應該清楚的是,儘管main知道IO單子參與, arrowUser沒有。如果沒有箭頭,就沒有辦法「隱藏」來自arrowUser 的IO,這並不是沒有辦法利用unsafePerformIO將中間一元值變回純粹的值(因此永遠丟失該上下文 )。例如:

arrowUser' :: (String -> String) -> String -> String 
arrowUser' f x = f x 

main' :: IO() 
main' = do 
    let h  = (++ "!") . foo . unsafePerformIO . bar . id 
     result = arrowUser' h "123" 
    putStrLn result 

試着寫,沒有unsafePerformIO,沒有arrowUser'不必 處理任何單子類型參數。

0

當我開始探索Arrow組合(實質上是Monads)時,我的方法是打破最常用的語法和組合,並且通過使用更多的聲明性方法來理解其原則。考慮到這一點,我找到下面的故障更直觀:

function(x) { 
    func1result = func1(x) 
    if(func1result == null) { 
    return null 
    } else { 
    func2result = func2(func1result) 
    if(func2result == null) { 
     return null 
    } else { 
     func3(func2result) 
    } 

所以,從本質上講,對於一些價值x,調用一個函數首先,我們認爲可能會返回null(FUNC1),另一種可能retun null或者是可互換地分配給null,最後,第三個函數也可以返回null。現在給定值x,將x傳遞給func3,如果它不返回null,則將此值傳遞給func2,並且只有當此值不爲null時,纔會將此值傳遞給func1。它更具確定性,並且控制流允許您構建更復雜的異常處理。

在這裏,我們可以利用箭頭組成:(func3 <=< func2 <=< func1) x

相關問題