簡體   English   中英

Haskell中的非monadic錯誤處理?

[英]non-monadic error handling in Haskell?

我想知道是否有一種優雅的方式在Haskell中進行非monadic錯誤處理,這在句法上比使用普通的MaybeEither更簡單。 我想要處理的是非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)

如果(+)也可能導致錯誤,則需要三個動作。 我認為當前方法中這種復雜性的兩個原因是:

  1. 正如本教程的作者所指出的,monad強制執行某種操作順序,這在原始表達式中是不必要的。 這就是問題的非一元部分來源(以及monad的“病毒”特征)。

  2. 在monadic計算之后,你沒有Int ,相反,你有Either a Int ,你無法直接添加。 當快遞變得比添加兩個術語更復雜時,樣板代碼將快速繁殖。 這就是封閉 - 問題的Either一部分來自於問題的一部分。

在第一個示例中,您希望自己編寫函數f :: a -> ma 讓我們為討論選擇一個特定的amInt -> 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函數並不特別需要IntString ,因此我們可以使它更有用:

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 (因為MaybeMonad一個實例,我們可以這樣做),我們得到與bindMaybe相同的類型:

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

讓我們來看看MonadMaybe實例:

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 我們需要FunctorMonad之間的東西。

讓我們來看看你給出的分組示例。 我們想要轉換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 ArithmeticErrorMaybe現在。

嗯,這有點像我們想要的,但是我們需要一種方法將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'

一個非常重要的說明

請注意,上述代碼均未提及FunctorApplicativeMonad 我們只是將它們的方法用作任何其他常規輔助函數。

唯一的區別是這些特定的輔助函數可以在許多不同的類型上工作,但如果我們不想這樣做,我們甚至不必考慮它。 如果我們真的想要,我們可以根據它們的特殊類型來考慮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.

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