7 ms·
I was raised on OOP, and at my ripe old age I really want to grok monads. This video did me a great service in that direction.
by zamalek 5y ago
I was raised on OOP, and at my ripe old age I really want to grok monads. This video did me a great service in that direction.
- ipnon 5y agoYou've probably been using IO monads as long as you've been programming: [Unix pipes as IO monads](https://www.okmij.org/ftp/Computation/monadic-shell.html https://www.okmij.org/ftp/Computation/monadic-shell.html)
- brigandish 5y ago> cat v | cmd is cmd < v I have never seen an `is` operator in a shell so my understanding stopped right there, which is a pity as I felt I was getting somewhere.
- jcranmer 5y agoWhat it's saying is that the shell command `cat v | cmd` is equivalent to `cmd < v`--if you look carefully, the 'is' is in a different typeface (a serif font) than the commands (monospace).
- brigandish 5y agoAh, my mistake (or my poor, abused laptop leering eyes). Thanks, I thought I was being introduced to some abstruse shell programming on top of monads!
- ipnon 5y agoThe monad as used in Haskell is really just a functional replacement for the semicolon: `doFoo(); logBar(); requestBaz()` in Haskell is `doFoo >> logBar >> requestBaz`. If you want to get really fancy you can pipe the inputs: `doFoo | logBar | requestBaz` becomes `doFoo >>= logBar >>= requestBaz`. Haskell suffers from the fact that endless mathematical analysis is enjoyable to a certain breed of programmer known as the "mathematician".
- tremon 5y agoWhat you're saying applies to the IO Monad only, using the IO side-effect for sequencing purposes. I'm not sure is applies to the myriad of other monads available in Haskell.
- _y5hn 5y agoIt's not that far off, if you think of side-effects / any magical processing happening at evaluation per bind-function (semi-colon ;). It's so because monads follow the exact same pattern by definition, but the different types of monads just inject some additional behaviour and constraints as well. Different patterns from this are no longer monadic. The "magic" part of this is why different types of monads don't compose well. Of course, this is overly simplified but might help for understanding.
- usrbinbash 5y ago> `cat v | cmd` is equivalent to `cmd < v` https://askubuntu.com/questions/172982/what-is-the-difference-between-redirection-and-pipe https://askubuntu.com/questions/172982/what-is-the-differenc...
- mrslave 5y agoHaskell for Imperative Programmers [0] is the best material I've come across recently for Haskell beginners. [0] https://www.youtube.com/playlist?list=PLe7Ei6viL6jGp1Rfu0dil1JH1SHk9bgDV https://www.youtube.com/playlist?list=PLe7Ei6viL6jGp1Rfu0dil...
- bedobi 5y agoShameless plug for something I wrote :P # Lambdas, Functions, Functors and Monads Even most OOP programmers are now familiar with map and, having used it quite a bit, have an intuitive understanding of what any given call to map will do. Does anyone know a bit more about map, what it is and where it came from? ## Category Theory A lot of Functional Programming derives from a field of Mathematics called Category Theory. Very briefly, Category Theory is the study of functions, where "function" refers to a process that associates to each element of a set X a single element of a set Y. Pure functions have only inputs and outputs- no access to anything other than what was sent in, and no ability to mutate what was sent in or generate other side effects. Now, I suck at math, but fortunately we don't need to know math to learn more about concepts programming languages have borrowed from Category Theory. Learning about it can make us better programmers if we go from an intuitive, but maybe a bit vague, understanding of things like map to fully understanding them! ## Functor (map) A *Functor* is anything that can be mapped over. Map, the defining function of a Functor, is quite simple. interface Functor<A> { fun <B> map (function: (A) -> B) : Functor<B> } This just says > Given a Functor of A, when map is called with a function of A to B, return a Functor of B ### An example This is all just a bunch of mumbo jumbo, but in our day to day programming, Functors (or potential Functors) are everywhere! Lists are a great candidate for a concrete example. class BoringList<A>(val backingList : List<A>) { } The above class is just a wrapper (with no added behavior) around a list. Let's make it do something interesting by declaring it a Functor. class InterestingList<A>(val backingList : List<A>) : Functor<A> { override fun <B> map(function: (A) -> B): InterestingList<B> { //implement - DON'T CHEAT by simply calling map on backingList } } fun main(args: Array<String>) { //experiment } Example implementation (very imperative, but that's ok) class InterestingList<A>(val backingList : List<A>) : Functor<A> { override fun <B> map(function: (A) -> B): InterestingList<B> { //the current InterestingList is of type A but we need to return //a InterestingList of type B, so first, make a new backingList //of type B val transformedList = ArrayList<B>() for (item in backingList) { //apply the provided function to each item val transformedItem = function(item) //add them to the transformedList of type B transformedList.add(transformedItem) } //return an InterestingList of type B return InterestingList(transformedList) } } fun main(args: Array<String>) { val interestingList = InterestingList(listOf("foo", "bar")) //when calling map on InterestingList<String> with a function: (String) -> Int, we get InterestingList<Int> val mappedInterestingList = interestingList.map { string -> string.length } } ### Other examples Now, we've used List as an example, but remember: Functors (or potential Functors) are everywhere! It's easy to think of Functors as "containers", but it's more useful to think of them as a "computational context", since eg Options and Futures can also have map. * The Functor of List applies the function to all elements in the list * The Functor of Option applies the function if it's non-empty. (or rather, any function applied to Nothing is still Nothing) * The Functor of Future applies the function to a result once a result is available. ### Functor laws Now, can someone think of a few important things to keep in mind when implementing map? Eg, what if your implementation of map for a List always returned an empty list, or a list twice the size of the given list? Or what if it mutated the items it mapped over, or called a bunch of other god knows what functions? That would be not so good! So when implementing Functors you actually have to observe the Functor Laws. There is nothing mysterious about these laws; their role is to guarantee map behaves sanely and actually performs a mapping operation (as opposed to some other nonsense). The first law is: map id = id id is the identity function, which returns its argument unaltered. The first law states that mapping id over a functorial value must return the functorial value unchanged. Next, the second law: map (g . f) = map g . map f It states that it should not matter whether we map a composed function or first map one function and then the other (assuming the application order remains the same in both cases). ## Monad (flatMap) A *Monad* is anything that can be flatMapped over. Sounds scary! It's actually surprisingly easy to understand, but can be a little more complex to implement. Don't feel bad if you don't get it right! FlatMap, the defining function of a Monad, is quite simple. interface Monad<A> { fun <B> flatMap (function: (A) -> Monad<B>) : Monad<B> } This just says > Given a Monad of A, when flatMap is called with a function of A to Monad B, return a Monad of B If you recall Functor interface Functor<A> { fun <B> map (function: (A) -> B) : Functor<B> } Which just says > Given a Functor of A, when map is called with a function of A to B, return a Functor of B The two are EXACTLY the same except instead of a function of A to B, it's a function of A to Monad B. ### An example Let's return to the concrete example of the list: if you call map on your list with a function of A to List B, what will you end up with? You would end up with a list of lists! And all flatMap does is "flatten" that into a single list. interface Monad<A> { fun <B> flatMap (function: (A) -> Monad<B>) : Monad<B> //yes! fun <B> flatMap (function: (A) -> Monad<B>) : Monad<Monad<B>> //no! } Implementing is left to the overachievers. ### Other examples Remember: just like Functors, Monads (or potential Monads) are everywhere! * The Monad of List flattens lists of lists. * The Monad of Option allows chaining many potentially absent things together, short circuits if any of them are empty, and returns a flattened end result. * The Monad of Future allows chaining together computations that don't have a result yet, and returns a flattened end result. ### The Monad laws Just like when implementing map for Functors the implementation must satisfy the Functor Laws, when implementing flatMap for Monads, the implementation must satisfy the Monad Laws. I will not get into them here, but for anyone who's interested this is a great resource https://en.wikibooks.org/wiki/Haskell/Understanding_monads https://en.wikibooks.org/wiki/Haskell/Understanding_monads ## A missed opportunity As we've seen, map and flatMap really shouldn't be these random functions that sometimes get added to things and sometimes not. Functional programming languages usually have an interface Functor, and an interface Monad etc which all classes that implement map and flatMap extend, and there's no reason why it shouldn't be like that in Java or Kotlin as well. Unfortunately, neither Java nor Kotlin do this, which is kind of a missed opportunity. ## But why? Most of us instinctively reached for looping language constructs like `for` and `while`. So since we're so used to that, where's the benefit in all this new map, flatMap etc nonsense? It's difficult to understand, hard to read etc etc. A few differences could be pointed out. ### An example //this looks fine but will actually blow up, can you tell why? int[] array = {1, 2, 3}; int[] doubled = int[array.size]; for (int i = 0; i <= array.size; i++){ doubled[i] = array[i] \* 2; } //even if it didn't blow up, there's still a lot of noise about *how* to //double that is completely besides the point of *what* we actually want, //which is simply to multiply the items by 2 #### Imperative vs Declarative loops describe in detail how to do something while map hides implementation details and let's you focus on what to do. And since describing in detail how to do something isn't required, the risk of bugs in the how part is completely eliminated. #### Stateful vs Stateless loops are literally stateful operations where we're, well, looping through a collection item by item (in a certain order) using something to keep track of how far we've looped. Sometimes the state matters to what's happening inside the loop, eg when we find what we're looking for and return early. Mapping operations have no such state, so the risk of bugs due to state are completely eliminated. #### Meaningless vs Meaningful loops have no real meaning. Eg, it's possible to loop through things and do nothing, mutate the items as you loop through them, mutate the collection you're looping through, return something from an enclosing method... Loops can do anything. Map on the other hand is a specific thing, has a specific use case, does no more and no less, makes guarantees as to what it does and doesn't do etc etc. This allows us to reason about code more effectively than with loops where we have to play compiler and debugger and keep a bunch of state in mind all the while trying to understand what's actually being done inside. These are just a few examples, I'm sure more could be thought of!
- exdsq 5y agoA monad is just a monoid in the category of endofunctors
- cryptonector 5y agoso what's the big deal?
- js8 5y agoI think monads are what we would ordinarily call "environment" or "context" of execution. Monad just describes how to compose the interactions with the context. Usually this is some serialization of these interactions but can be other things. IO monad is where the environment (context of execution) is just your entire operating system. In imperative languages, this is a normal state of affairs, but functional programmers view that with similar suspicion that most programmers of today view global variables. That's why in general "different monads don't compose", because it's like trying to compose two different environments (as we know, for example, porting between different OSes is not trivial). You can define it (with monad transformers) but it's often a lot of work.
- hither_shores 5y agoMore precisely: the composition of two different monads isn't always a monad. `[IO Int]` is a perfectly sensible type, you just can't flatten a `[IO [IO Int]]`.
- tromp 5y agoIsn't that what joinMMT [1] is for ? [1] https://hackage.haskell.org/package/TypeCompose-0.9.14/docs/Control-Compose.html#v:joinMMT https://hackage.haskell.org/package/TypeCompose-0.9.14/docs/...
- tome 5y agoThat particular function won't work because IO is not Traversable.
- js8 5y agoI think (I am not sure) that you cannot define monad composition (transformer) with IO because IO is purposely open-world (so that Haskell can use C libraries and OS calls) and as such is not entirely formally defined. If we defined it as a closed-world, i.e. these are the side effects that you can do with IO and nothing else, then it might become possible to compose with other monads.
- grumpyprole 5y agoOne advantage of monads is that they can communicate via the type that a function is not pure. I remember a serious bug in a trading system (distributed over multiple time zones), that was caused by Java's toString reading the system locale from the OS. In Haskell, such a function would have a type that makes it clear the function is not pure, e.g. toString : Date -> IO String. Monads are not the only way of tracking effects in the type, but they are one way. For example, Java has "checked exceptions", which also represent an effect in the type system. Monads also allow one to assemble a computational ready for execution later. One can therefore write an Async library, without needing baked-in language support.
- cryptonector 5y agoNote that using a monad does not imply the code is impure. Using the IO monad does.
- grumpyprole 5y agoYes and some would argue that all Haskell functions are pure even those that return computations in the IO monad. One can use referential transparency to reason about the IO monad values also.
- cryptonector 5y agoSure, you can treat I/O functions as World -> World functions, but unlike other pure functions, the compiler can't compute them at compile time when they are applied to constant inputs. A program consisting entirely of pure function applications to constants can be evaluated at compile-time to just the constant(s) it would output, while a program that calls I/O functions cannot be (assuming those calls to I/O functions are not dead code).
- ModernMech 5y agoIf you've ever used a language like Rust that has sum types, and you've gotten a Option<T> from a function and wanted to put it into some other function that takes a T, then you have a good intuition for what a monad is. Monads unwrap the T from the sum type so you can chain those kinds of function calls. This is relevant for IO because it can maybe fail.
- skavi 5y agoI think the more important part in the case of the IO monad is not that it might fail, but that it’s a side effect in a lazy language. Not super sure though. Haven’t used much Haskell.
- kaba0 5y agoLaziness is besides the point, scala (and pretty much every language) can do laziness without monads, it is as simple as if private variable == null, then initialize, and then return that variable on every getter call. It is “needed” due to pureness - without haskell’s unsafe, there is no other way to do side-effects[1]. But you are otherwise right. The reason it is beneficial in FP languages is that the optimizer can reason very well about these pure expressions. [1] though execution itself can be considered a side-effect, and haskell code can throw exceptions, so it is more like compartmentalizing the majority of effects
- dllthomas 5y agoOn the contrary, purity is needed because of laziness! https://www.microsoft.com/en-us/research/publication/wearing-hair-shirt-retrospective-haskell-2003/ https://www.microsoft.com/en-us/research/publication/wearing... Not, mind you, merely the possibility of laziness; as you say, you can do that without monads or purity. But with pervasive laziness it becomes difficult to reason about the precise order in which thunks will be evaluated and be sure that (say) the warning will be printed before the missiles are fired instead of after. With purity it (mostly) doesn't matter the precise evaluation order, so (again, in the setting of pervasive laziness) we want that wherever we can have it! But at some point we need to actually fire those missiles and print those messages. The Monad interface for IO, in chaining together effectful computations with bind, makes the input of later computations depend on the output of earlier computations such that the regular data dependencies between the thunks ensure that things happen in the order we asked for.
- kaba0 5y agoI gave a try explaining it the easiest way I could here: https://news.ycombinator.com/item?id=28844533 https://news.ycombinator.com/item?id=28844533
- cryptonector 5y ago> To understand Monads, we have to first understand Functors, Monoids and the Applicative type classes (type classes are more or less the same as interfaces in Java, C++ and the like). The parenthetical is extremely important too. What looks like a function call in Haskell is dispatched based on the types of its arguments (yes, functions take just one argument, but be that as it may). This is important to understand. The "methods" available depend on the types of the values being passed to them.
- kaba0 5y agoI tried to write Java-style method invocation wherever possible.
- cryptonector 5y agoRight, but when reading Haskell, one has to know this. It's critical.