CWKSC's 翻譯文章

CIS 194 08 IO 輸入輸出

Source: 08-IO

▌IO 輸入輸出

CIS 194 第 8 週 2013 年 3 月 11 日

建議閱讀:

▌The problem with purity 純度問題

請記住,Haskell 是惰性 (lazy),因此 純粹 (pure)。這意味著兩件事:

  1. 函數可能沒有任何外部影響。例如,某個函數可能無法在屏幕上打印任何內容。函數只能計算其輸出
  2. 函數可能不取決於外部因素。例如,它們可能無法從鍵盤,文件系統或網絡讀取。函數可能僅取決於其輸入,換句話說,函數每次都應為相同的輸入提供相同的輸出

但是 — 有時候我們 確實 希望能夠做這樣的事情!如果使用 Haskell 唯一可以做的事情是編寫函數,然後我們可以在 ghci 提示符下對其進行評估,那麼從理論上講這很有趣,但實際上沒有用

實際上,使用 Haskell 可以做這些事情,但是看起來與大多數其他語言完全不同

▌The IO type / IO 類型

解決難題的方法是一種稱為 IO 的特殊類型。 IO a 類型的值是對有效計算 (effectful computations) 的描述,如果執行這些計算,(可能)將執行一些有效的 I/O 操作,並最終產生 a 類型的值。 這裡有一個間接的水平,對於理解這一點至關重要。 IO a 類型的值本身本身就是一種惰性的,完全安全的東西,沒有任何影響。 這只是對有效計算的描述。 認為它是一種一流的命令式程序

舉例來說,假設您有

c :: Cake

你有什麼?為什麼,當然是美味的蛋糕。乾淨利落

img

相比之下,假設您有

r :: Recipe Cake

你有什麼?一塊蛋糕?不,你有一些 如何做蛋糕的 說明,只是一些書面的紙張

img

不僅您實際上沒有蛋糕,僅擁有食譜對其他任何事物都沒有影響。只需將食譜握在手中,就不會導致烤箱變熱或麵粉灑在地板上或任何類似的東西上。要真正製作蛋糕,必須 遵循 食譜(導致麵粉撒出,配料混合,烤箱變熱等)

img

同樣,IO a 類型的值只是用於產生 a 類型值的 “配方”(並且可能在此過程中會產生一些影響)。 像任何其他值一樣,它可以作為參數傳遞,作為函數的輸出返回,存儲在數據結構中,或者(如我們將很快看到的)與其他 IO 值組合成更複雜的配方

那麼,實際上如何執行 IO 類型的值? 只有一種方法:Haskell 編譯器尋找特殊值

main :: IO ()

實際上將被交給運行時系統並執行。而已!將 Haskell 運行時系統想像成是一位主廚,他是唯一可以做飯的廚師

img

如果您希望遵循自己的食譜,那麼最好將其作為大食譜(main)的一部分,交給大廚。當然,main 可以任意複雜,並且通常由許多較小的 IO 計算組成

因此,讓我們編寫第一個實際的可執行的 Haskell 程序!我們可以使用函數

putStrLn :: String -> IO ()

給定一個 String,它返回一個 IO 計算,該計算將(在執行時)在屏幕上打印出該 String。 因此,我們將其簡單地放在一個名為 Hello.hs 的文件中:

main = putStrLn "Hello, Haskell!"

然後在命令行提示符下鍵入 runhaskell Hello.hs,導致我們的消息被打印到屏幕上!我們還可以使用 ghc --make Hello.hs 生成一個稱為 Hello 的可執行版本(Windows 上的 Hello.exe )

▌There is no String “inside” an IO String

IO String “內部” 沒有 String

許多新的 Haskell 用戶最終都在問一個問題,例如 “我有 IO String,如何將其轉換為 String ?”,或 “如何從IO String 中獲取 String ”? 根據上述直覺,應該清楚這些是無意義的問題:IO String 類型的值是對某種計算(一種用於生成 String 的配方)的描述。 在IO字符串“內部”沒有 String,在蛋糕配方“內部”沒有蛋糕。 要生成字符串(或美味的蛋糕),需要實際執行計算(或配方)。 唯一的方法是通過 main 將其(可能是更大的 IO 值的一部分)提供給 Haskell 運行時系統

▌Combining IO 組合 IO

現在應該很清楚了,我們需要一種將 IO 計算組合成更大的方法。

合併兩個 IO 計算的最簡單方法是使用 (>>) 運算符(發音為“ and then”),其類型為

(>>) :: IO a -> IO b -> IO b

這只是創建了一個 IO 計算,其中包括依次運行兩個輸入計算。 注意,第一次計算的結果被丟棄,我們只關心它的作用。 例如:

main = putStrLn "Hello" >> putStrLn "world!"

這對於“執行此操作; 做這個; 這樣做”,結果並不重要。 但是,通常這是不夠的。 如果我們不想放棄第一次計算的結果怎麼辦?

解決這種情況的第一個嘗試可能是使類型為 IO a -> IO b -> IO (a, b)。 但是,這也不足夠。 原因是我們希望第二個計算能夠依賴於第一個計算的結果。 例如,假設我們要從用戶那裡讀取一個整數,然後打印出比他們輸入的整數多一個的整數。 在這種情況下,第二次計算(在屏幕上打印一些數字)將根據第一次的結果而有所不同

相反,有一個運算符 (>>=)(發音為 “bind” ),其類型為

(>>=) :: IO a -> (a -> IO b) -> IO b

首先這可能很難,會繞到你的頭! (>>=) 需進行計算,該計算將產生類型 a 的值,而一個函數根據該類型 a 的中間值進行第二次計算。 (>>=) 的結果是(a 的描述)計算,該計算將執行第一個計算,並使用其結果來確定下一步要執行的操作,然後執行該操作

例如,我們可以編寫一個程序來讀取用戶的號碼並打印出其後繼者。請注意,我們使用的 readLn :: Read a => IO a ,這是一種從用戶讀取輸入並將其轉換為 Read 實例的任何類型的計算

main :: IO ()
main = putStrLn "Please enter a number: " >> (readLn >>= (\n -> putStrLn (show (n+1))))

當然,這看起來很醜陋,但是有更好的編寫方法,我們將在以後討論

▌Record syntax 記錄語法

該材料未在講課中介紹,但作為完成作業 8 的額外資源而提供

假設我們有一個數據類型,例如

data D = C T1 T2 T3

我們還可以使用 記錄語法 (Record syntax) 聲明此數據類型,如下所示:

data D = C { field1 :: T1, field2 :: T2, field3 :: T3 }

我們不僅為存儲在構造函數中的每個字段指定類型,而且為其指定 名稱 C。這個新版本的 D 可以與舊版本完全相同的方式使用(特別是我們仍然可以對 D 類型的值 C v1 v2 v3 進行構造和模式匹配)。 但是,我們還有其他好處

  1. 每個字段名稱自動是一個 投影函數 (projection function),該函數從 D 類型的值中獲取該字段的值。例如,field2 是類型的函數

    field2 :: D -> T2
    

    以前,我們將不得不通過編寫代碼來自己實現 field2

    field2 (C _ f _) = f
    

    如果我們具有許多字段的數據類型,那麼這將消除很多樣板!

  2. 除了用於 D 類型值的構造、修改和模式匹配之外,還有特殊的語法(除了用於此類事物的常規語法外)

    我們可以使用如下語法構造 D 類型的值

    C { field3 = ..., field1 = ..., field2 = ... }
    

    用正確類型的表達式填充 ... 。 請注意,我們可以按任何順序指定字段

    假設我們有一個值 d :: D。我們可以使用如下語法進行 修改 d

    d { field3 = ... }
    

    當然,“修改”並不是指實際上對 d 進行突變(mutating),而是構造一個新的 D 類型值,該值與 d 相同,只是用給定值替換了 field3 字段

    最後,我們可以像這樣對 D 類型的值進行模式匹配:

    foo (C { field1 = x }) = ... x ...
    

    這僅與 D 值中的 field1 字段匹配,將其稱為 x (當然,可以代替 x 我們也可以放置任意模式),而忽略其他字段

CWKSC

Author 作者

CWKSC

喜歡編程,會一點點鋼琴,會一點點畫畫,喜歡使用顏文字 About me 關於我

For any comments or discussions on my blog post, you can open an issue here

對於我博客文章的任何評論或討論,可以在這裏開一個 issue

Feel free to give your comments. OW<