簡體   English   中英

Haskell 錯誤處理方法

[英]Haskell approaches to error handling

這里沒有爭論 Haskell 中有多種機制來處理錯誤並正確處理它們。 Error monad、Either、Maybe、異常等。

那么為什么用其他語言編寫易發生異常的代碼比用 Haskell 更直接?

假設我想編寫一個命令行工具來處理在命令行上傳遞的文件。 我想:

  • 驗證提供了文件名
  • 驗證文件是否可用且可讀
  • 驗證文件具有有效的標題
  • 創建 output 文件夾並驗證 output 文件是否可寫
  • 處理文件、解析錯誤、不變錯誤等。
  • Output 文件,寫入錯誤,磁盤已滿等錯誤。

所以一個非常直接的文件處理工具。

在 Haskell 中,我會將此代碼包裝在某種單子組合中,使用 Maybe 和 Either,並根據需要翻譯和傳播錯誤。 最后,這一切都得到了一個 IO monad,我可以在其中向用戶提供 output 的狀態。

在另一種語言中,我只是拋出一個異常並在適當的地方捕獲。 直截了當。 我不會花太多時間在認知邊緣試圖解開我需要什么樣的機制組合。

我只是在接近這個錯誤還是這種感覺有一些實質內容?

編輯:好的,我收到反饋,告訴我感覺更難,但實際上並非如此。 所以這是一個痛點。 在 Haskell 中,我正在處理單子堆棧,如果我必須處理錯誤,我將在此單子堆棧中添加另一層。 我不知道我必須添加多少電梯和其他語法垃圾才能使代碼編譯但添加零語義意義。 沒有人覺得這增加了復雜性?

在 Haskell 中,我會將此代碼包裝在某種單子組合中,使用 Maybe 和 Either,並根據需要翻譯和傳播錯誤。 最后,這一切都得到了一個 IO monad,我可以在其中向用戶提供 output 的狀態。

在另一種語言中,我只是拋出一個異常並在適當的地方捕獲。 直截了當。 我不會花太多時間在認知邊緣試圖解開我需要什么樣的機制組合。

我不會說你一定是錯誤地接近它。 相反,您的錯誤在於認為這兩種情況是不同的。 他們不是。

“簡單地拋出和捕獲”相當於將與 Haskell 錯誤處理方法的某種組合完全相同的概念結構強加於您的整個程序。 確切的組合取決於您要與之比較的語言的錯誤處理系統,這說明了 Haskell看起來更復雜的原因:它允許您根據需要混合和匹配錯誤處理結構,而不是給您一個隱含的、一個- 最適合的解決方案。

所以,如果你需要一種特殊的錯誤處理方式,你可以使用它; 並且您僅將它用於需要它的代碼。 不需要它的代碼——由於既不生成也不處理相關類型的錯誤——被標記為這樣,這意味着您可以使用該代碼而不必擔心會產生這種錯誤。


關於句法笨拙的主題,這是一個尷尬的主題。 理論上,它應該是無痛的,但是:

  • Haskell 一直是一種研究驅動的語言,在其早期,許多東西仍在不斷變化,有用的習語還沒有普及,所以舊代碼很可能是一個糟糕的角色 model
  • 一些庫在處理錯誤方面沒有那么靈活,要么是由於上述舊代碼的僵化,要么只是缺乏修飾
  • 我不知道有關如何最好地構造新代碼以進行錯誤處理的任何指南,因此新手只能自己動手

我猜你可能以某種方式“做錯了”,並且可以避免大部分語法混亂,但期望你(或任何普通的 Haskell 程序員)自己找到最好的方法可能是不合理的.

至於 monad 轉換器堆棧newtype ,我認為標准方法是為您的應用程序新建整個堆棧,派生或實現相關類型類的實例(例如MonadError ),然后使用通常不需要的類型類的功能lift 您為應用程序的核心編寫的一元函數都應該使用newtype d 堆棧,因此也不需要提升。 我認為,唯一無法避免的低語義含義是liftIO

處理大量的轉換器可能是一個真正令人頭疼的問題,但只有當有很多不同轉換器的嵌套層時(堆積StateTErrorT的交替層,中間有一個ContT ,然后試着告訴我你的代碼是什么實際上會這樣做)。 不過,這很少是您真正想要的。


編輯:作為一個小附錄,我想提請注意我在寫一些評論時發生的更普遍的觀點。

正如我所說並且@sclv 很好地展示了,正確的錯誤處理確實那么復雜。 您所能做的就是改變這種復雜性,而不是消除它,因為無論您執行什么可能會獨立產生錯誤的多個操作,您的程序都需要以某種方式處理所有可能的組合,即使這種“處理”只是簡單地失敗結束並死去。

也就是說,Haskell 在一個方面確實與大多數語言有本質上的不同:通常,錯誤處理是明確的和一流的,這意味着一切都是公開的並且可以自由操作。 這樣做的另一面是隱式錯誤處理的丟失,這意味着即使您只想打印錯誤消息並死掉,您也必須明確地這樣做。 所以實際上在 Haskell 中進行錯誤處理更容易,因為它具有一流的抽象,但忽略錯誤更難。 然而,在任何現實世界的生產使用中,那種“所有人都棄船”錯誤幾乎永遠不會正確,這就是為什么看起來尷尬被拋在一邊的原因。

因此,當您需要明確地處理錯誤時,雖然一開始事情確實會更復雜,但重要的是要記住這就是它的全部內容 一旦你學會了如何使用正確的錯誤處理抽象,復雜性幾乎達到了一個平台,並且隨着程序的擴展並沒有真正變得更加困難。 你使用這些抽象的次數越多,它們就越自然。

讓我們看看您想要做的一些事情:

驗證提供了文件名

如果他們不是? 干脆放棄吧?

驗證文件是否可用且可讀

如果有些不是? 處理剩下的,當你遇到壞的時拋出異常,警告壞的並處理好的? 在做任何事情之前退出?

驗證文件具有有效的標題

如果他們不這樣做? 同樣的問題——跳過壞的,提前中止,警告壞的,等等......

處理文件、解析錯誤、不變錯誤等。

再次,做什么,跳過壞行,跳過壞文件,中止,中止和回滾,打印警告,打印可配置的警告級別?

關鍵是有可用的選擇和選項。 要以一種反映命令式的方式做你想做的事,你根本不需要任何可能或任何一個單子堆棧。 您所需要的只是在 IO 中拋出和捕獲異常。

如果你不想到處使用異常,並獲得一定程度的控制,你仍然可以在沒有 monad 堆棧的情況下做到這一點。 例如,如果您想處理可以處理的文件並獲得結果,並在無法處理的文件上返回錯誤,那么 Eithers 工作得很好——只需編寫 function of FilePath -> IO (Either String Result) 然后將其mapM到您的輸入文件列表上。 然后partitionEithers結果列表,然后 mapM 一個 function 的Result -> IO (Maybe String)覆蓋結果,並catMaybe錯誤字符串。 現在您可以mapM print <$> (inputErrors ++ outputErrors)來顯示兩個階段中出現的所有錯誤。

或者,你知道,你也可以做其他事情。 在任何情況下,在 monad 堆棧中使用MaybeEither都有它的位置。 但是對於典型的錯誤處理案例來說,直接顯式的處理更方便,功能也很強大。 只需要一些時間來適應各種各樣的功能,就可以方便地操作它們。

評估到Either ea和模式匹配與trycatch有什么區別,除了它傳播異常的事實(如果你使用 Either monad,你可以模擬這個)

請記住,大多數情況下,單子使用某些東西(在我看來)是丑陋的,除非您大量使用可能會失敗的函數。

如果你只有一個可能的失敗,那就沒有錯

func x = case tryEval x of
             Left e -> Left e
             Right val -> Right $ val + 1

func x = (+1) <$> trvEval x

它只是表示同一事物的一種功能方式。

暫無
暫無

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

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