6 ms·
> 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 bi
by 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.
- thsealienbstrds 7y ago
- 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!