Hacker News new | ask | show | jobs
by Rusky 3025 days ago
> (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 >>=.

3 comments

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.