Hacker News new | ask | show | jobs
by harpocrates 3016 days ago
You've changed my mind on adding special syntax for `async`, generators, and results (`?`). I previously thought it was a big mistake given that these all generalize as monads (granted, there is some ground to be covered before such an abstraction would fit in Rust).

What I hadn't considered: specialized syntax leads to better error messages for the most common cases. That's probably quite a good thing, especially since descriptive error messages make me much more productive. (Not to mention that specialized syntax also acts as a type annotation of sorts too; a common problem I have in Haskell is figuring out _which_ monad a certain `do` block is using.)

2 comments

> (granted, there is some ground to be covered before such an abstraction would fit in Rust)

I would claim that such an abstraction is fundamentally incompatible with Rust.

Not only does Rust lack the means to write a Monad trait on which to build do-notation, but even given HKT there's no single type signature that the various monad instances would fit. `Result` and `Option` are type constructors while `Iterator` and `Future` are generic traits; some instances require `Fn` or `FnMut` while others require `FnOnce`.

And even assuming those problems could be solved, the way do-notation interacts with control flow is not composable with idiomatic Rust. Nested closures prevent the use of return/break/continue and imperative loops; Haskell doesn't have those features so people just lift their functional counterparts into monads. Monad transformer stacks make the situation even worse- they are a pain to compose even with each other, let alone imperative code.

If you want a unifying mechanism for this stuff in Rust it's gonna need to be fundamentally more powerful than monads. Scoped continuations, maybe? Certainly nothing that looks like >>=.

HKT would somewhat ease the type signature question; as I understand it in the rust community being able to parametrize over ownership/mutability is the big selling point that gets discussed. You could instead have a single Fn<Foo>, and stuff may require Fn<mut> or Fn<const>, rather than a separate FnMut. There's no keyword now, but owned could be part of that dance too. There's obviously the question of how to deal with backwards compatibility though.

There are more convenient ways of composing monads than monad transformers; extensible effects are a lot easier to work with.

Ulttimately I agree though, monads are not the right abstraction for a systems language. I'm quite happy using them in languages like Haskell and OCaml, where you've not only got a garbage collector solving the tricky ownership qustions for you, it's also faster than it has any right to be, so you can basically ignore the overhead of heap allocating a closure.

They become much less attractive when you're in a problem domain where you actually want to worry about fiddly details around memory allocation. This is much smaller space than people think it is, but it's what Rust is for.

Algebraic effects are new hotness in FP research. They are easier to compose than monads and they can express things like async/await naturally. However, I'm not sure this approach works as intended with imperative programs where code will have (side) effects sprinkled everywhere. And there is no GC in rust
I'm a fan of effects, and I think they would work great for Rust. (I/O just probably wouldn't be one of them, or else it would be "on by default" like `Sized`.)

I'm not sure how they'd give you a unified mechanism to implement these kinds of things, though. The Monad typeclass lets you implement new instances in libraries. Is there any work on defining effects like async/await in libraries? If so I imagine they'd still have to use something like continuations.

You don't need do-notation to use Monads, that's just useful for building up a lazy-evaluated data-structure describing what IO to perform.

Java and Javascript, for example, have been gradually introducing monadic concepts into their ecosystems. Java collections and Futures grow 'of' and 'flatMap', JS Promises aren't quite purely monadic but have 'resolve' and 'then'. So it's perfectly possible for an imperative language to use monadic features to implement async operations.

But much as I enjoy using JS Promises (really!) I prefer ES6 async/await, even though it's just syntactic sugar. Not least because even fairly straightforward-looking async/await code can desugar to something you wouldn't want to try to read by hand.

I'm... not sure what you're getting at here. Rust is chock-full of "monadic concepts" like that, including the entire sets of `Iterator` and `Future` combinators. (Note that even without do-notation these have the composability problems I mention- you can't, for example, `break` out of a loop from within a `.then` callback; you have to break the loop down and reimplement it yourself with recursion. This is what the async/await "sugar" helps with- makes normal control structures compose with async code.)

What I'm talking about is a single Monad abstraction to tie these all together. I mention do-notation because it's one example of something you would built on top of such an abstraction, and one way to bring together the seemingly-disparate collection of `?`/`yield`/`await!`. In Haskell, for example, you can use do-notation not only for IO, but also for async code, and list comprehensions, and early returns, and a host of other things.

I think we agree with each other, then -- sorry for misunderstanding your initial post.
> a common problem I have in Haskell is figuring out _which_ monad a certain `do` block is using.

Does type signatures not help in that case?

Yes, when people use them. There is no really elegant way of adding a type annotation to a `do` block.

    (do x <- pure 1
        y <- pure 5
        pure (x + y)) :: Maybe Int
It isn't uncommon to have to scan the `do` block for some statement that constrains the monad somehow.

This is a known and discussed problem, so I'll refer you to the article usually reference around this issue: https://wiki.haskell.org/Do_notation_considered_harmful

Many times do blocks are passed into functions so the monad type can be looked up from the function definition.

    flip runStateT s $ do ...
To find the type of this do block I don't have to scan it I just have to look up the definition of runStateT.

If I have to add an explicit annotation to a do block it's simple enough to separate it into another function.

    justSix :: Maybe Int
    justSix = do
        x <- pure 1
        y <- pure 5
        pure (x + y)
I read the article and couldn't find the part where they said that type inference is a problem with do notation.