Dlaczego Monada nie ma już metody fail?

1

Na HaskellWiki jest informacja że Monada to type clasa która ma następujące metody (czy można tu w ogóle używać słowa metody?)

class Monad m where
  (>>=)  :: m a -> (  a -> m b) -> m b
  (>>)   :: m a ->  m b         -> m b
  return ::   a                 -> m a
  fail   :: String -> m a

Jednak w innym artykule jest informacja że funkcja/metoda fail jest pogardzana

Fail is referring to the "fail" function, which is part of the Monad typeclass and is almost universally despised.

Dodatkowo na hackage.haskell.org można zobaczyć że funkcja fail nie jest już częścią Monady i została przeniesiona do type class MonadFail

Po tym przydługim wstępie mam trzy pytania do znawców Haskella:

  1. Dlaczego funkcja fail była pogardzana?
  2. Czy jeśli funkcja fail jest w osobnej type classie MonadFail to dalej jest pogardzana?
  3. Jeśli dalej jest pogardzana to jak Monada powinna zgłaszać błędy? Niby artykuł daje odpowiedź że powinienem użyć Control.Failure, ale nie widzę w czym to jest lepsze od MonadFail

Z góry dziękuję za odpowiedzi

P.S. Teraz widzę że failure jest deprecated. Czyli zamiast Failure powinienem użyć MonadThrow

3

Nie znam się na Haskellu, to się wypowiem. Na Monadach też się nie znam, więc podwójnie merytoryczna wypowiedź [Można kliknąć łapkę dwa razy by docenić ten wkład :D]

Z tego co rozumiem, Monada jest dobrze zdefiniowana w sensie algebraicznym (endofunktor + dwie naturalne transformacje).
Fail nie wygląda na transformatę, która by się łapała pod tę "porządną algebraiczną definicję" (i nie wygląda na naturalną transformatę), może to jest źródło pogardy?

2

Haskella zaledwie lekko liznąłem, ale monady nie są ograniczone do Haskella.

czy można tu w ogóle używać słowa metody

Z tego co widzę na https://wiki.haskell.org/Failure#Enter_failure_package.2C_stage_left używają słowa functions. Z drugiej strony pojawia się słowo methods ale nie odnosi się do struktur syntaktycznych tylko oznacza podejście do rozwiązania problemu.

Po tym przydługim wstępie mam trzy pytania

Myślę, że chodzi o to, że pakowanie zbyt dużej ilości funkcjonalności do jednej bardzo ogólnej typeclassy jest błędne, podobnie jak np pakowanie zbyt dużej liczby metod do java.lang.Object. Czy Monad.fail ma sens w przypadku każdej monady? Dla przykładu monada Identity - tutaj w ogólności nie da się przerobić Stringa na instancję monady (no chyba, że mamy monadę Identity dla typu String, ale to przypadek szczególny i nawet nie wydaje mi się, by pakowanie komunikatu błędu wprost do wyniku miało sens). Skoro się nie da zaimplementować sensownie funkcji fail dla typu Identity to implementacja Monad z funkcją fail dla Identity musiałby być wadliwa. Po drugie czy argument typu String jest używany? Monad.fail dla np listy liczb zwraca pustą listę, a więc pomija wspomniany parametr typu String.

Control.Failure jest deprecated: http://hackage.haskell.org/package/failure

Ogólnie ten artykuł wygląda na trochę przestarzały, ale jeszcze raz powtórzę, że się nie znam dobrze na Haskellu.

Prawdopodobnie chodzi o to, by do reprezentowania błędów używać dodatkowych struktur danych i/ lub dodatkowych typeclass i to tam gdzie to ma sens, a nie próbować zamodelować reprezentowanie błędu wprost (bez dodatkowych warstw abstrakcji) dla każdej monady.

0
yarel napisał(a):

Z tego co rozumiem, Monada jest dobrze zdefiniowana w sensie algebraicznym (endofunktor + dwie naturalne transformacje).
Fail nie wygląda na transformatę, która by się łapała pod tę "porządną algebraiczną definicję" (i nie wygląda na naturalną transformatę), może to jest źródło pogardy?

Jest racja w tym co piszesz. Są monady które z definicji nie powinny użyć fail jak Maybe/Option lub Either (który ma swój własny sposób obsługi błędu). Ale są monady które z definicji mogą użyć fail jak IO. Niestety dalej nie wiem czy pisząc własną monadę powinienem użyć MonadFail czy złożenia Monad Either czy jeszcze coś innego spoza biblioteki standardowej. Wiem że Monad Either ma większą elastyczność niż MonadFail, ale tej elastyczności nie potrzebuję (przynajmniej na razie).

1

Doradzam używać mtl-a, w tych czasach to już standard. Jeżeli chcesz przepychać błędy przez wiele warstw logiki to używanie Either albo Maybe jest złym pomysłem. ExeptT powinno być lepszym wyborem. mtl (czy tam transformers) mocno ustandaryzował haskella:

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
import           Text.Read
import           Data.Text                      ( Text(..) )

import           Control.Monad.Except

newtype App a = App {
        runApp' :: ExceptT String IO a
    } deriving (Functor, Applicative, Monad, MonadError String, MonadIO)

runApp :: App a -> IO (Either String a)
runApp = runExceptT . runApp'

Handler z Servanta jest identycznie zaimplementowany https://hackage.haskell.org/package/servant-server-0.17/docs/src/Servant.Server.Internal.Handler.html#Handler

Potem możesz sobie rzucać błędy gdzie chcesz i wszystko spropaguje się w górę.

getInt :: App Int
getInt = do 
    line <- liftIO getLine 

    case readMaybe line :: Maybe Int of
        Nothing     -> throwError "Invalid input" 
        Just int    -> return int
        
mainLoop :: App Int 
mainLoop = do
    liftIO $ print "x = "
    x <- getInt
    
    liftIO $ print "y = "
    y <- getInt

    return $ x + y

main :: IO ()
main = do 
    result <- runApp mainLoop

    case result of 
        Left err        -> print err
        Right answer    -> print answer

Łapanie błędów to też nie jest nic trudnego.

mainLoop :: App Int 
mainLoop = do
    liftIO $ print "x = "
    x <- getInt
    
    liftIO $ print "y = "
    y <- getInt `catchError` \err -> do 
        liftIO $ print $ "Error occured when parsing y: " ++ err ++ " Defaulting to 0"
        return 0

    return $ x + y

1 użytkowników online, w tym zalogowanych: 0, gości: 1