Как да се справя с много нива на отстъп?

Пиша скрипт, който има много логически сложен цикъл:

main = do
    inFH <- openFile "..." ReadMode
    outFH <- openFile "..." WriteMode

    forM myList $ \ item ->
        ...
        if ... 
            then ...
            else do
                ...
                case ... of
                    Nothing -> ...
                    Just x  -> do
                        ...
                            ...

Кодът скоро лети надясно, така че мислех да го разбия на части, използвайки например where клаузи. Проблемът е, че много от тези ... съдържат изрази за четене/запис към двата манипулатора inFH и outFH и използването на израз where ще изведе тези две имена извън контекста. Ще трябва да изпращам тези две променливи всеки път, когато използвам израз where.

Има ли по-добър начин за справяне с това?


person xzhu    schedule 08.10.2015    source източник


Отговори (3)


В много случаи тези дълбоко вложени вдлъбнатини са резултат от проверка на дълбоко вложени грешки. Ако това е така за вас, трябва да разгледате MaybeT и неговия по-голям брат ExceptT. Те предлагат чист начин за отделяне на кода „какво правим, когато нещо се обърка“ от кода „какво правим, ако приемем, че всичко върви както трябва“. Във вашия пример мога да напиша:

data CustomError = IfCheckFailed | MaybeCheckFailed

main = handleErrors <=< runExceptT $ do
    inFH  <- liftIO $ openFile ...
    outFH <- liftIO $ openFile ...
    forM myList $ \item -> do
        when (...) (throwError IfCheckFailed)
        ...
        x <- liftMaybe MaybeCheckFailed ...
        ...

liftMaybe :: MonadError e m => e -> Maybe a -> m a
liftMaybe err = maybe (throwError err) return

handleErrors :: Either CustomError a -> IO a
handleErrors (Left err) = case err of
    IfCheckFailed    -> ...
    MaybeCheckFailed -> ...
handleErrors (Right success) = return success

Забележете, че все още увеличаваме отстъпа в цикъла forM; но другите проверки се извършват "в ред" в main и се обработват на едно и също ниво на отстъп в handleErrors.

person Daniel Wagner    schedule 08.10.2015

Въпреки че вероятно има по-добри начини за решаване на вашия конкретен проблем (вижте например отговора на Daniel Wagner), винаги можете да използвате let, за да въведете ново име в произволен обхват. Ето една наистина безсмислена демонстрация:

main = do
    inFH <- return "inf"
    outFH <- return "ouf"

    let subAction = do
            if length inFH > 2
                then print "foo"
                else subSubAction

        subSubAction = case outFH of
            [] -> print "bar"
            _ -> print "baz"

    forM [1..10] $ \ item -> do
        print item
        subAction
person duplode    schedule 08.10.2015
comment
Благодаря. Това е нещо, което пренебрегнах. Но едно нещо, което не ми харесва в използването на let тук е, че обръща реда на четене -- първо дефинирам част от стъпката и след това идва останалата част от потока. Също така не ми харесва да давам име на част от логически поток, което е друга причина, поради която исках да избегна използването на where. - person xzhu; 09.10.2015

Трябва да направите същото, което бихте направили с всеки друг език за програмиране. Функциите трябва да са лесни за разбиране. Това обикновено означава, че ако е дълъг, няма много контролен поток, в противен случай го разделете на отделни функции.

Така че основното може да изглежда така:

main = do
    inFH <- openFile ...
    outFH <- openFile ....

    mapM prcoessItem myList
person dave    schedule 08.10.2015
comment
Хм, това е доста несъвместимо с заявеното изискване да не се пропускат inFH и outFH навсякъде. - person Ørjan Johansen; 08.10.2015