What is so special about Monads?

前端 未结 5 2054
深忆病人
深忆病人 2021-01-31 04:00

A monad is a mathematical structure which is heavily used in (pure) functional programming, basically Haskell. However, there are many other mathematical structures available, l

5条回答
  •  别那么骄傲
    2021-01-31 04:43

    If a type m :: * -> * has a Monad instance, you get Turing-complete composition of functions with type a -> m b. This is a fantastically useful property. You get the ability to abstract various Turing-complete control flows away from specific meanings. It's a minimal composition pattern that supports abstracting any control flow for working with types that support it.

    Compare this to Applicative, for instance. There, you get only composition patterns with computational power equivalent to a push-down automaton. Of course, it's true that more types support composition with more limited power. And it's true that when you limit the power available, you can do additional optimizations. These two reasons are why the Applicative class exists and is useful. But things that can be instances of Monad usually are, so that users of the type can perform the most general operations possible with the type.

    Edit: By popular demand, here are some functions using the Monad class:

    ifM :: Monad m => m Bool -> m a -> m a -> m a
    ifM c x y = c >>= \z -> if z then x else y
    
    whileM :: Monad m => (a -> m Bool) -> (a -> m a) -> a -> m a
    whileM p step x = ifM (p x) (step x >>= whileM p step) (return x)
    
    (*&&) :: Monad m => m Bool -> m Bool -> m Bool
    x *&& y = ifM x y (return False)
    
    (*||) :: Monad m => m Bool -> m Bool -> m Bool
    x *|| y = ifM x (return True) y
    
    notM :: Monad m => m Bool -> m Bool
    notM x = x >>= return . not
    

    Combining those with do syntax (or the raw >>= operator) gives you name binding, indefinite looping, and complete boolean logic. That's a well-known set of primitives sufficient to give Turing completeness. Note how all the functions have been lifted to work on monadic values, rather than simple values. All monadic effects are bound only when necessary - only the effects from the chosen branch of ifM are bound into its final value. Both *&& and *|| ignore their second argument when possible. And so on..

    Now, those type signatures may not involve functions for every monadic operand, but that's just a cognitive simplification. There would be no semantic difference, ignoring bottoms, if all the non-function arguments and results were changed to () -> m a. It's just friendlier to users to optimize that cognitive overhead out.

    Now, let's look at what happens to those functions with the Applicative interface.

    ifA :: Applicative f => f Bool -> f a -> f a -> f a
    ifA c x y = (\c' x' y' -> if c' then x' else y') <$> c <*> x <*> y
    

    Well, uh. It got the same type signature. But there's a really big problem here already. The effects of both x and y are bound into the composed structure, regardless of which one's value is selected.

    whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
    whileA p step x = ifA (p x) (whileA p step <$> step x) (pure x)
    

    Well, ok, that seems like it'd be ok, except for the fact that it's an infinite loop because ifA will always execute both branches... Except it's not even that close. pure x has the type f a. whileA p step <$> step x has the type f (f a). This isn't even an infinite loop. It's a compile error. Let's try again..

    whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
    whileA p step x = ifA (p x) (whileA p step <*> step x) (pure x)
    

    Well shoot. Don't even get that far. whileA p step has the type a -> f a. If you try to use it as the first argument to <*>, it grabs the Applicative instance for the top type constructor, which is (->), not f. Yeah, this isn't gonna work either.

    In fact, the only function from my Monad examples that would work with the Applicative interface is notM. That particular function works just fine with only a Functor interface, in fact. The rest? They fail.

    Of course it's to be expected that you can write code using the Monad interface that you can't with the Applicative interface. It is strictly more powerful, after all. But what's interesting is what you lose. You lose the ability to compose functions that change what effects they have based on their input. That is, you lose the ability to write certain control-flow patterns that compose functions with types a -> f b.

    Turing-complete composition is exactly what makes the Monad interface interesting. If it didn't allow Turing-complete composition, it would be impossible for you, the programmer, to compose together IO actions in any particular control flow that wasn't nicely prepackaged for you. It was the fact that you can use the Monad primitives to express any control flow that made the IO type a feasible way to manage the IO problem in Haskell.

    Many more types than just IO have semantically valid Monad interfaces. And it happens that Haskell has the language facilities to abstract over the entire interface. Due to those factors, Monad is a valuable class to provide instances for, when possible. Doing so gets you access to all the existing abstract functionality provided for working with monadic types, regardless of what the concrete type is.

    So if Haskell programmers seem to always care about Monad instances for a type, it's because it's the most generically-useful instance that can be provided.

提交回复
热议问题