7 ms·
This is a syntax thing only. You can do the same in Smalltalk with an array of blocks (which are closures). { [ user := url fetchUser ]. [ address :=
by rbehrends 9y ago
This 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.
- rbehrends 9y ago> 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. This is fairly tautological and would make semantics meaningless. I'm not talking about the universality of Turing-complete languages, but about the semantics of language constructs. > 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. This has nothing to do with what I said, so I'm not sure what your point is?
- KirinDave 9y ago"Do notation" is a syntax thing. What's more fundamental is modeling every computation in your language with these chainable algebraic constructs in such a way that they're always composable (when of the same computation type) and combinable (albeit with complexity, as in mtl). Monads are just a modeling technique for this, and when laid bare without do notation, they're neither impressive nor surprising. However, it's worth noting that there are some very neat properties with generic monads (something that Haskell does very well because of how typeclasses can model constraints). It allows for reuse across surprising axes. Let's extend the prior example: loadUser :: (MonadIO m) => UserRef -> m User loadUser url = do userRecord <- liftIO $ fetchAndParseUser url -- One function for compactness emailAddress <- validateEmailAddress userRecord pure $ User (publicName userRecord) emailAddress This simplified, but what's really nice about this specific construction is that we've placed a constraint on how we load UserRefs (we need to pull data from a url). In most cases I'd expect that this would be some variant of ExceptT IO * or MaybeT *. But it also could be something more sophisticated. I've had a case where I realized that I was using a dowdy 2008 approach to software assuming users would have only one identity record, but I wrote a load function a lot like this. I needed to change how I loaded users since users would often have multiple user records (i.e., they created and linked accounts to one another). I had to change my fetch and parsers (naturally, I chose a new endpoint). However, I started calling my load user function with ListT IO [1]. I barely had to change logic (mostly pattern matching on the edges) to cope with this change. But you might also imagine that the computation includes async properties, or validating properties, or whatever. Because we can generically address ALL kinds of computations and even combinations of computations, we can freely swap them around as we see fit without substantial modification. That kind of flexibility is fundamental to how these languages work in tandem with monadic computation, and it's very powerful. P.S., I recognize that you might argue a value encoding could capture this in Smalltalk, and I agree. That's often how people implement this under the covers before they optimize it. What's worth noting is that without good type checking, interlocking more than a few of these constraints correctly is very difficult, which is why this is much more feasible to do in Haskell. [1]: As an aside, I don't use ExceptT for runtime exceptions. IO already covers that and I think it's a bit of a fantasy to pretend I can do better. Also, I'm aware that ListT is a slightly troubled cousin in the world of monad transformers, but I was happy to use it here, none of its downsides really came into play.
- tathougies 9y agoCan you extend this approach to delimited continuations?
- rbehrends 9y agoDepends on what you mean precisely by "extend to", but generally this is mostly a question of whether the underlying execution model supports them. "Code as values" is not exactly a particularly complicated thing (though arriving at an efficient implementation can be).
- tathougies 9y agoBut Haskell’s execution model has no underlying support for continuations, and yet there are perfectly efficient implementations. You actually can not implement delimited continuations in languages like c++, javascript, or go, unless you write your own execution environment at which point Id argue you’ve left the realm of the language. In c++ at least, such an implementation would certainly use undefined behavior
- rbehrends 9y ago> But Haskell’s execution model has no underlying support for continuations, and yet there are perfectly efficient implementations. What you need is a way to capture and manipulate stack frames at a very basic level. Haskell cannot magically avoid that. As I recall (though it has been a while), Control.Monad.CC reifies stack frames explicitly. Monads function in this context as a very basic metaprogramming technique (or as some sort of AOP, if you want to look at it this way). > In c++ at least, such an implementation would certainly use undefined behavior This is what I mean by support in the execution model. You do need to have access to stack frames, and C++ doesn't permit that. There are workarounds [1], but they generally require non-trivial metaprogamming, due to the insane complexity of C++. You don't have that problem with, say, Smalltalk, as most Smalltalk VMs gives you the necessary functionality. For example, one of the original continuation-based web frame works, Seaside, was all Smalltalk [2]. [1] http://www.filpizlo.com/papers/baker-ccpe09-accurate.pdf; http://www.filpizlo.com/papers/baker-ccpe09-accurate.pdf; the context here is GC, but the underlying problem is similar, access to stack frames (in this context, for root scanning). [2] http://seaside.st/about/examples http://seaside.st/about/examples