6

此代碼(從Learn You A Haskell拍攝):懶惰的評價和IO副作用混亂

main = do putStr "Hey, " 
      putStr "I'm " 
      putStrLn "Andy!" 

顯然desugars到

main =  putStr "Hey, " >>= 
     (\_ -> putStr "I'm " >>= 
     (\_ -> putStrLn "Andy!")) 

其中,按照我的理解,可以interpretted的話說,「爲了putStrLn「Andy!」我首先需要把STR「我是」,爲了做到這一點,我首先需要把STR「嘿」;

我不同意這種解釋,這是令人討厭的,因爲t他的編譯器顯然不會讓我感到困惑。我對它的問題是,lambda忽略了他們的論點,在懶惰的評估中,這種事情不應該被認可並被短路?

此外,當然,綁定返回一個IO操作,當該IO操作落入主操作時,它會被執行。但是,如何阻止它打印「嘿,安迪!我是」?我懷疑這是綁定正在做的事情。

此外,類型爲「IO()」的IO操作如何攜帶足夠的信息以允許運行時系統打印「嘿,我是安迪!」? IO()與IO()的區別是如何打印「Hello World!」或寫入文件?

考慮另一個,從維基百科頁面單子:

加糖版本:

do 
    putStrLn "What is your name?" 
    name <- getLine 
    putStrLn ("Nice to meet you, " ++ name ++ "!") 

脫版本:

putStrLn "What is your name?" >>= 
    (\_ -> 
     getLine >>= 
     (\name -> 
      putStrLn ("Nice to meet you, " ++ name ++ "!"))) 

類似這裏的故事。

我想我只需要看到IO的綁定的定義,然後它將全部清楚。如果有人能夠幫助我逐步瞭解程序實際得到評估的方式並找出發生副作用的確切時刻,那麼其他一些有用的東西就會有幫助。

+3

「拉姆達斯忽視他們的論點,在懶惰的評估過程中,這種事情不應該被認爲是短暫的嗎?」你打賭! '(>> =)'的第二個參數在這裏是一個特別懶惰的函數,但是'(>> =)'函數_itself_不是懶惰的。 –

回答

10

閱讀Simon Peyton Jones撰寫的「Tackling the awkward squad」論文。

對於相關的問題,請參見

採取任何這樣的解釋,包括我的一粒鹽 - 沒有揮手可以取代嚴格的同行評議文件,並且解釋必然過於簡化。

一個非常粗略的觀點是,>>=可以被看作是一個列表構造:

data IO = [Primitive] 

和IO子系統解構的main價值,並消耗了名單。即`主要is just a list. So you may want to take a look at the definition of Haskell entry point above主要,綁定是相當無趣。

您還可以閱讀有關haskell歷史的論文,並查看IO子系統的早期版本,以瞭解正在發生的事情。

也看看C language is purely functional Conal Elliott的諷刺文章。

功能純度的定義並不重要,我記得一篇闡述定義的文章,但我不記得標題。

+0

有趣的是,每個人總是通過比喻解釋>> = IO值。在前奏的某個地方沒有定義嗎?爲什麼沒有人會引用它?在魔術發生的地方綁定了IO嗎?我現在正在閱讀那些笨拙的班級報告,甚至連SPJ都不願意實際定義綁定 – TheIronKnuckle

+5

'IO'類型是抽象的,所以在Haskell標準中沒有'>> ='的定義。根據你實現'IO'的方式,你會有'>> ='的不同實現。如果你深入瞭解ghc,你會發現'IO'是一個狀態monad,'>> ='只是綁定狀態monad。 (編譯器中有更多的魔術來使這個效率更高。) – augustss

+0

@ThelronKnuckle它不是'值'的類比。這是使用舊的main :: [Request] - > [Response]'想法的另一個純IO實現的類比。另外最有趣的部分不是綁定,而是IO monad的'runIO'。由於IO monad是抽象的,我們必須或者像SPJ那樣提供抽象的解釋,或者像我一樣使用一些實現策略。 – nponeccop

1

我認爲如果您再次將這些操作視爲功能,這是更容易理解的。您綁定示例(do { foo <- getLine ; putStrLn foo ; })直觀類似於以下功能:

apply arg func = func (arg) 

除了功能是事務。因此我們的電話func(arg)被評估,如果有任何只有(arg)成功完成。否則,我們fail在我們的行動。

這是不同於普通的功能,因爲然後Haskell真的不在乎,如果(arg)完全計算或根本不在乎,直到它需要一點點func(arg)繼續程序。

7

在一個真正的Haskell實現中看着IO可能會比其啓發更多的混淆。不過想到IO像這樣被定義的(假設你知道GADTs):

data IO a where 
    Return a :: IO a 
    Bind :: IO a -> (a -> IO b) -> IO b 
    PutStr :: String -> IO() 
    GetLine :: IO String 

instance Monad IO where 
    return = Return 
    (>>=) = Bind 

putStr :: String -> IO() 
putStr = PutStr 

getLine :: IO String 
getLine = GetLine 

因此,當你評估程序(IO()型),它所做的是建立一個描述如何IO()類型的數據結構一旦你執行它,與世界的交互就會發生。然後,您可以想象執行引擎正在寫入,例如C,並且所有效果都發生在哪裏。

所以

main = do putStr "Hey, " 
      putStr "I'm " 
      putStrLn "Andy!" 

相同

main = Bind (PutStr "Hey, ") (\ _ -> Bind (PutStr "I'm ") (\ _ -> PutStr "Andy!")) 

和這些來自執行引擎的工作方式排序。

這就是說,我知道沒有Haskell實現,實際上這樣做。真正的實現傾向於實現IO作爲狀態monad,並帶有表示真實世界被傳遞的令牌(這是保證排序的原因),而基本類型如putStr只是對C函數的調用。

+0

+1爲GADT方法和'在真正的Haskell實現中查看IO可能會混淆更多的東西,而不是啓發'的事情。 – nponeccop

3

我想我只需要看IO的綁定定義,然後它就會全部清楚。

是的,你應該這樣做。這其實很簡單,如果我remeber正確它是這樣

newtype IO = IO (RealWorld -> (a, RealWorld)) 

(IO f) >>= g = ioBind f g 
    where 
     ioBind :: (RealWorld -> (a, RealWorld)) -> (a -> IO b) -> RealWorld -> (b, RealWorld) 
     ioBind f g rw = case f rw of 
      (a, [email protected]) -> case g a of 
       IO b -> b rw 

的「絕招」是每一個IO值實際上是基本功能,但要評價它,你將需要RealWorld類型的令牌。只有一個實例可以提供這樣的值 - 運行main的運行時系統(當然還有不能命名的函數)。