4 ms·
> For example, the continuation monad, which is just a library in Haskell, can't be implemented in most imperative languages without extending the language itse
by lojack 9y ago
> For example, the continuation monad, which is just a library in Haskell, can't be implemented in most imperative languages without extending the language itself (async in JS, C#, etc).
I don't know about other languages, but the continuation monad is pretty trivial to write using promises. These can either be provided natively or using a promise library. It's a little more verbose than Haskell, but it's actually a useful pattern in JS.
https://codepen.io/anon/pen/JpLLJR?editors=0011 https://codepen.io/anon/pen/JpLLJR?editors=0011
FWIW, I'm no JS expert, so its probably likely that there's a library out there that provide the functionality of applyAsync and composeAsync that I wrote.
- pka 9y agoOf course! When I said "it can't be implemented" I meant the logic that the continuation monad itself encapsulates, not async computations in general. For those you wouldn't even need promises, you could just use callbacks: fetchUser("url", (user) => { fetchAddress(user.address, (address) => { ... }); }); But this repetetive plumbing is exactly what the continuation monad abstracts away: do user <- fetchUser "url" address <- fetchAddress user.address ... So the point is that you can write your async code in the same way you'd write normal, sequential code (which is what the async extensions of JS/C# allow you to do).
- rbehrends 9y agoThis is a syntax thing only. You can do the same in Smalltalk with an array of blocks (which are closures). { [ user := url fetchUser ]. [ address := user fetchAddress ]. } and pass that array as an argument to whatever evaluation strategy you want. You can also do with with proper macros, e.g. in Nim (fictitious example, there's no actual `usingContinuations` macro): usingContinuations do: user = fetchUser("url") address = user.fetchAddress This is all a matter of syntax allowing you to write a sequence of code fragments in a readable fashion while allowing transformations on the underlying sequence. That said, outside of Haskell (or other functional languages emulating the approach) you'll encounter them fairly rarely in this particular form, because just allowing for a sequence of code fragments is a bit limiting if you can combine them in more general ways. For example, Smalltalk builds pretty much all control flow in what we'd call combinators (on closures and values) now and allows you to pretty much extend that arbitrarily. Not because Smalltalk does anything super-special here, but because it has a nice, concise syntax for expressing executable code fragments as values.
- marcosdumay 9y agoEvery difference between Turing complete languages is a "syntax only thing". A monad is basically an abstraction that encapsulates an evaluation strategy. You can certainly reimplement it on most functional languages with a syntax that is only a bit more verbose and error prone. That's not a win. Also, macros are an all powerful construction, with heavy implementation and maintainability costs. They certainly can be used as monads. That's also not a win.
- rbehrends 9y ago> Every difference between Turing complete languages is a "syntax only thing". I'm pretty sure you can't ignore language semantics here. > You can certainly reimplement it on most functional languages with a syntax that is only a bit more verbose and error prone. That's not a win. Note that I was giving examples from imperative languages; Haskell has the additional problem that it has to transform the do notation into what's essentially function composition; but function composition is already the natural denotational semantics of imperative code [1]. [1] https://en.wikipedia.org/wiki/Denotational_semantics#Denotational_semantics_of_state https://en.wikipedia.org/wiki/Denotational_semantics#Denotat...
- marcosdumay 9y ago> I'm pretty sure you can't ignore language semantics here. As long as the languages operate on the same virtual computer (same IO capacities), you can create the same semantics on any language. At the worst case, you can write an interpreter for any language on any other language. > but function composition is already the natural denotational semantics of imperative code Most high-level languages got some influence by Lisp-style functional programing, so they transform into function reduction/composition with some amount of naturality. The imperative languages are the ones with the less straight-forward transformation, while Lisp is basically alone at the other extreme. Pure function reduction/composition is a great representation for computer. It's simple to analyze, and cheap to represent. But I don't think it is that great for human consumption. Do notation is a bit more complex to represent, but often a lot more legible.
- lojack 9y agoEliminating the repetitive plumbing is a feature of haskell, not a feature of the monad. What the continuation monad gets you is a pipeline of asynchronous and synchronous operations. Whether or not something is asynchronous is abstracted away. Your example could be rewritten as: composeAsync([ fetchUser("url"), function(user) { return user.address }, fetchAddress, function(address) { return address.zipcode } ]).then(function(output) { document.write("Zipcode: " + output); }); In this example you don't need to care if fetchAddress is synchronous or asynchronous, it'll work either way. Its at least worth noting that one thing Haskell gives you is the ability to perform assignment within the async functions. This is also possible with Javascript, but a bit more intuitive in Haskell. Again, this is a feature of haskell, not a feature of monads. var outputs = {}; composeAsync([ fetchUser("url"), function(output) { outputs['user'] = output }, function() { return outputs.user.address }, fetchAddress, function(output) { outputs['address'] = output } ]).then(function(output) { ... }); composeAsync could also be rewritten to store this in an accumulator, making it look a little nicer.
- pka 9y agoThis is not as general as a monad though, it's just feeding the output of one function into the next. I'm sure you know this, but the thing that makes monads a more general solution is that (because bind depends on a value produced at runtime) one can dynamically alter control flow, i.e: fetch = do user <- fetchUser "url" if userEmpty user then do fetchUserDetails "details" fetch -- recurse else fetchAddress user For that to work, your functions inside the `composeAsync` array would need to able to return nested promises and at that point you've just reimplemented the continuation monad :)
- lojack 9y agoYeah, I guess I may have not completely implemented the continuation monad, but my point was precisely what you just said. That is, it's totally possible to implement continuation monads in vanilla javascript. It's not only possible, but its reasonably easy to do without heavy lifting.
- Rusky 9y agoThe reason async/await is implemented as a language extension in JS/C#/Kotlin/etc. is not because they're not powerful enough for a promise library, but because the language extension version composes with imperative control flow. Haskell has exactly the same problem- do-notation is nice, but not very different from just any of the sibling comments' tricks with things like arrays of closures. It doesn't let you write things like this: while some_condition() { let result = await some_async_operation() await something_else_async(result) } Instead you just use recursion and (lifted) higher order functions like elsewhere in Haskell. This is not very unusual there but it would be a pain in JS/C#/Kotlin/etc. which do use imperative control flow in idiomatic code.
- deleted 9y ago[deleted]