6 ms·
An important realization that I had was that monads/functors/applicatives aren't patterns in the sense of design patterns. You don't solve a singular problem wi
by _hardwaregeek 7y ago
An important realization that I had was that monads/functors/applicatives aren't patterns in the sense of design patterns. You don't solve a singular problem with a monad. With something like a strategy pattern you have a concrete problem: how do I select different potential algorithms? Monads don't have a specific problem that they solve. Any attempt to motivate monads in such a manner falls flat because the problem is either too general to be motivating or too specific to apply to monads as a whole.
Instead, functors/monads/applicatives are more like a technique that can be used to solve a wide variety of problems that all coincidentally use the same function signature. And therefore, it's perfectly acceptable to say "I know how monads work with Maybe and List, but not Reader" Because fundamentally, how a Reader implements bind is in no way related to how Maybe or List implement bind.
- mavelikara 7y ago> And therefore, it's perfectly acceptable to say "I know how monads work with Maybe and List, but not Reader" Because fundamentally, how a Reader implements bind is in no way related to how Maybe or List implement bind. If this is indeed true, what is the point of learning these "patterns"? From a mechanical understanding of the signature of bind and unit, you'd arrive at more sophisticated signatures, say filterM, but an understanding of what it does will still need an understanding of the specific implementation of the Monad instance it is being applied on. That sounds like a leaky abstraction. If so, why bother?
- TheAsprngHacker 7y agoTo my understanding, although the behaviors of filterM, etc. differ depending on the monad instance, as long as the monad instances follow the laws, those functions like filterM have predictable behavior. It's a consequence of "theorems for free" / parametricity.
- bad_user 7y agoThe point is that Monad, Applicative and Functor are well defined interfaces with laws (properties) you can count on. They are in fact much better, more precisely defined than classic design patterns. And in expressive programming languages (that support higher kinded types or that at least let you encode such types) you can also describe generic code that works over any applicative or monadic type. Having reusable functions that work just as well on lists, maybe/option, reader, io / promise or what have you means these type classes do a very good job at abstracting over data types. These are the purest forms of abstraction. And you can get syntactic sugar from the language as well. For example "for comprehensions" in Python work via the iterator/enumerable protocol, but that's super limiting. Haskell's "do notation" or Scala's "for comprehensions" work on any monad instead, being much more powerful and reusable. Unfortunately it takes an expressive language to understand and work with these abstractions comfortably. You won't grok monads in Go or Java.
- mlevental 7y ago>The point is that Monad, Applicative and Functor are well defined interfaces with laws (properties) you can count on. except you can't because you can have unlawful implementations.
- mnsc 7y ago> Having reusable functions that work just as well on lists, maybe/option, reader, io / promise or what have you means these type classes do a very good job at abstracting over data types. These are the purest forms of abstraction. This is where I get kinda lost. Can you give an example of such a reusable function that works for all these things which does something valuable?
- lmm 7y agotraverse (for sequence-like structures) or cataM (for tree-like structures). They let you walk a data structure with an applicative or monadic operation and compose the monadicity in the way that obviously makes sense.
- thsealienbstrds 7y agoThanks for this short explanation. The benefits of these patterns were not clear to me just from reading the article. Questiob: is the idea always to think in terms of types as groups of values + some operations on those types such that <insert properties about resulting groups here>?
- lmm 7y agoIt's important not to think of types as just sets of values, because types are more semantic than that; two quite different types might have the same underlying values but they're different types because you use them to mean something different. A typeclass is one or two levels higher than a type, and yes, for a type to conform to a typeclass usually means that that type supports particular operations in a way that conforms to certain laws (e.g. a type `a` will have an instance of the monoid typeclass if there is a definition of `+` for `a` that is associative and a value `0` of type `a` that behaves as left and right identity wrt this `+`). I'm not sure how important that is when working practically with them though. I tend to think of typeclasses as something like categories of types that behave similarly.
- pyrale 7y agoYou may not know how it is implemented, but because you have used other monads, you will know how to use it and how to compose it.
- willtim 7y agoMonads let you redefine what a statement is. This is very powerful and makes Haskell my favourite language for imperative programming. There's no need to modify the compiler for e.g. Async/Await, it can be a library. A Monad instance has a set of properties or laws that it obeys. This gives you a general understanding of what e.g. filterM will do, i.e apply an effectful filter function such that the effects are sequenced in the order of the input list.
- kccqzy 7y agoYour example, filterM has the type filterM :: Applicative m => (a -> m Bool) -> [a] -> m [a] from this I can tell you conclusively what it does: it takes a monadic function that returns a Bool after performing some "action" as well as a list of values; it then performs this action on each element and look at whether the result is True or False; when True, keep it otherwise discard it. Now this is already a lot of information on what filterM does. But to use it in an action piece of code, you need to supply your own understanding of what the "action" is in this context. It does not matter whether or not you understand the implementation details of the specific Monad instance. You just need to understand its behavior: with the Reader monad, you know the action in question is just reading a piece of value from a "hidden" environment, so your filtering function has access to this environment when it determines whether or not to keep or retain an element; with the State monad, you know the action in question can possibly modify this environment as well; with the Maybe monad, you know the filtering action can say I don't know and that would result in the entire result to be Nothing as well. Your argument of functions like filterM being a leaky abstraction is akin to saying, a Java interface is a leaky abstraction because you can't instantiate an interface (you need a class) so you need to understand both the interface as well as the class being used that implements the interface. Instead, think about it, it's just how the nature of combining orthogonal (de-coupled) things requires you to have an understanding of both of the pieces you are combining.
- dllthomas 7y ago"Conclusively" is probably a little strong. You're relying not just on the type, but also the name - the type alone isn't enough here, even without resorting to bottom, and even when requiring every piece be meaningfully used. For instance, with the same type we can write: doubleIf :: Applicative m => (a -> m Bool) -> [a] -> m [a] doubleIf f values = case values of [] -> pure [] x:xs -> prepend x <$> f x <*> doubleIf f xs where prepend :: a -> Bool -> [a] -> [a] prepend x True xs = x:x:xs prepend x False xs = x:xs
- zeugmasyllepsis 7y agoMaybe a simpler way to illustrate this is two different functions built off of filter: `keep`: Given a predicate and a collection, filter the collection to retain only items for which predicate evaluates to true. `discard`: Given a predicate and a collection, filter the collection to discard items for which the predicate evaluates to false. These are two very reasonable functions that would have the exact same signature, would be appropriate for the same arguments, but have two different behaviors!
- lonelappde 7y agoYour description means they are design patterns: a technique to solve a problem that appears repeatedly. What trips people up is that monad is a 2nd-level pattern, as can be seen in the type. 'List a' is-a pattern for nondeterminism. 'Maybe a' is-a pattern for handling failure. Instancing 'm a' is a pattern for 'sequencing effects with branching' and also other patterns for various other applications.
- LeanderK 7y agoI still wouldn't call them design patterns. At least in haskell, they are not used like design patterns in OOP languages. They are more inherent properties of certain types (Maybe a "is" a monad etc.) and the instances are provided regardless of whether they are needed to solve a problem or not. In OOP design patterns not only solve a specific problem, they are only used to solve the problem (no need to sprinkle the visitor-pattern all over your classes just because it's possible). Monad/Functor etc. instances are defined where they are possible. I view them as making inherent properties visible and explicit ("this action obeys these laws"). They are used to solve problems, but mainly by making abstract properties directly visible and reminding the programmer that he can just view them as this abstract concept and reuse existing machinery.