簡體   English   中英

使用getChar重新實現getContents

[英]Reimplementing getContents using getChar

在我對Haskell中抓住懶惰IO的哀悼中,我嘗試了以下方法:

main = do
  chars <- getContents
  consume chars

consume :: [Char] -> IO ()
consume [] = return ()
consume ('x':_) = consume []
consume (c : rest) = do
  putChar c
  consume rest

它只是回顯了所有在stdin中鍵入的字符,直到我點擊'x'。

所以,我天真地認為應該可以使用getChar重新實現getContentsgetChar執行以下操作:

myGetContents :: IO [Char]
myGetContents = do
  c <- getChar
  -- And now?
  return (c: ???) 

原來它不是那么簡單,因為??? 需要IO [Char] -> [Char]類型的函數,我認為這會打破IO monad的整個想法。

檢查getContents (或者更確切地說是hGetContents )的實現揭示了整個香腸工廠的臟IO東西。 我的假設是否正確, myGetContents不使用臟的,即monad-breaking代碼, myGetContents就無法實現?

您需要一個新的原語unsafeInterleaveIO :: IO a -> IO a ,它會延遲其參數操作的執行,直到評估該操作的結果。 然后

myGetContents :: IO [Char]
myGetContents = do
  c <- getChar
  rest <- unsafeInterleaveIO myGetContents
  return (c : rest)

如果可能的話,你應該真的避免在System.IO.Unsafe使用任何東西。 除非絕對必要,否則它們往往會破壞參照透明度並且不是Haskell中常用的函數。

如果你稍微改變你的類型簽名,我懷疑你可以對你的問題采取一種更慣用的方法。

consume :: Char -> Bool
consume 'x' = False
consume _   = True

main :: IO ()
main = loop
  where
    loop = do
      c <- getChar
      if consume c
      then do
        putChar c
        loop
      else return ()

你可以做到這一點,沒有任何黑客攻擊。

如果您的目標只是將所有stdin讀入String ,則不需要任何unsafe*函數。

IO是Monad,Monad是應用程序的Functor。 Functor由函數fmap定義,其簽名為:

fmap :: Functor f => (a -> b) -> f a -> f b

滿足這兩個定律:

fmap id = id
fmap (f . g) = fmap f . fmap g

實際上, fmap將函數應用於包裝值。

給定一個特定的字符'c'fmap ('c':)的類型是fmap ('c':) 我們可以將這兩種類型寫下來,然后將它們統一起來:

fmap        :: Functor f => (a      -> b     ) -> f a      -> f b
     ('c':) ::               [Char] -> [Char]
fmap ('c':) :: Functor f => ([Char] -> [Char]) -> f [Char] -> f [Char]

回想一下IO是一個myGetContents :: IO [Char]函數,如果我們要定義myGetContents :: IO [Char] ,使用它似乎是合理的:

myGetContents :: IO [Char]
myGetContents = do
  x <- getChar
  fmap (x:) myGetContents

這是接近的,但不完全等同於getContents ,因為此版本將嘗試讀取文件末尾並拋出錯誤而不是返回字符串。 只要看一下它就應該說清楚:沒有辦法返回一個具體的清單,只有一個無限的連鎖鏈。 知道EOF的具體情況是"" (並使用fmap的中綴語法<$> ),我們將:

import System.IO
myGetContents :: IO [Char]
myGetContents = do
  reachedEOF <- isEOF
  if reachedEOF
  then return []
  else do
    x <- getChar
    (x:) <$> myGetContents

應用類提供(輕微)簡化。

回想一下IO是一個Applicative Functor,而不僅僅是任何舊的Functor。 這個類型類有“應用法則”,就像“Functor Laws”一樣,但我們會特別注意<*>

<*> :: Applicative f => f (a -> b) -> f a -> f b

這與fmap (aka <$> )幾乎完全相同,只是應用的函數也被包裝。 然后我們可以通過使用Applicative樣式來避免在我們的else子句中綁定:

import System.IO
myGetContents :: IO String
myGetContents = do
  reachedEOF <- isEOF
  if reachedEOF
  then return []
  else (:) <$> getChar <*> myGetContents

如果輸入可能是無限的,則需要進行一次修改。

還記得當我說你不想unsafe*函數時,如果你只是想把所有stdin讀成一個String嗎? 好吧,如果你只想要一些輸入,你就可以。 如果您的輸入可能無限長,那么您肯定會這樣做。 最終的程序在一個導入和一個單詞中有所不同:

import System.IO
import System.IO.Unsafe
myGetContents :: IO [Char]
myGetContents = do
  reachedEOF <- isEOF
  if reachedEOF
  then return []
  else (:) <$> getChar <*> unsafeInterleaveIO myGetContents

惰性IO的定義函數是unsafeInterleaveIO (來自System.IO.Unsafe )。 這會延遲IO動作的計算,直到需要它為止。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM