5 ms·
Don't monads in themselves have the same problem of introducing "coloring" (where each monad is a different color) , so that functions which work for one monad
by simiones 6y ago
Don't monads in themselves have the same problem of introducing "coloring" (where each monad is a different color) , so that functions which work for one monad won't work with easily compose with functions which work with another monad?
For example, isn't it true that you can't easily compose a function f :: a -> IO b with a function g :: b -> [c] (assuming you don't use unsafePerformIO, of course)?
Of course, there will be ways to do it (just as you can wrap a sync function in an async function, or .Wait() on an async function result to get a sync function), and Haskell's very high level of abstraction will make that wrapping easier than in a language like C#.
- klodolph 6y agoYou can, h :: a -> IO [c] h = liftM g . f Or fmap, or use various arrow operators, or <$>. I might describe it as “pathologically easy” to compose functions in Haskell, since it’s so much easier in Haskell than other languages (and mixing monads & pure code is still very easy).
- linkdd 6y ago> I might describe it as “pathologically easy” to compose functions in Haskell Maybe because "composing functions" is the whole point of category theory, monads and Haskell?
- klodolph 6y agoThe point of category theory is composing morphisms! Functions are morphisms in the Set category. Haskell has something which looks very similar to category theory, but the functions and types in Haskell do not form a category. I would call it “category theory inspired”.
- linkdd 6y agoThank you for the clarification. But still, since it's all about composition, the simplicity is a direct consequence IMHO.
- simiones 6y agoSure, but that's not a feature of Monads themselves, but a feature of the excellent generic programming support in Haskell, along with polymorphic return types that makes the boilerplate really easy. Still, there is a slight bit more to the story if you actually had to pass a [c] to another function ( you would have to liftM that function as well, I believe, and so on for every function in a chain).
- klodolph 6y agoSo I don’t think that this criticism is quite fair, and I’ll explain why. When you choose to make something implicit/easy in a language, it often comes with the cost of making other things explicit/hard. The best example of this, in my mind, is the choice to use Hindley-Milner style type inference (ML, Haskell) versus forward-only type inference (C, Java). HM style type inference comes with its advantages, but introduces limitations that affect polymorphism. In Haskell, you can’t automatically lift something into a Monad because this would require a type of ad-hoc polymorphism that is impossible with a coherent Hindley-Milner type of type inference system. However, that exact type inference system is what lets you just write “liftM”, and it automatically does the Right Thing based on the type of monad expected at the call site. I think ultimately—Haskell is in a sense an experiment to see if these alternative tradeoffs are viable, and it’s a massive success in that regard. (Also: you wouldn’t need to liftM every function in a chain. Just liftM the chain at the end.)
- 6gvONxR4sf7o 6y ago> Sure, but that's not a feature of Monads themselves, but a feature of the excellent generic programming support in Haskell This easiness comes from the existence of fmap, which is defined for any functor (monads are all functors). To instance it and get haskell's lovely syntax, any given "container" has to define its own fmap implementation. So it is a feature of monads. If you implement a monad in another language, you're still going to be defining fmap (or an equivalent), which lets you use this.
- pas 6y agoYes, they do. (That's why there's the monad transformer.) But this is fundamentally a very simple thing. If you use an async framework, you have to think inside that. It's just as fundamental as CPU architecture, or operating system. If you have an Intel CPU you need a corresponding motherboard. If you have Windows you need programs that use the Win32 API (or use the necessary translation layer, like WSL). Similarly, if you use - let's say - libuv in C, you can't just expect to call into the kernel and then merrily compose that with whatever libuv is doing. This "coloring" is a suddenly overhyped aspect of programming (software engineering). And of course fundamentally both are wrappable from the other, but it's good practice to don't mix sync/async libs and frameworks and components. (Or do it the right way, with separate threadpools, queues, pipes, or whatnot.)
- an_opabinia 6y ago> you can't just expect to call into the kernel and then merrily compose that with whatever libuv is doing. Quasar Fibers, which eliminated function coloring for Java async versus blocking IO, I wound up doing the equivalent of that in the Java ecosystem, and it works in production extremely well. Defeating the coloring problem makes gluing all sorts of really valuable stuff together much easier. In a world where you can’t just rewrite from scratch the things you’re gluing.
- panacpp 6y agohttps://web.archive.org/web/20170629202409/http://blog.paralleluniverse.co/2013/05/02/quasar-pulsar/ https://web.archive.org/web/20170629202409/http://blog.paral...
- Arnavion 6y agoThis kind of "transparent async" (like what golang also does) always has the danger of creating race-condition-style bugs, because some synchronous-looking piece of code doesn't run synchronously so some state at the beginning of the function has a different value at the end of the function. Have you had those kind of problems with your Java codebase?
- lmm 6y ago> Don't monads in themselves have the same problem of introducing "coloring" (where each monad is a different color) , so that functions which work for one monad won't work with easily compose with functions which work with another monad? I'd say that monads surface a problem that was already there, and give you a vocabulary to talk about it and potentially work with it. Composing effectful functions is still surprising even if you don't keep track of those effects in your type system (for example, combining async functions with mutable variables, or either with an Amb-style backtracking/nondeterminism function, or any of those with error propagation). But yeah, monads are an incomplete solution to the problem of effects, and when writing composable libraries you often want to abstract a bit further. In the case of Haskell-style languages we have MTL-style typeclasses, or Free coproducts, or various more experimental approaches.
- sullyj3 6y agoIt's not entirely clear what you want to happen here with `f` and `g`. It's trivially easy to apply `g` to the `b` in the result of f. You just use `fmap`. -- suppose we have some x :: a fmap g (f x) :: IO [c] Of course, you wouldn't want the [c] to be able to escape the IO container, because that would break referential transparency - you could no longer guarantee that a function returns the same result given the same arguments. Of course, that's no problem for manipulating the [c] value by passing it to pure code. It's true that "monad's don't compose" in the sense that you need to manually write monad transformers to correctly combine monadic effects, whereas functors compose automatically. However, that's not the same thing as saying it's difficult to use functions which happen to return different monadic results together, if the types match.
- marcosdumay 6y agoOf course the functionality that abstracts over coloring has the same problems coloring has on any other language. If you don't want coloring to happen on your code, you shouldn't declare it by using a monad.
- 6gvONxR4sf7o 6y agoThe idea is that your individual monad has to say how to compose those functions (and package up values). The type signature of bind and return are is (>>=) :: m a -> ( a -> m b) -> m b return :: a -> m a In your case f gives you an IO b, which is the first argument to bind (your m a). Then g's output needs packing up, so you use return to take [c] to m [c], giving `return . g :: b -> m [c]`, which will be the second argument to bind (your a -> m b). That gives you `composed_function x = (f x) >>= (return . g)`, which a "do" block makes potentially clearer :`do {y <- f x; return (g y)}`
- 6gvONxR4sf7o 6y ago(Too late to edit) I'm an idiot. All monads are functors, for which you can just use fmap. `composed_function x = fmap g (f x)` which you can also write as `composed_function x = g <$> f x`, which I now realize someone else has already said. Oops. Anyways the idea is that each sort of "container" defines how to apply functions to it.