haskellfunctional-programmingapplicativepure-function

How to skip unnecessary IOs in pure functions?


Update

There is an extra limit of this question, which is to avoid IO as much as possible.

This limit was originally placed at the end of my question, which seems to be hard to be noticed.

I actually know how to achieve my goal in Haskell, just as I know how to do it in other imperative programming languages - the imperative way.

But I am not using any other imperative programming, right? I am using Haskell. I'd like a Haskell way, a pure way.

I have reorganized my question by relocating the extra limit at a relatively conspicuous place. That was my bad. Lots of thanks to those quick responses.

Original Question

main :: IO ()
main =
    putStrLn =<< cachedOrFetched
        <$> getLine
        <*> getLine

cachedOrFetched :: String -> String -> String
cachedOrFetched cached fetched =
    if not $ null cached
    then cached
    else fetched

The above code performs IO for twice. But the desired behavior is to skip the second IO when the result of the first IO is not null.

I know I can achieve this by using do or when. Given using too many dos violates my original intention using Haskell, I probably gonna live with when.

Or is there a better way? A purer way?

Here is the entire thing

I started learning Haskell about two weeks ago. I am not expecting a job from it, but simply attracted by programming language itself. Because it's the "purest" as far as I know.

At first, everything seemed as good as I expected. But then I found I have to write IOs among my pure code. I spent quite a long time to find a way to do impure-taint-control. Applicative Functor seemed to be the rescue.

With it, I can "curry" the impure IOs into my pure functions, which saves a lot of do, <- and explicit IO notations. However, I got stuck with this very problem - I am not able to skip unnecessary IOs in pure functions.

Again, a lot of searches and read. Unfortunately, no satisfying answer till now.

References


Solution

  • To skip IO, you must tell the cachedOrFetched that you're doing IO, instead of passing two strings to it after reading both lines. Only then, it can conditionally run the second getLine IO action. Of course, cachedOrFetched doesn't necessarily need to deal with IO, it can work for any monad:

    main :: IO ()
    main = putStrLn =<< cachedOrFetched getLine getLine
    
    cachedOrFetched :: Monad m => m String -> m String -> m String
    cachedOrFetched first second = do
        cached <- first
        if not $ null cached
            then return cached
            else second
    

    I would probably write

    main :: IO ()
    main = getLine >>= ifNullDo getLine >>= putStrLn
    
    ifNullDo :: Applicative f => f String -> String -> f String
    ifNullDo fetch cached = 
        if not $ null cached
            then pure cached
            else fetch
    

    I am not using any other imperative programming, right?

    Yes you are. If you are writing a main function or use the IO type, you are writing an imperative program, one IO action after the other. The (pure) program needs to control when, if, and in what order to run the putStrLn and getLine commands.

    Haskell just makes imperative programming explicit, and allows to abstract from it very easily, e.g. running a simulation instead of actual IO by swapping out the monad.

    I'd like a Haskell way, a pure way.

    The Haskell way is to separate the pure application logic from the impure boundary to the outside world; very much like the ports and adapters architecture. Of course, this can only go so far, and for a toy example such as cachedOrFetched it's hard to see where to put the boundary. But in larger applications, you typically come up with your own abstraction for "actions" in the business logic, and deal only with them instead of IO directly.