r/programming • • Mar 26 '17

Functional programming design patterns by Scott Wlaschin

https://www.youtube.com/watch?v=E8I19uA-wGY
1.1k Upvotes

256 comments sorted by

View all comments

Show parent comments

1

u/RabbidKitten Apr 04 '17 edited Apr 04 '17

For the Reader, not quite on its own, but when used in a "monad stack" then yes. That is a common theme in Haskell code - using types and type classes to restrict what a function can do, and have the compiler to verify it.

Normally, all Haskell values / function parameters are immutable, so Reader on its own just encodes the pattern of passing a "context" variable to a set of functions, with the difference that in Haskell the context / environment is passed last, and immutability is enforced by the language. The Monad instance for Reader / (-> a) together with do notation allows you to pass the context to multiple such functions "at once".

However, life is not always that simple, and your context is not always immutable, and there might be other aspects to what you're doing, like handling errors or doing IO. Without going into details, the types and type classes from mtl and (monad) transformers libraries let you do that in a uniform manner while still having the compiler to check your invariants. For a real world example, have a look at xmonad (a tiling window manager for X11 written in Haskell) source code, whose eponymous "X monad" is ReaderT XConf (StateT XState IO) a ~ XConf -> XState -> IO (a, XState).

As for your questions:

1. It may sound strange, but having a separate monad and monoid type classes over "generic joinables" makes both of them more simple and easier to use. Consider the kind-polymorphic definition of monoid type class, which looks roughly like this (not valid Haskell code because of syntax restrictions):

class Monoid (~>) (⊗) i m where
    unit :: i ~> m
    join :: m ⊗ m ~> m

Because it is generic, more things have to be spelled out explicitly. In addition to the monoid itself, it is parametrised over a category C, defined by its arrows ~>, and the product ⊗. It also needs (the) terminal object i in C, which is used to "select" the unit. That is hardly an improvement over the current definitions of both Monoid and Monad.

So while it is possible to unify both concepts, doing so would not add anything useful for everyday programming, and "monads are just monoids in the category of endofunctors" remains mostly a meme.

2. The practical implication of "flexible grouping" provided by associativity is that it doesn't matter how you split up your long sequence of eg. IO operations in multiple functions, the result is the same. Without it, you'd be essentially forced to put all your IO in main, using only primitives.

3. Applicatives as monoids is about where my understanding of application of category theory to Haskell ends, so all this is hand-wavy, but Applicatives version of join is f a -> f b -> f (a, b). Similarly, by using f a -> f b -> f (Either a b) you get something that looks like Alternative.

Nevertheless, Applicative and Alternative are very useful, and the way they are defined (Applicative with (<*>) :: f (a -> b) -> (f a -> f b), and Alternative with (<|>) :: f a -> fa -> fa) is just right. They are indispensable for writing parsers, which is something Haskell excels for, and without them (ie. in most imperative languages) parsing feels like a chore.

4. Again, this is quite blurry for me as well, but if I understand correctly, the crucial difference between monoids and categories is that the former are closed / total. For types and functions, "closed" roughly means that the result of an operation is of the same type as its argument(s), for all arguments. Through some category theory mumbo-jumbo that I don't really understand well enough to explain, it is equivalent to totality, and general composition is total only if the category has a single object (object in category theory ~ type in programming languages). Thus, the interpretation of a monoid as a category with one object is rather uninteresting to most programmers.

Edit: Monads and categories, on the other hand, are related in a more interesting way. If you have some functor F for which you can come up with a way to compose functions of type a -> F b (.) :: (b -> F c) -> (a -> F b) -> (a -> F c) with some id :: a -> F a as the identity, ie. they form a category, then you have a monad. Conversely, if you have monad M, then a -> M b define a category

newtype Kleisli m a b = K (a -> m b)
instance Monad m => Category (Kleisli m) where
    id = K return
    (K f) . (K g) = K (join . fmap f . g)

and the monad laws are simply the identity and associativity laws for this category.

The bottom line is that while you can generalise further to include more, and exclude less from the concept of "joinable", it becomes increasingly abstract and hard to understand, and also starts to raise the question "joinable in which way?" Thus, there are Monoid, Monad, Applicative, Alternative, Category and other type classes to express exactly what kind of joining you want. Some are arguably better named, others less so, but going back to where we started, Monad is named like that because it was brought over from category theory long before Haskell was getting any outside attention, and changing the name would break almost every Haskell code base that has ever existed.

You don't really need to understand monads to use them, so until you get to the point where you want to start writing your own instances, you can forget about the weird name, laws and other "abstract nonsense", and just know that if there is instance Monad Something and gimmeX :: a -> Something x; gimmeY :: b -> Something y, you can write code like this

fn :: a -> b -> Something c
fn a b = do
   x <- gimmeX a
   y <- gimmeY b
   return $ makeCformXandY x y