When this was released I posted this comment on the Haskell subreddit, thinking I was on the rust subreddit. It's got some musings about what the dangers of becoming like Haskell are:
I wonder, were the Rust community be given the same power as the Haskell community, would they use it to create a complex more advanced idiomatic Rust that you could only read and understand after months of training?
There is this weird phenomenon in Haskell, I attribute it to Haskell having a syntax that feels limiting, but a type system that seems almost limitless.
There are two branches of idiomatic Haskell. One is the style anyone would recognize as generic "functional programming" and would be able to work with and be productive after a week or two of practice.
The other is where a dozen language features are enabled, and the latest and greatest utility libraries are imported and Haskell shows itself to be as flexible and alienating as Lisp. An expert can wield extreme expression under purity and safety. A beginner stands no chance.
It would be interesting to see more expressive power come to Rust. But it would be a bad thing if it would come at the cost of being beginner friendly. Rust might be challenging to write as a beginner due to the borrow checker, but at least it's easy enough to read.
Um, yes. C++ went down that rathole, and it's not pretty.
The functional people have been able to hammer Rust into a functional language, sort of. Now they want a Haskell-like "do" construct so they can get imperative sequentiality back. Is that right?
I'm not a huge fan of extensible languages. I've had to debug LISP code written by others.
> The functional people have been able to hammer Rust into a functional language, sort of. Now they want a Haskell-like "do" construct so they can get imperative sequentiality back. Is that right?
Not really, in that even before the blog gets to do notation, it depends on features that don't exist yet: generic associated types, which are RFC-accepted but not yet implemented, and associated traits, which haven't even been proposed. For now the whole thing is just a sketch. However, since associated traits aren't critical to the design (and would be a good feature to add anyway), at least some version of it will presumably be implementable in the future.
Overall, Rust has always been inspired by functional languages to some extent, e.g. Rust traits are based on Haskell typeclasses, and Rust has pattern matching and full type inference. But I don't see `Monad` ever becoming idiomatic. I also don't see `do` notation being accepted, because its use cases largely overlap with other features, namely async/await and generators.
On the other hand, it would require only minor changes to generators to allow implementing `do` notation on top of them, something I'm highly interested in mainly just because I want to prove it's possible. ;p
Please note that these are musings about possible language extensions that would let us express monads; this is not even at the stage of a proposal yet, and we haven’t all agreed that enabling this kind of code is actually useful.
The lang team would be in charge of these kinds of features, and I'm not on it, so I can't totally say. There are certainly members who do want this stuff. However, community sentiment is all over the map. https://www.reddit.com/r/rust/comments/b6bu2c/idiomatic_mona... is the reddit thread on this post, you can see a number of different opinions expressed there.
I personally want to see examples of real Rust code that has a problem that this solves. I get monads, I like monads in Haskell, but I'm unconvinced that the additional complexity is worth it in Rust.
I'd say that not enough functional extremism killed Scala. It tried to be all things for all purposes, compatible with every approach. So it grew into a incomprehensible monster of a language, full of subtle traps, like C++.
Making things compact and focused helps. Of mixed-approach languages, I see Kotlin's approach to integrating FP as more successful.
I'm sorry, I respectfully disagree on the "not enough" :-)
I think it is a matter of choice of audience. If you want pure functional languages, there are plenty that will rock your boat (Haskell, OCaml ...).
But, to my mind, I see Rust as a somewhat "modernised" C/C++ with, like Python, the most interesting and accessible parts of FP backed in. It is a balance and it is fragile. So, as I said, I hope that Rust will keep that fine line.
All that said, it only is my opinion, I might be wrong and the wider audience might be ready for "harder" FP. But I doubt it.
I'm not sure what parts of FP python has you're referring to but it seems pretty lacking. It has a few combinators, that's about it.
The influence of "FP" on rust is really more like the influence of Haskell's type system. Rust is pretty far from a functional language but it does share a lot of type system features with Haskell: type classes, sum and product types, HM type inference
Rust not only has some good parts from FP; they are added thoughtfully enough to keep them logically sound. So the fact that there are math-backed theories behind them still matters.
With C++, I can't say the same is true. Some libraries try to alleviate that, of course.
imo the early explosion of language complexity is what doomed Scala. There were a lot of failed and semi-failed experiments in the language. They even tried and gave up on structural typing and regressed to some half-assed attempt. Jumped on bandwagons they regretted like xml literals. There was also that research-based scala v2 rewrite. http://dotty.epfl.ch/ -- These kinds of endeavors suck a lot of thought power from the community.
There was functional extremism in Tony Morris and his scalaz pet project, but Scala was already doomed.
Of course, these are all things Rust has seemed to avoid and hopefully continues to avoid.
Was? Last I heard, Dotty is ongoing and will transition into Scala 3.
I like to think of Scala as the C++ of the JVM world. It's a complex language with a lot of features, but it can be extremely powerful if you know what idioms to use and where to use them. For example, template abuse in C++ is akin to implicit abuse in Scala.
Ok, I've been oversimplifying by reducing it to the functional aspect of it (granted that might come from Scalaz recollection) but I stand by my opinion that too much functionalism will hurt.
But, on the other hand, a balance has to be found if Rust ever want to be widely adopted. Let's be honest 2s here, Rust is already hard enough to learn for the average developer, adding higher order function on top of that will not help adoption.
IMHO, right now Rust the just enough amount of complexity to still be interesting to the majority. It really depends on who Rust want to target in the long run.
> I like monads in Haskell, but I'm unconvinced that the additional complexity is worth it in Rust.
Haskell and Rust are different enough that this is definitely a concern. Lazy evaluation and GC used throughout, vs. eager evaluation in a language which uses affine typing and regions to avoid GC... I can see how general monads might not work very well, and the OP does touch on this a little bit.
I'm not that familiar with Rust, but what is the rational against higher-kinded types? Is it a technical limitation that everyone agrees is a limitation, or is there some rationale why higher-kinded types in Rust are not useful?
This is probably not relevant, but as a professional Scala programmer, I find higher-kinded types absolutely indispensable. Without them, writing truly generic (and imo, beautiful) code is almost impossible. Programming with higher-kinded types does really feel like some "next level shit." higher-kinded types are to types as first-order functions are functions: the natural and logical extension that unlocks simplicity, genericity, and beauty.
> what is the rational against higher-kinded types?
There isn't so much a rationale against them, but rather, nobody has put forward a coherent proposal for them. Changes only happen when proposals are made and accepted, and there's never really been one for HKT.
That said, there are arguments that apply fairly generally that would apply there too, namely "is the additional complexity worth it"?
Generic associated types are equally expressive to higher-kinded types while being more straightforward to implement on top of Rust's existing syntax. Adding true higher-kinded types in the future is not out of the question, but you'd need to add new syntax to declare them, which represents a fair bit of complexity for something that may not be necessary.
There are also questions about ambiguities with respect to default parameters. If you have
struct Foo<A = i32>(A);
you can currently write `Foo` to refer to `Foo<i32>`, but what if you meant to refer to the higher-kinded type `Foo` itself? It could be determined based on context, but that would be somewhat confusing, and might not be possible in all situations (like when declaring type aliases, or if a way is ever added for a parameter to accept multiple kinds).
One of the purposes of publishing such papers, as see it, is triggering responses with simpler, easier proposals. They may cover the important 90% of the original, doing away with the most complexity-inducing 10%.
Implementing monads will be convoluted, using them is relatively easy. It wouldn’t change any of the existing code, but it would allow you to write a function like this:
fn double_inner<M: Monad<u64>>(m: M) -> M {
m.bind(|x| M::unit(x * 2))
}
Which takes anything wrapping an integer and doubles it, whether it’s an option or result or iterator.
Does any of this actually compile? Even with generalised_associated_types enabled I couldn't get the Functor definition to compile with a `trait MapFn...`
edit: If the main point of the article is to show that 'monads are feasible in rust' shouldn't you not be assuming a bunch of language features exist that don't?
>*edit: If the main point of the article is to show that 'monads are feasible in rust' shouldn't you not be assuming a bunch of language features exist that don't?8
Except if the main point is "monads are feasible in rust if we add these features".
> Except if the main point is "monads are feasible in rust if we add these features".
This is just showing imaginary syntax though, it doesn't speak at all to whether implementing these features is actually possible. Just because you make up some syntax doesn't mean it can actually be done (or how difficult it is etc)
I think the main point of the article isn't exactly "monads are feasible in Rust", but rather, "monads are a viable motivating case for these potential developments in Rust". The author is on the Rust compiler team and has several posts from similar angles.
> Pretty misleading to write "... I want to demonstrate that monads are feasible in Rust." if that's not actually the case
When not taken out of context, I don't think it's misleading at all.
> You see, there’s a problem with talking about whether monads would be useful or not, and it’s this: there are a large number of design challenges to overcome to have any hope of implementing them at all — to the best of my knowledge, there currently exists no realistic (that is, practical) design for monads in Rust. In fact, there are so many obstacles that some people express doubt that it’s even possible.
> In general, I don’t think it’s worth talking about the virtue of a language feature if we think there’s no way we could actually implement it. However, I think there are arguments to be made in favour of higher-level abstractions in Rust. Thus, to facilitate discussion, I want to demonstrate that monads are feasible in Rust.
There's no discussion of implementation, we have no idea how "reasonable" an implementation would or wouldn't be. My assumption from reading this is it would require a ton of non trivial additions to the type system
I think by "feasible", the author here means "the features required to support monads in rust could feasibly be added" not "it is feasible to implement monads is rust as it exists currently".
That was my first reading, your reading didn't occur to me, so I don't see that the title is especially misleading.
> feasible: possible to do easily or conveniently.
> likely; probable.
Support for monads in Rust is not possible to be added easily to the language, nor is it particularly probable. The features that come with the imagined syntax in the blog post are non-trivial additions to the type system.
Call me closed-minded, but I seriously hope something like this never makes it into Rust. Not only is the complexity overbearing, but the motivation for this solution is rather thin.
The first definition is says it takes a normal function and returns a function in category f. But second definition says it takes a value fa and a function and return fb. Does the order of defining arguments matters or its just a taste of matter? Thanks.
> Does the order of defining arguments matters or its just a taste of matter?
It's only a matter of taste or convenience. Haskell is a curried language, so you put the callback first to help partial application, and reversing the order is just a `flip` away.
Rust is uncurried and uses a C-style syntax, so you generally put lambdas in tail position, for a more block-y usage:
Don't make the mistake of thinking that translating a type signature into written English gives a canonical description of the function. The first definition says "(a -> b) -> f a -> f b" - that is it.
You can translate one into the other easily
map' :: (a -> b) -> f a -> f b
map' i j = map'' j i
map'' :: f a -> (a -> b) -> f b
map'' m n = map' n m
You're thinking in terms of currying, which Rust doesn't support. If you could partially apply arguments to functions in Rust, your reasoning would be correct, though the functions would still be equivalent since you can define `flip` (as in Haskell).
One question I almost never see addressed with monads is the question of the
stack. The stack is not endless, and may actually be quite small on some
devices. How is that fact reconciled with the fact that do-notation is
essentially
Your concern isn't completely unwarranted but in practice things generally turn out fine. Each time you hit a >>=, you are conceptually yielding control to the monadic datatype. You pass it a function, which captures some closure, which your compiler presumably optimizes to save off only the data that you may need if your function gets invoked (though over-capturing definitely can happen). Really it's a reference to the closure and maybe the function that end up sitting on the stack. The monadic datatype itself is responsible for making sure you don't create too many frames. There are different approaches they can take to accomplish this, including everything from trampolining to iterating with tail calls to imperative loops. It does occasionally happen that some datatype simply shirks its responsibility to be stack-safe. Anyway, overall, there are solutions to avoid blowing up the stack, and they generally get implemented, but occasionally it does become an issue.
That is assuming that your compiler knows about such optimisations and can
apply them in non-recursive situations. As far as I know, Go doesn't apply
tail-call optimisation. Haskell probably does. Not sure about Rust.
Either way, depending on the compiler making one particular optimisation just
so that your code works seems rather fragile to me.
There are languages where TCO is defined as part of the language spec, specifically to give programmers permission to write this sort of code. Some Lisps (Scheme, I think) and Lua work this way. TCO is a relatively well-understood optimization that isn't prone to being brittle the way e.g. auto-parallelizers or auto-vectorizers can be. So it's less fragile than you'd think.
TCO is a lot easier to do in a garbage-collected language. In particular, with Rust, having any live stack value that conforms to Drop will break the ability to do tail-calls.
Or just skip the trait and require there to be nothing live on the stack that conforms to Drop after a `become`.
In fact, the main reason to have a keyword like `become` is to signal to the compiler to do this kind of analysis, because it's so easy to accidentally break the ability to do TCO when you can't lean on a GC to clean things up later.
Didn't know about Lua[1], thanks! Although I think I would prefer an explicit
syntax, like Tcl's “tailcall”[2], Perl's “goto &sub”[3], or “become”[4], with
the compiler checking that this is actually a tail-call.
This article might do better by actually explaining what a monad is in layman’s terms, rather than deferring the reader to Wikipedia via a footnote. I read 1/3 of the article hoping to gain an understanding but gave up and didn’t read further :(
That's because it's a technical article about monad implementation, building on previous technical discussion. The author could spend time explaining what monads are, but that's a notoriously tricky thing to teach, and if you don't understand them fully, the article as a whole won't be worth anything to you anyways.
Perhaps the author could have put such a disclaimer? "Note: it is beyond the scope of this blog post to explain what a monad is. Please refer to x,y,z for background material."
> I am going to be assuming some familiarity with monads (and to a certain extent, do notation). There are enough introductions to monads out there as it is.
I agree with eatbitseveryday and don't think that this should have been downvoted. I've tried to learn some of the more esoteric concepts in computer science (like monads, and ycombinators for that matter) but eventually forget how they work each time.
Personally, I think that monads are a sore spot for functional programmers, because they are the crux of why FP runs into difficulties in the real world when dealing with things like mutable state, side effects and logic that happens over multiple steps (imperatively).
Here is a relatively nontechnical discussion about how monads bridge functional and imperative programming, by treating IO sort of like an imaginary number that can be passed along as a black box until it can finally be computed:
Monads in my experience are less likely used for IO than people think. Of course Haskell developers use monads in this way, but OCaml and Scala programmers generally avoid this.
Instead monads are used to unify the foundation of many types: options, lists, trees, etc. Also monads are really nice for futures and parsers.
The point I’m trying to make I guess is that monads are very useful for other things than dealing with impurity, which I think a lot of people just learning them miss.
There already are way too many articles explaining monads on the internet. For some reason, it's many bloggers favorite subject.
But I'll give it a go, I suppose. A functor is a type that represents turning something into something else. For example, for example turning a List<Int> into a List<Double>.
A monad is a functor that also has a bind or flatMap method attached to it. Both essentially allow you flatten or "chain" the values inside. So the monad defines a way to map things (inherited from functors), and flatten things (via bind or flatMap depending on how you want to define it). Also you need applicatives, which is just a way to box things. i.e turning a Int into a List<Int>.
There are also some laws associated with them, but you probably get the idea.
Examples of useful monads: lists, options, futures, database transactions, logging, parsing, etc.
> Also you need applicatives, which is just a way to box things. i.e turning a Int into a List<Int>.
Applicative functions do have that property, but their key attribute is the ability to two things together, e.g. turning a List<Int> and a List<Double> into a List<Pair<Int,Double>>. A functor which also has the ability to put arbitrary things into the functor context, but not the ability to combine two contexts, is sometimes referred to as a pointed functor. (There is no official Haskell typeclass for pointed functors; the main reason, besides historical baggage, is that non-applicative pointed functors don't have any real laws aside from the ones which apply to functors.)
Applicatives are sufficient for sequencing effects but the List<Double> in the previous example can't depend on the Int values in the List<Int>. This is great for static analysis of effects but it limits the kinds of programs you can write to those whose effects are known in advance. A monad lifts this restriction and allows effects (not just values) to be computed dynamically from the values of prior effects.
Finite[1] applicative parsers can only parse context-free languages. Monadic parsers, on the other hand, can handle context-sensitive languages (without resorting to an infinite set of production rules).
I wonder, were the Rust community be given the same power as the Haskell community, would they use it to create a complex more advanced idiomatic Rust that you could only read and understand after months of training?
There is this weird phenomenon in Haskell, I attribute it to Haskell having a syntax that feels limiting, but a type system that seems almost limitless.
There are two branches of idiomatic Haskell. One is the style anyone would recognize as generic "functional programming" and would be able to work with and be productive after a week or two of practice.
The other is where a dozen language features are enabled, and the latest and greatest utility libraries are imported and Haskell shows itself to be as flexible and alienating as Lisp. An expert can wield extreme expression under purity and safety. A beginner stands no chance.
It would be interesting to see more expressive power come to Rust. But it would be a bad thing if it would come at the cost of being beginner friendly. Rust might be challenging to write as a beginner due to the borrow checker, but at least it's easy enough to read.