簡體   English   中英

是否建議在尾遞歸形式中使用遞歸IO操作?

[英]Is it recommended to use recursive IO actions in the tail recursive form?

請考慮以下兩個變體:

myReadListTailRecursive :: IO [String]
myReadListTailRecursive = go []
    where 
    go :: [String] -> IO [String]   
    go l = do {
                 inp <- getLine;
                 if (inp == "") then 
                     return l;
                 else go (inp:l);
                }

myReadListOrdinary :: IO [String]
myReadListOrdinary = do
        inp <- getLine
        if inp == "" then
            return []
        else
            do
                moreInps <- myReadListOrdinary
                return (inp:moreInps)

在普通的編程語言中,人們會知道尾遞歸變體是更好的選擇。

但是,通過這個答案 ,顯然haskell的遞歸實現與重復使用遞歸堆棧的實現並不相似。

但是因為在這種情況下,所討論的程序涉及行動和嚴格的單子行列,我不確定是否適用相同的推理。 事實上,我認為在IO情況下,尾遞歸形式確實更好。 我不確定如何正確推理這一點。


編輯: David Young指出這里最外面的調用是(>>=) 即使在這種情況下,這些風格中的一種是否優於另一種?

這真的不是我寫它的方式,但是你所做的很清楚。 (順便說一下,如果你想能夠有效地從鏈中的任何函數插入任意輸出,而不使用monad,你可以嘗試使用Data.ByteString.Builder 。)

你的第一個實現非常類似於左折,而你的第二個非常類似於右折或地圖。 (您可能會嘗試實際編寫它們!)第二個對I / O有幾個優點。 處理輸入和輸出最重要的一點是它可以是交互式的

您會注意到第一個從外部構建整個列表:為了確定列表的第一個元素是什么,程序需要計算整個結構以到達最里面的thunk,即return l 程序首先生成整個數據結構,然后開始處理它。 當你減少一個列表時,這很有用,因為尾遞歸函數和嚴格的左折疊是有效的。

對於第二個,最外面的thunk包含列表的頭部和尾部,因此您可以抓住尾部,然后調用thunk來生成第二個列表。 這可以使用無限列表,它可以生成並返回部分結果。

這是一個人為的例子:一個程序,每行讀取一個整數並打印到目前為止的總和。

main :: IO ()
main = interact( display . compute 0 . parse . lines )
  where parse :: [String] -> [Int]
        parse [] = []
        parse (x:xs) = (read x):(parse xs)

        compute :: Int -> [Int] -> [Int]
        compute _ [] = []
        compute accum (x:xs) = let accum' = accum + x
                               in accum':(compute accum' xs)

        display = unlines . map show

如果你以交互方式運行,你會得到類似的東西:

$ 1
1
$ 2
3
$ 3
6
$ 4
10

但你也可以使用累積參數以遞歸方式編寫compute

main :: IO ()
main = interact( display . compute [] . parse . lines )
  where parse :: [String] -> [Int]
        parse = map read

        compute :: [Int] -> [Int] -> [Int]
        compute xs [] = reverse xs
        compute [] (y:ys) = compute [y] ys
        compute (x:xs) (y:ys) = compute (x+y:x:xs) ys

        display = unlines . map show

這是一個人為的例子,但嚴格的左側折疊是一種常見的模式。 但是,如果您使用累積參數編寫computeparse ,則這是您嘗試以交互方式運行時獲得的結果,並且在數字4之后命中EOF(Unix上的control-D ,Windows上的control-Z ):

$ 1
$ 2
$ 3
$ 4
1
3
6
10

這個左折版本需要先計算整個數據結構才能讀取任何數據結構。 這無法在無限列表上工作(你何時會達到基本情況?如果你這樣做,你甚至會如何反轉無限列表?)和一個在退出之前無法響應用戶輸入的應用程序 - 斷路器。

另一方面,尾遞歸版本在其累積參數中可以是嚴格的,並且將更有效地運行,尤其是當它沒有被立即消耗時。 除了參數之外,它不需要保留任何thunk或上下文,甚至可以重用相同的堆棧幀。 當您將列表縮減為值,而不是構建急切評估的輸出列表時,嚴格的累積函數(如Data.List.foldl' )是一個很好的選擇。 sumproductany函數不能返回任何有用的中間值。 它們本身必須首先完成計算,然后返回最終結果。

FWIW,我會選擇現有的monadic組合器,專注於可讀性/安心性。 使用unfoldM :: Monad m => m (Maybe a) -> m [a]

import Control.Monad (liftM, mfilter)
import Control.Monad.Loops (unfoldM)

myReadListTailRecursive :: IO [String]
myReadListTailRecursive = unfoldM go
  where
    go :: IO (Maybe String)
    go = do
        line <- getLine
        return $ case line of
            "" -> Nothing
            s -> Just s

或使用MonadPlus的實例Maybe ,與mfilter :: MonadPlus m => (a -> Bool) -> ma -> ma

myReadListTailRecursive :: IO [String]
myReadListTailRecursive = unfoldM (liftM (mfilter (/= "") . Just) getLine)

另一種更通用的選擇可能是使用LoopT

暫無
暫無

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

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