When you’re dealing with monadic (straight IO, ReaderT IO, mtl, effect systems, free monads) code in Haskell, what are the various styles for handling them, and what are the tradeoffs between them?
For instance, I see that some users like to pretend Haskell is Cobol or Python, using a very imperative and accessible dialect:
main :: IO ()
main = program
program :: IO ()
program = do
let a = 3
let b = 4
print (a + b)
Others might prefer a more functional seeming dialect, going straight to monadic binds when viable, essentially being a form of monadic pipelining or point-free.
main :: IO ()
main = program
program :: IO ()
program = getLine >>= putStrLn
There are, I think, other approaches, which hybridize the pure bind and do approaches
import Network.HTTP.Conduit (simpleHttp)
import System.IO (hFlush, stdout)
import Data.ByteString.Lazy qualified as LBS
main :: IO ()
main = program
program :: IO ()
program = do
putStrLn "Enter the website you'd like to access."
putStr "Website: "
hFlush stdout
site <- getLine >>= simpleHttp
LBS.putStr site
putStrLn ""
There is also the where-intensive approach:
import Network.HTTP.Conduit (simpleHttp)
import System.IO (hFlush, stdout)
import Data.ByteString.Lazy qualified as LBS
main :: IO ()
main = program
program :: IO ()
program = getText
>>= getSite
>>= showSite
where
getText :: IO String
getText = do
putStrLn "Enter the website you'd like to access."
putStr "Website: "
hFlush stdout
getLine
getSite :: String -> IO ByteString
getSite = simpleHttp
showSite :: ByteString -> IO ()
showSite = (>> putStrLn "") . LBS.putStr
Are there any other ways to style Haskell monadic code that bear mentioning? What are the tradeoffs of each approach, in terms of readability, concision, flexibility, and modularity?
Edit: I’ll just put out my working hypothesis.
-
The ultra-imperative method is a waste, but is acceptable when people are looking for maximum onboarding potential, or are dealing with learners.
-
The pure bind approach obviously fails when you are reusing a value for multiple operations, but it also loses readability when the chain gets too large, at around 4-6 elements (human working memory averages 4, SD 1).
Monadic then also gets clumsy when you have more than 2 monadic values.
-
The hybrid approach can get eclipsed by the pure bind approach when the elements are small enough, or when it’s monadic / applicative then. It also allows breaking up terms that a pure bind approach has more trouble doing, or has to relegate itself to where.
-
Where doesn’t have to be where, because the functions could be top-level and reusable. The main benefit of where-like approaches might be in helping to provide an outline of your program, and encouraging monadic actions to be more composable.
On the other hand, it has the trade-off of introducing more terms than is strictly necessary, which can make code harder to comprehend.
There is a further potential for unidiomatic Haskell code, as well, which is double-edged. Aggressively using where potentially allows you to have a 0% pure Haskell program, at least on the top-level, which I’d consider bad, but it also is potentially helpful for learners to understand how to handle purity.