[英]non-monadic error handling in Haskell?
我想知道是否有一種優雅的方式在Haskell中進行非monadic錯誤處理,這在句法上比使用普通的Maybe
或Either
更簡單。 我想要處理的是非IO異常,例如在解析中,您自己生成異常以便稍后知道,例如,輸入字符串中出現錯誤。
我問的原因是monad似乎對我有病毒感染。 如果我想使用異常或類似異常的機制來報告純函數中的非嚴重錯誤,我總是可以使用either
並對結果進行case
分析。 一旦我使用monad,它很麻煩/不容易提取monadic值的內容並將其提供給不使用monadic值的函數。
更深層次的原因是monad似乎對許多錯誤處理來說是一種過度殺傷。 我學習使用monad的一個理由是monad允許我們穿過狀態。 但是在報告錯誤的情況下,我認為不需要線程狀態(失敗狀態除外,我真的不知道使用monad是否必不可少)。
(
編輯:正如我剛才所讀到的,在monad中,每個動作都可以利用之前動作的結果。 但是在報告錯誤時,通常不必知道先前操作的結果。 因此,使用monads可能存在過度殺戮。 在許多情況下,所需要的只是在不知道任何先前狀態的情況下中止並報告現場故障。 對我來說, Applicative
似乎是一個限制較少的選擇。
在解析的具體例子中,我們自己提出的行為/錯誤是否真的有效? 如果沒有,是否有一些比Applicative
模型錯誤處理更弱的東西?
)
那么,是否存在比monad更弱/更一般的范例,可以用來模擬錯誤報告? 我現在正在閱讀Applicative
並試圖找出它是否合適。 只是想事先詢問,以便我不會錯過顯而易見的事實。
與此相關的一個問題是,是否存在一種機制,它只是簡單地用每個基本類型括起來,例如,一個Either String
。 我在這里問的原因是所有monad(或者可能是functor)都包含一個帶有類型構造函數的基本類型。 因此,如果您想要將非異常感知功能更改為異常感知,那么您可以從,例如,
f:: a -> a -- non-exception-aware
至
f':: a -> m a -- exception-aware
但是,這種改變打破了在非例外情況下可以起作用的功能組合。 雖然你可以做到
f (f x)
你不能這樣做
f' (f' x)
因為外殼。 解決可能性問題的一種可能天真的方法是將f
改為:
f'' :: m a -> m a
我想知道是否有一種優雅的方式在這條線上進行錯誤處理/報告工作?
謝謝。
- 編輯---
只是為了澄清這個問題,請從http://mvanier.livejournal.com/5103.html舉個例子來制作一個像
g' i j k = i / k + j / k
能夠處理零除錯誤,當前的方法是逐項分解表達式,並在monadic動作中計算每個術語(有點像用匯編語言重寫):
g' :: Int -> Int -> Int -> Either ArithmeticError Int
g' i j k =
do q1 <- i `safe_divide` k
q2 <- j `safe_divide` k
return (q1 + q2)
如果(+)
也可能導致錯誤,則需要三個動作。 我認為當前方法中這種復雜性的兩個原因是:
正如本教程的作者所指出的,monad強制執行某種操作順序,這在原始表達式中是不必要的。 這就是問題的非一元部分來源(以及monad的“病毒”特征)。
在monadic計算之后,你沒有Int
,相反,你有Either a Int
,你無法直接添加。 當快遞變得比添加兩個術語更復雜時,樣板代碼將快速繁殖。 這就是封閉 - 問題的Either
一部分來自於問題的一部分。
在第一個示例中,您希望自己編寫函數f :: a -> ma
。 讓我們為討論選擇一個特定的a
和m
: Int -> Maybe Int
。
好的,正如你指出的那樣,你不能只做f (fx)
。 好吧,讓我們來概括這一點更加g (fx)
比方說,我們給出一個g :: Int -> Maybe String
使事情變得更加具體的),看看你需要做的情況下,逐案的內容:
f :: Int -> Maybe Int
f = ...
g :: Int -> Maybe String
g = ...
gComposeF :: Int -> Maybe String
gComposeF x =
case f x of -- The f call on the inside
Nothing -> Nothing
Just x' -> g x' -- The g call on the outside
這有點冗長,就像你說的那樣,我們希望減少重復。 我們還可以注意到一種模式: Nothing
總是Nothing
,並且x'
被取出Just x'
並被賦予組合。 另外,請注意,我們可以使用任何 Maybe Int
值來代替fx
,使事情變得更加通用。 所以讓我們把我們的g
拉成一個參數,所以我們可以給這個函數任意 g
:
bindMaybe :: Maybe Int -> (Int -> Maybe String) -> Maybe String
bindMaybe Nothing g = Nothing
bindMaybe (Just x') g = g x'
有了這個輔助函數,我們可以像這樣重寫我們的原始gComposeF
:
gComposeF :: Int -> Maybe String
gComposeF x = bindMaybe (f x) g
這非常接近於g . f
g . f
,如果它們之間沒有Maybe
差異,那么你將如何編寫這兩個函數。
接下來,我們可以看到我們的bindMaybe
函數並不特別需要Int
或String
,因此我們可以使它更有用:
bindMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b
bindMaybe Nothing g = Nothing
bindMaybe (Just x') g = g x'
實際上,我們所有必須改變的是類型簽名。
現在, bindMaybe
實際上已經存在:它是來自Monad
類型類的>>=
方法!
(>>=) :: Monad m => m a -> (a -> m b) -> m b
如果我們用Maybe
替換m
(因為Maybe
是Monad
一個實例,我們可以這樣做),我們得到與bindMaybe
相同的類型:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
讓我們來看看Monad
的Maybe
實例:
instance Monad Maybe where
return x = Just x
Nothing >>= f = Nothing
Just x >>= f = f x
就像bindMaybe
一樣,除了我們還有一個額外的方法讓我們把東西放到“monadic context”中(在這種情況下,這只是意味着將它包裝在Just
)。 我們原來的gComposeF
看起來像這樣:
gComposeF x = f x >>= g
還有=<<
,這是>>=
的翻轉版本,讓它看起來更像普通的合成版本:
gComposeF x = g =<< f x
還有一個內置函數用於組合函數,其類型為a -> mb
稱為<=<
:
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c
-- Specialized to Maybe, we get:
(<=<) :: (b -> Maybe c) -> (a -> Maybe b) -> a -> Maybe c
現在這真的看起來像功能組合!
gComposeF = g <=< f -- This is very similar to g . f, which is how we "normally" compose functions
正如您在問題中提到的那樣,使用do
notation將簡單除法函數轉換為正確處理錯誤的函數更難以閱讀且更冗長。
讓我們更仔細地看一下這個問題,但讓我們從一個更簡單的問題開始(這實際上比我們在這個答案的第一部分中看到的問題更簡單):我們已經有了一個函數,比如乘以10,我們想用一個給我們一個Maybe Int
的函數來組合它。 我們可以立即簡化這一點,說我們真正想做的是采用“常規”函數(例如我們的multiplyByTen :: Int -> Int
),我們想給它一個Maybe Int
(即一個值)在錯誤的情況下不存在)。 我們想要一個Maybe Int
也可以回來,因為我們希望錯誤傳播。
maybeCount :: String -> Maybe Int
,我們會說我們有一些函數maybeCount :: String -> Maybe Int
(可能將我們在String
使用單詞“compose”的次數除以5 maybeCount :: String -> Maybe Int
。具體是什么它並不重要雖然)我們想要將multiplyByTen
應用於結果。
我們將從相同類型的案例分析開始:
multiplyByTen :: Int -> Int
multiplyByTen x = x * 10
maybeCount :: String -> Maybe Int
maybeCount = ...
countThenMultiply :: String -> Maybe Int
countThenMultiply str =
case maybeCount str of
Nothing -> Nothing
Just x -> multiplyByTen x
我們可以再次對multiplyByTen
進行類似的“拉出”以進一步概括:
overMaybe :: (Int -> Int) -> Maybe Int -> Maybe Int
overMaybe f mstr =
case mstr of
Nothing -> Nothing
Just x -> f x
這些類型也可以更通用:
overMaybe :: (a -> b) -> Maybe a -> Maybe b
請注意,我們只需更改類型簽名,就像上次一樣。
然后我們的countThenMultiply
可以被重寫:
countThenMultiply str = overMaybe multiplyByTen (maybeCount str)
這是Functor
fmap
!
fmap :: Functor f => (a -> b) -> f a -> f b
-- Specializing f to Maybe:
fmap :: (a -> b) -> Maybe a -> Maybe b
事實上, Maybe
實例的定義也完全相同。 這允許我們將任何“正常”函數應用於Maybe
值並返回Maybe
值,任何失敗都會自動傳播。
fmap
: (<$>) = fmap
還有一個方便的中綴運算符同義詞。 這將在以后派上用場。 如果我們使用這個同義詞,這就是它的樣子:
countThenMultiply str = multiplyByTen <$> maybeCount str
Maybes
怎么辦? 也許我們有多個參數的“正常”函數,我們需要應用於多個Maybe
值。 當你在你的問題有,我們可以用做這個Monad
,並do
標記,如果我們這樣的傾向,但我們並不真正需要的全功率Monad
。 我們需要Functor
和Monad
之間的東西。
讓我們來看看你給出的分組示例。 我們想要轉換g'
以使用safeDivide :: Int -> Int -> Either ArithmeticError Int
。 “正常” g'
看起來像這樣:
g' i j k = i / k + j / k
我們真正想做的是這樣的事情:
g' i j k = (safeDivide i k) + (safeDivide j k)
那么,我們可以與Functor
接近 :
fmap (+) (safeDivide i k) :: Either ArithmeticError (Int -> Int)
順便說一下,這種類型類似於Maybe (Int -> Int)
。 Either ArithmeticError
部分只是告訴我們,我們的錯誤以ArithmeticError
值的形式提供信息,而不僅僅是Nothing
。 它可以幫助在精神上取代Either ArithmeticError
與Maybe
現在。
嗯,這有點像我們想要的,但是我們需要一種方法將Either ArithmeticError (Int -> Int)
的函數應用於Either ArithmeticError Int
。
我們的案例分析如下:
eitherApply :: Either ArithmeticError (Int -> Int) -> Either ArithmeticError Int -> Either ArithmeticError Int
eitherApply ef ex =
case ef of
Left err -> Left err
Right f ->
case ex of
Left err' -> Left err'
Right x -> Right (f x)
(作為旁注,第二種case
可以用fmap
簡化)
如果我們有這個功能,那么我們可以這樣做:
g' i j k = eitherApply (fmap (+) (safeDivide i k)) (safeDivide j k)
這仍然看起來不太好,但現在讓我們繼續吧。
結果是eitherApply
也已經存在:來自Applicative
(<*>)
。 如果我們使用這個,我們可以到達:
g' i j k = (<*>) (fmap (+) (safeDivide i k)) (safeDivide j k)
-- This is the same as
g' i j k = fmap (+) (safeDivide i k) <*> safeDivide j k
您可能記得早些時候有一個名為<$>
fmap
的中綴同義詞。 如果我們使用它,整個事情看起來像:
g' i j k = (+) <$> safeDivide i k <*> safeDivide j k
這看起來很奇怪,但你已經習慣了。 您可以將<$>
和<*>
視為“上下文敏感的空白”。 我的意思是,如果我們有一些常規函數f :: String -> String -> Int
,我們將它應用於普通的String
值,我們有:
firstString, secondString :: String
result :: Int
result = f firstString secondString
如果我們有兩個(例如) Maybe String
值,我們可以應用f :: String -> String -> Int
,我們可以像這樣將f
應用於它們:
firstString', secondString' :: Maybe String
result :: Maybe Int
result = f <$> firstString' <*> secondString'
區別在於我們不是添加空格,而是添加<$>
和<*>
。 這以這種方式推廣到更多的參數(給定f :: A -> B -> C -> D -> E
):
-- When we apply normal values (x :: A, y :: B, z :: C, w :: D):
result :: E
result = f x y z w
-- When we apply values that have an Applicative instance, for example x' :: Maybe A, y' :: Maybe B, z' :: Maybe C, w' :: Maybe D:
result' :: Maybe E
result' = f <$> x' <*> y' <*> z' <*> w'
請注意,上述代碼均未提及Functor
, Applicative
或Monad
。 我們只是將它們的方法用作任何其他常規輔助函數。
唯一的區別是這些特定的輔助函數可以在許多不同的類型上工作,但如果我們不想這樣做,我們甚至不必考慮它。 如果我們真的想要,我們可以根據它們的特殊類型來考慮fmap
, <*>
, >>=
etc,如果我們在特定類型上使用它們(在所有這些中我們都是這樣)。
我問的原因是monad似乎對我有病毒感染。
這種病毒特征實際上非常適合異常處理,因為它會強制您識別您的功能可能會失敗並處理故障情況。
一旦我使用monad,它很麻煩/不容易提取monadic值的內容並將其提供給不使用monadic值的函數。
您不必提取值。 以Maybe
作為一個簡單的例子,你經常可以編寫簡單的函數來處理成功案例,然后使用fmap
將它們應用於Maybe
值, maybe
/ fromMaybe
來處理失敗並消除Maybe
wrap。 Maybe
是一個monad,但這並沒有強迫你使用monadic界面或do
記法。 一般來說,“monadic”和“pure”之間並沒有真正的對立。
我學習使用monad的一個理由是monad允許我們穿過狀態。
這只是眾多用例中的一個。 Maybe
monad允許您在失敗后跳過綁定鏈中的任何剩余計算。 它不會破壞任何類型的狀態。
那么,是否存在比monad更弱/更一般的范例,可以用來模擬錯誤報告? 我現在正在閱讀
Applicative
並試圖找出它是否合適。
您當然可以使用Applicative
實例鏈接Maybe
計算。 (*>)
相當於(>>)
,並且沒有等價於(>>=)
因為Applicative
不如Monad
強大。 雖然通常不使用比實際需要更多的功率是一件好事,但我不確定使用Applicative
是否比你想要的更簡單。
雖然你可以做
f (fx)
你不能做f' (f' x)
你可以寫f' <=< f' $ x
但是:
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c
您可能會發現關於(>=>)
答案 ,以及可能在該問題中的其他討論,這很有趣。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.