Search code examples
haskellfunctional-programmingmonadsdryreusability

Does the expressiveness of monads come at the expense of code reuse?


When I compare the binary operations of the Applicative and Monad type class

(<*>) :: Applicative f => f (a -> b) -> f a -> f b
(=<<) :: Monad m       => (a -> m b) -> m a -> m b

two differences become apparent:

  • ap expects a normal, pure function, whereas bind expects a monadic action, which must return a monad of the same type
  • with ap the sequence of actions is determined by the applicative, whereas with bind the monadic action can determine the control flow

So monads give me additional expressive power. However, since they no longer accept normal, pure functions, this expressiveness seems to come at the expense of code reuse.

My conclusion might be somewhat naive or even false, since I have merely little experience with Haskell and monadic computations. Any light in the dark is appreciated!


Solution

  • Code reuse is only an advantage if you can reuse code to do what you actually want.

    GHCi> putStrLn =<< getLine
    Sulfur
    Sulfur
    GHCi> 
    

    Here, (=<<) picks the String result produced in an IO context by getLine and feeds it to putStrLn, which then prints said result.

    GHCi> :t getLine
    getLine :: IO String
    GHCi> :t putStrLn
    putStrLn :: String -> IO ()
    GHCi> :t putStrLn =<< getLine
    putStrLn =<< getLine :: IO ()
    

    Now, in the type of fmap/(<$>)...

    GHCi> :t (<$>)
    (<$>) :: Functor f => (a -> b) -> f a -> f b
    

    ... it is perfectly possible for b to be IO (), and therefore nothing stops us from using putStrLn with it. However...

    GHCi> putStrLn <$> getLine
    Sulfur
    GHCi> 
    

    ... nothing will be printed.

    GHCi> :t putStrLn <$> getLine
    putStrLn <$> getLine :: IO (IO ())
    

    Executing an IO (IO ()) action won't execute the inner IO () action. To do that, we need the additional power of Monad, either by replacing (<$>) with (=<<) or, equivalently, by using join on the IO (IO ()) value:

    GHCi> :t join
    join :: Monad m => m (m a) -> m a
    GHCi> join (putStrLn <$> getLine)
    Sulfur
    Sulfur
    GHCi> 
    

    Like chi, I also have trouble understanding the premises of your question. You seem to expect that one of Functor, Applicative and Monad will turn out to be better than the others. That is not the case. We can do more things with Applicative than with Functor, and even more with Monad. If you need the additional power, use a suitably powerful class. If not, using a less powerful class will lead to simpler, clearer code and a broader range of available instances.