Monads and GATs in Nightly Rust
fpcomplete.com
fpcomplete.com
It sounds silly but its true.. as you increase the complexity of the language, the hardcore users making the libraries adopt those features, which means users have to adopt those features as well. Pretty soon everyone is talking about monad transformers and HKTs and it takes 4 hours to write something that would take 15 minutes in Python.
Then the wave of FP consultancies and trainings show up so they can bill you $10000s on workshops and conferences to teach your devs monads.
It doesn't track that it's possible to simultaneously ruin a language by sabotaging all of its major libraries with novel features if writing code using novel features is actually incredibly difficult. It certainly doesn't track that, once you have somehow sabotaged a language's major libraries, that nobody bothers to "fix" them by introducing a new, simpler version.
Some people are passionate about the language itself, and programming language theory in general. Others are passionate about solving whatever particular problem their project solves.
A simple thought experiment - think about the most widely used libraries and tools across the whole developer ecosystem. How many are built in Haskell? I count maybe one, Pandoc. How many are built in terrible code bases and languages but chug along anyways? I count thousands. How many wildly successful companies have pristine code bases and how many have trash fire code bases that chug along anyways?
Purescript and Elm are two more. If you don't count languages, then Xmonad and Darcs are another two. Both Github and Facebook's efforts in mass source-code searching are written in Haskell (though Facebook's is not really released to the whole developer ecosystem).
This is also a misguided thought experiment - Haskell is relatively unpopular anyways (as Rust is). It has a reputation for being difficult to learn (as Rust does). How many tools across the developer ecosystem are written in Rust? Ripgrep, and maybe Alacritty. Does this reflect badly on Rust? No, it's immature and needs a lot of developer support - which is why much of Rust's development effort is in new libraries.
Does a(n alleged) lack of tools reflect badly on Haskell? No, both because it was for a long time considered an academic language, and because Haskell's great successes have also been outside of the "developer ecosystem" - in webservers, for example.
And none of this addresses my original objection to your point: why, if simplicity is so productive, is it not easy to replace complicated libraries with simpler versions? In Haskell, the answer is that the simpler versions are much less powerful, and the power of advanced languages features is actually a boon for productivity, because encoding your invariants in a good type system saves you work elsewhere. That's the whole benefit of Rust's borrow-checker over C++. There is no real risk of Rust getting "too complicated", because these advanced concepts still let people build shit.
Fortunately, when you look at the latest version, Scala 3, you will see that they put more effort into simplifying the language and removing things that people complained about than adding new things.
To be a bit more concrete:
- Remove certain ways of using implicits and making them less confusing
- Cleaning up the language core and removing features (such as impure macros or constructs that are rarely used or confusing)
- Making syntax easier for many cases without adding new features
- Adding union types (comparable to typescript). This is a new feature and not a small one, but I think it will make the language easier to use for many people.
And I think union types and intersection types are a very practical thing to have. So all in all, I'm happy to see that Scala becomes more practical and less esoteric with this release.
Conversely, using the same technique, one could also ascertain that every language is also languishing.
"Any method of abstraction that you have internalized becomes conceptually free to you." (So for the sake of others, choose wisely which you will expect maintenance programmers to have internalized!)
If you've internalized all of the FP ideas, then adding them to a new environment seems useful and seems to cost nothing. It is hard to keep track of how much burden you are adding for people just learning the environment because, for you, it is free.
I suspect that the above reflects a very personal experience rather than something general. I have been using Scala for 10 years and never used monad transformers. I find Scala code usually easy to write and to read. At this point I wouldn't trade it for any other language.
From where I stand, Scala was neither "taken over", nor "destroyed by FP fanatics". It is not Haskell, and the upcoming Scala 3 is not going in the direction of being more like Haskell. Scala has always been about supporting both object-orientation and functional programming. It's still the case with Scala 3, and it's getting better and better.
With the exception of shops using Scala exclusively for Spark, someone entering the ecosystem can expect to be constantly talked down to if you dare to use something deemed by the FP hivemind to be evil (so, anything other than pure, immutable, FP-style with all side effects controlled, no exceptions or nulls, no inheritance (only typeclasses), etc). They'll be derided and skoffed at constantly for being "just a java++ programmer".
Meanwhile, for all their smugness, the FP community has achieved nothing at all that has reached beyond to the world outside of Scala. The projects successful in bringing in developers to Scala have been decidedly in the disparaged lightly-FP/"Java++" style: Spark, Kafka, LinkerD (1.X -- they rewrote 2.X in Go/Rust), Flink, Akka, PlayFramework, Twitter's stack, Prisma, etc. Shockingly, the predominant view among these delusional pure-FP-obsessives, which usually goes unchallenged, is that the creators of Spark don't know anything about Scala or are bad engineers!
Scala 3 is a joke and will do the exact opposite of the stated goals from years back. It was supposed to streamline and simplify the language, remove gotchas, add a few high-impact simple features like Union types and trait parameters. Now it's a grown into a monstrosity of complicated features that your average dev will never use. Rather than streamline and simplify Scala 2's issues with having dozens of ways to do the same thing, it introduces multiple new dimensions by which people can do things in multiple ways (legacy implicit system vs new "given" syntax; braces syntax vs indentation-based).
Control structures helpful for imperative programmers are being removed (you cannot early return from a for loop without a huge hassle), `do-while` is removed, etc. These breakages were not pushed back against because the only people still around are FP'ers who don't want people using loops in the first place.
New type-system features are being added without even knowing if there's a possible use-case. The whole thing is a mess.
feels so right, is it true?
(I, and my company, use Scala as a general-purpose language, targeting both the JVM and JavaScript, by the way.)
I use Scala, I love it, I am looking forward very much to Scala 3, and I find help in the community when I need it. And I am not a pure FP programmer, although I like lots of the ideas of FP.
> everyone but the fanatical FP-ers have long-since moved on to other more professional/productive circles
I for one haven't and I am not a "fanatical FP-er". I bet I am not the only one.
> Scala 3 is a joke […] it's a grown into a monstrosity of complicated features that your average dev will never use
I don't think that's true at all.
> New type-system features are being added without even knowing if there's a possible use-case.
I don't know what you are referring to. From the doc, I note:
- intersection types, which are essentially a better way (commutative) of doing `A with B`
- union types, which several languages now have, including TypeScript, and which are definitely a very useful feature (in particular for Java and JavaScript interop, but there are other use cases)
- dependent and polymorphic function types, which are just an extension of what was possible before with methods
- match types and type lambdas, which I cannot comment on
> The whole thing is a mess
I obviously don't see things with the same eyes you do. I am really excited about Scala 3 and I do think it improves the language significantly, as it should.
Scala 3 also aims at solving a very real issue with previous releases of Scala, namely that there is a solid binary compatibility story, within Scala 3.x, but also between Scala 2 and Scala 3.
> the only people still around are FP'ers who don't want people using loops in the first place
I can't remember the last time I used `do-while`. But you can rewrite this trivially to a `while`, which is not going away (in fact, I think that Scalafix will do that for you automatically). In any case, community questions about things like mutability, loops, or more imperative features, typically receive answers to the effect that it's all right to use such constructs, especially in the small (like a the level of a single function). These features are part of Scala and generally accepted. They are used by the Scala standard library, and regularly acknowledged by Martin Odersky. A quick code search finds such uses in libraries such as Circe and Cats.
Regarding non-local `return`, you are also overlooking the fact that this is a frequent cause of confusion and errors. Removing this feature has little or nothing to do with FP fanaticism.
Personally, I can only encourage programmers to look into Scala. It is a fantastic language with great features. It also has an incredible JavaScript story with Scala.js, which is rock-solid. The transition to Scala 3 will be a good time for newcomers to look at the language and its community with a fresh look.
Prismas query engine is a complex piece of code and receives very little outside contributions. Moreover, Prisma only has bindings for JS/TypeScript and Go at the moment, so there is no way to consume Prisma from Scala. As a result, very few Scala developers know about Prisma.
We enjoyed Scala as a language, and the massive JVM ecosystem is a huge benefit. That said, we were forced to rewrite the query engine in Rust as we had a need for a more modular architecture enabling us to embed parts in JS and Go libraries. We looked at the Scala Native and Graal projects (spent 6 months building a prototype), but neither delivered a sufficiently low memory footprint. The Prisma2 rewrite to rust is a much more stable product, and we love the Rust language.
All the best to both the Scala and Rust ecosystems. Hugs.
Isn't the FP community in the Scala world at war?
I have heard that there have been one or two difficult individuals in certain segments of the FP community, especially 5-10 years ago. I understand that this caused the Scalaz/Cats split. But I have never really needed to care about it and it seems to me that this is history. I could be wrong but it's my personal experience.
Using FP without reaching for the most advanced techniques (which Haskell seems to employ) is a very valid and viable choice.
* https://www.simplehaskell.org/
* https://www.snoyman.com/blog/2019/11/boring-haskell-manifest...
"Our recommended [language extensions] defaults are: AutoDeriveTypeable BangPatterns BinaryLiterals ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable DoAndIfThenElse EmptyDataDecls ExistentialQuantification FlexibleContexts FlexibleInstances FunctionalDependencies GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MonadFailDesugaring MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude OverloadedStrings PartialTypeSignatures PatternGuards PolyKinds RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TupleSections TypeFamilies TypeSynonymInstances ViewPatterns"
39 language extensions just to get started. This screams 'incredibly complicated', even if perhaps reality is rather more mundane. Consider the 40th language extension: GradualTyping, so perhaps those that would rather write code about data than about types using a half baked and evolving type language (which taken to its logical conclusion will have to become a full fledged theorem prover in the Coq / Idris / Agda / Lean lineage anyways) could get their jobs done.
Wish you guys all the best!
Rust uses "traits" to do both generic-like things and inheritance-like things. It's not clear this was a win. The result seems to be a system which does both badly.
Rust generics are very restrictive. They're not at all like C++ generics. It's not enough that every operation on a type needed by a generic be implemented. The type has to be part of a single trait that provides for all those operations. It's like C++ subclassing. So generics over types defined by others can be impossible to write in Rust. This has no safety benefit, since generics are resolved at compile time and all safety tests can be made after generic expansion.
Traits and fixed array bounds do not play well together. This is considered a bug, and it's been a bug since at least 2017. Generic parameters can only be types, not numbers. This led to a horrible hack involving a Rust crate which has types U1, U2... U50 or so, so small numeric constants can be bashed through the Rust type system.
Not seeing the benefit of all this.
I have to go struggle with another non-helpful "lifetime `'static` required" message from the borrow checker now.
I agree with you about the orphan rule being annoying and wish they’d relax that, but I much prefer traits to C++ duck-typed templates, if only because it makes error messages from passing an unsupported type to a generic function clear and concise as opposed to the thousands of lines of confusing output you often get in C++.
The compiler can verify that types are satisfied. It cannot verify that you agree on what those types and operations mean.
For example, `Iterator` isn't very useful without being able to agree on what `Iterator::next`'s `Option` means. So implicit structural traits á la Go is somewhere between useless and actively harmful.
You can have nominal traits while allowing orphan instances (that is, implementations of foreign traits on foreign types). Scala and Haskell both have that. But IMO neither has a good solution for the conflicts that inevitably arise in that case.
It's not only safety. That's why C++ concepts were invented. https://cpp.godbolt.org/z/8Gcrj6
FWIW the remedy for this is being stabilized soon: https://github.com/rust-lang/rust/pull/79135
IMO the fact that Rust doesn't do inheritance well is a feature, not a bug. My understanding is that most people even in OOP circles have realized that composition is better than inheritance, and I see Rust's choices around it as a reflection of that.
> It's not enough that every operation on a type needed by a generic be implemented. The type has to be part of a single trait that provides for all those operations.
Maybe I'm misunderstanding, but do you know that a generic can combine multiple traits? For example:
fn foo<T: Copy + Add + Sync>(x: T) {
...
}
Here T must implement Copy and Add and Sync.Maybe what you're talking about is the fact that methods' identities aren't fully described by their names + types, but also by their trait? i.e.:
trait TraitA {
fn foo(&self, x: i32) -> i32;
}
trait TraitB {
fn foo(&self, x: i32) -> i32;
}
struct Foo {
}
impl TraitA for Foo {
fn foo(&self, x: i32) -> i32 {
x + 1
}
}
fn requires_b<T: TraitB>(x: T) {
x.foo(12);
}
fn main() {
let f = Foo { };
requires_b(f);
}
error[E0277]: the trait bound `Foo: TraitB` is not satisfied
--> src/main.rs:25:16
|
19 | fn requires_b<T: TraitB>(x: T) {
| ------ required by this bound in `requires_b`
...
25 | requires_b(f);
| ^ the trait `TraitB` is not implemented for `Foo`
This was an opinionated decision made by the Rust team - which I think was a good decision - to address the fact that it's possible to have unwanted overlaps between trait method signatures. Just because fn foo(&self, x: i32) -> i32 is defined for a struct, doesn't mean it's the same foo that you're wanting to call. Distinguishing things by trait strengthens the contract. It also avoids the diamond problem. You're right that this isn't directly related to "safety" in a memory sense, but it's part of an overarching philosophy of Rust that encourages intentionality and discourages footguns.> Generic parameters can only be types, not numbers.
This is a work in progress and has partially been rolled-out; the full implementation is on the way: https://rust-lang.github.io/rfcs/2000-const-generics.html
Rust has macros and procedural macros for the "check after generic expansion" use case. It's a good thing that this is separate from generics; it side-steps a whole lot of incidental complexity that's seen in C++.
trait Iterator {
type Out;
fn next() -> Option<Out>;
}
vs trait Iterator<T> {
fn next() -> Option<T>;
}
Multiple ways to do something has made it harder to understand.Sure the language _has_ higher kinded types and implicits can be twisted to support type classes, but neither of those seems to have been forced in the language from FP fanatics, but rather the language has always included some set of advanced features.
>But Rust is not Haskell. The ergonomics of GATs, in my opinion, will never compete with higher kinded types on Haskell's home turf. And I'm not at all convinced that it should. Rust is a wonderful language as is. I'm happy to write Rust style in a Rust codebase, and save my Haskell coding for my Haskell codebases.
How is any of this fanatical?
Do people generally feel like the more features the better the language? I'm personally of the opinion that less is more.
Is this a pain point at the moment for Rust devs? Do you feel like the code you write is the same code 99% of other Rust developers would write to solve the same problem? Or is there actually a really large variety of styles?
Rust already has generics and Rust already has associated types. Previously you needed to know/remember those features cannot interact for some reason, now they can. To me this is in a sense 'less' and not 'more'.
So, a matter of perspective I guess.
Associated types make the distinction far clearer. They better capture the qualitative distinction that "you can choose these, I get to choose those".
Also, associated types are inherently "functionally dependent" in the sense of Haskell's multi-parameter type classes. If you have a trait `F<X, Y>` with an associated type Z, you know that given X and Y, Z is fixed. Without associated types, `F<X, Y, Z>` could have multiple legal implementations for the same X, Y and different Z.
This is extremely meaningful in languages like Haskell and Rust which implicitly thread around the trait methods. Typeclasses in Haskell can be imagined as describing concrete dictionaries of functions over the given types. Languages like Java (manually) and Scala (automatically, using "implicit") reify these typeclasses as dictionaries that are threaded through functions as extra parameters. You can often define multiple implementations of a given trait for the same types, and you get to choose which implementation to pass along.
Rust and Haskell assert that traits have a single implementation for a given batch of types, and they automatically look up the correct implementation given the types you've specified. These "functional dependencies" mean that, in my example of `F<X, Y>` with associated type Z, it's sufficient to state what X and Y are to find the right implementation of F -- Z doesn't contribute to the lookup. If you don't have associated types, you could have multiple implementations that simply vary on Z, so you have to tell the compiler explicitly which one to use (by stating what Z is).
Consider the example of Rust's Iterator trait[0]. It has an associated type for the iteration item. It could have a generic type argument instead, but the two implementations would be different:
1. The associated type is a direct consequence of the instance head. That means that, for the type that Iterator is being implemented on, the associated type is known entirely from that type. If you have an iterable value, you know it only produces one kind of iterator item.
2. As a result, the associated-type version can only be implemented at most once. The generic version would be implementable any number of times, for any number of choices of type argument - and as a result, you would have to specify that you are iterating over an iterator with a particular choice of iterator item type, every time you iterate.
[0]: https://doc.rust-lang.org/std/iter/trait.Iterator.htmlI imagine abuse of specialisation could also increase the confusion, if you were so inclined.
> "The difference is that when using generics, as in Listing 19-13, we must annotate the types in each implementation; because we can also implement Iterator<String> for Counter or any other type, we could have multiple implementations of Iterator for Counter. In other words, when a trait has a generic parameter, it can be implemented for a type multiple times, changing the concrete types of the generic type parameters each time. When we use the next method on Counter, we would have to provide type annotations to indicate which implementation of Iterator we want to use."
If you have a `trait A { type B; ... }` then for any type T you can implement one specific A with one specific B.
But if you have `trait A<B> { ... }` then you can have a different implementation of A for every disjoint type B.
Then if you have `trait A<B> { type C; ... }` you can have one specific C for every possible disjoint B.
So with associated types you can express additional constraints/semantics which you can not express with only "classic" generics.
Furthermore the compiler can rely on this constraints for e.g. type inference.
I'm working though a Rust course right now, and while my code works fine (once it compiles), I always see the reference implementations of the code, and it's like 2 lines of filter/map/collect, and voila. Meanwhile, my 8 line frankenfunction looks like the Charlie Brown Christmas tree.
I always wondered how this would then be configured in a convenient way. I mean, there are situations where you can not just let the compiler parallelize to the max (e.g. web services).
I do not currently feel like there are many different solutions to a problem - at least, not in the sense that the solutions only differ by style. Of course each problems have different solutions that are different points in the performance spectrum (static vs dynamic dispatch, etc...).
All the big features that are coming (GAT, const generics), all feel like lifting restrictions that will remove the need for hacks. To take const generics as an example, it will obsolete the need for `typenum` and `generic-array`, giving us a more consistent style. Less type-hacking is a good thing.
I do agree with the sentiment: Rust is already a big language, and has to be careful when weighting new language features to make sure they don't spend too much mental overhead. So far, I feel like they've done a good job at it.
It is starting to become a pain to me, but I'm glad that apparently there is some semblance of h.k.t.'s now, which was very much wanted.
Where I get some analysis paralysis is in getting one library over another one.
Eg. I'd love to have a std lib recommended way of doing async and task execution with a thread pool eg. Async-std vs Tokyo Vs Rayon.
They're all individually great pieces of software but none of them feel complete and I always reach out for something that is not there.
To be fair I don't think this is rust specific, I think this is just the bane of OSS solutions with unpaid maintainers.
Sure, I could contribute something or fork them but it takes time.
I'd prefer to just buy in into a solution, pay a monthly fee and know that I'll get a full featured solution with a well paid maintainer.
I'm not convinced that the libs being in std would help you with this issue. Instead you'd probably have the same incompleteness with a whole bunch of the functionality only available in nightly rust because of the stability contract implied by std.
Picking either tokio or async-std (flip a coin if you can't decide) and using their recommended way doing a given async task isn't really all that different from doing the same with a hypothetical std::async.
That said, there are some traits being standardized and moving to the std, which should be great for interop.
For example, I'd like to put in my Cargo.toml:
sstd = { version = "1.0", features = ["clap", "serde", "rand"] }
Where the sstd crate essentially has a whole mess of crates bundled together ensuring only one version is used for each individual dependency?
Perhaps this is a question best asked elsewhere - your comment just brought it once again to my mind.
(Also, this would not assure that only one version is used of a dependency.)
So I don't really know what to say to your initial question, except "this feature is useful", and that the question (and especially your answer to it) may be a bit overly reductionist.
To be clear here, you don’t need to write any of this stuff in the blog post to write an async method, but the desugaring of an async method involves GATs, so the language has to have them.
What I need is something similar to:
trait AsyncCallback {
type Input<'a>: 'a;
type Output<'a>: Future + 'a
fn call<'a>(self, Self::Input<'a>) -> Self::Output<'a>;
}But besides that the amount work is indeed very close by each other. Which is why there is currently no plan to first have lifetime only GATs stable as far as I know.
You cannot specialize on lifetimes, it is not sound.
But then if you did so you probably already implemented most/all of the code to typecheck non lifetime GATs and then doing the rest proper is probably not to hard either and might be not harder then doing liftime only GATs.
> You cannot specialize on lifetimes, it is not sound.
Good to know, given that specialization was stuck in nightly in the last years I didn't use it and in turn didn't look to much into it.
We don't need new languages without features: it has already been proven that you only need exactly one feature in your programming language to write any program possible. Look up single instruction languages for proof that it is possible and such single features languages exist.
What we need are languages with many features that are easy to use to create complex programs that are still easy to understand.
C++ is a very powerful language, but the various features do not fit together well and so even experts tend not to know how to use everything and so the whole suffers. Most languages since then have attempted to take the [all or some subset of] the features of C++ to make a better language. (when any other language comes up with a good feature C++ copies it so is often getting credit for ideas that actually come from elsewhere - possibly better)
When someone is saying they want less features they mean one of the following: "I don't know how to use some feature and I've gotten this far so it must be useless". "That feature is useless for my domain so I don't want to pay the compromises required to allow it". "That feature is too often abused to write bad code and so it isn't worth having". "That features is neat but it makes the whole language ugly and so it isn't worth having". There is probably something I missed but you get the idea. Some of the above are logical fallacies, the others are compromises that apply to a particular problem. None of them are universal truths for problems.
The RFC for GATs was opened in 2016. It's still not clear when this is hitting stable Rust yet.
Everything else seems to be on the order of small polish to the language (trailing commas), standard library expansion, and in some cases language restrictions to make rust fit better with embedded systems.
In many ways, Java is seeing more major changes than rust is.
In C++ this is less so the case. It's quite simple to write code which seems ok, currently happens to execute expected behaviour but triggers undefined behaviour.
* async - you can write futures without async, but async lets you do more because of how it resolves lifetime issues you'd normally get when using combinators. The other route without async would be to manually write unsafe state machines. Yuck.
* GATs - these are required to be able to have native support for async methods in traits (interfaces) without needing to box the return value (ie. have statically dispatched ... not sure what the term is, but it lets it be online too, IIRC; I might be wrong about the performance ipact, but ergonomic pain is real)
* const generics - this gives better support for stuff like arrays, since really what it's doing is stuff like supporting array length as a generic type parameter instead of a special case.
No, in rust as long as you don't use a feature you don't need to know about it.
> Or is there actually a really large variety of styles?
There are official(-ish?) style guidelines. So while there are a variety of styles most code uses mostly the same style.
> I'm personally of the opinion that less is more.
Sure, but you normally want to have a "complete well rounded language".
For this in rust you need GAT, but you don't need HKT, Functors, Monades or similar.
I hope there will never be a Monade trait in std, it would add a lot of mostly hardly useful complexity we really don't need and would mostly be used for IMHO mostly useless over abstraction.
But GAT are really needed. E.g. to properly work with certain async use cases (through for this you only need GAT for lifetimes, not arbitrary type. In my experience GAT limited to lifetimes cover 80+% of the cases where you really really need GAT).
Is that not the same in C++ and in most languages?
... until someone else in your team uses it in your code base. And then you need to know about it in Rust as well.
Also C++ has a bunch of "hidden" features and unexpected interactions with other features and UB like e.g. forward guarantee because of which `while(1);` is UB.
I don't think that's a particularly good argument, since it's true for all languages. Even if you don't use a feature, you will use and read code written by others all the time and understanding that code can be pretty important.
My experience in Rust has been pretty positive. I'm writing a new book on systems programming with Rust and, as a result, have spent a great deal of time digging into the internals of libraries, the compiler. The compiler is, by far, the most surprising because it's allowed to use nightly features in new and interesting ways. Everything else, when I encounter something new to me, I'm able to understand from the Rust documentation. The key differentiator with C++ is, I think, the focus on documentation and ensuring that new features are explainable in a simplistic way. This helps make new features introduced into the language jive, to my eye.
That said, there are areas of the ecosystem _outside_ the language that are hard to keep up with. The future notion in Rust used to be like that, before Future was included in the base language. That's tricky but, again, I think Rust strikes a good balance here: conservative about content in the base language, enthusiastic experimentation in the ecosystem. It's possible that this'll break down some day but it hasn't yet and I don't see it as happening soon.
But there are cases, often for libraries, where features can be quite helpful and make me faster. If a feature is basically "make code that should have already worked actually work", that's a huge win.
A lot of Rust features tend to be that. It's like "OK, we have 'impl trait', but it only works in some places. Let's let it work in more places." So,yeah, sure, that's a new feature - impl trait in new positions - but it's really just supporting code that many people would have expected to work.
I see this GAT feature similarly. It wouldn't be hard to "accidentally" try to have a generic associated type - in fact, I have probably run into this myself. And so GAT isn't really adding more complexity to me, it's just unlocking code that I would have already written.
Similarly, with GAT, we can unlock 'async' in traits. I already know 'async', I know it on functions and methods. So this is, again, just making an existing feature more consistent.
[1] https://dl.acm.org/doi/pdf/10.1145/3315454.3329960?download=...
I used to have this position as well, writing everything in either assembly or pure lambda calculus, to avoid all these pesky higher level language features.
What's missing is implementing Functor for `Option` (the "unapplied" option, without a type parameter), in such a way that it is evident in from the trait implementation that if you pass an Option to `fmap`, then you get an Option out (not just any Functor). In other words, that the particular flavour of functor or monad is preserved in these operations.
Proof of concept code for this exists, but Rust doesn't support it in a fluent way, even with the GAT feature.
I was momentarily confused by this explanation, so allow me to distill the problem.
In the trait declaration for Functor, nothing requires that `<X<T> as Functor>::Wrapped<T> == X<T>`. In other words, the implementing type can be different from the return type of `map`. You would want to return `Self<T>` from `map`, but `Self` refers to the fully-reified type, which is to say the implementor has already decided what the type parameter (if any) is, and you as the trait author have no control over it.
You need some way to force the above equality, which is probably what the referenced "proof of concept" does. (I think I found it here: https://github.com/edmundsmith/type-plugs)
(EDIT: I found another proof of concept via the author's bug report. The two approaches seem pretty much the same, and it doesn't look too difficult to understand. https://users.rust-lang.org/t/monads-in-rust-with-gats/50487)
The article does mention this problem explicitly:
>> Interestingly, we have lost the knowledge here that Self::Wrapped<T> is also a Pointed. That's going to be a recurring theme for the next few traits.
To be explicit, a lazy collection type in Rust is more than just `List<T>` -- it needs to carry an additional type describing the set of transformations being applied. You'd really have something closer to `List<F, T>`, and as you apply transformations, F grows while T is substituted.
(You could avoid this by storing a trait object for your transformation stack instead of parametrizing over F, but half the point is that you can often flatten the whole stack down into highly-efficient generated code, and abstracting over the precise F hinders that flattenability.)
The thing I like most about Rust is that it is still a practical language where I can still solve my problems in different ways and always know what will happen. I also care about the "how" and not only about the "what".
Everything being F[_]ed is what I do not need anymore.
Combine that with GHC's optimizer and it's no contest which language to choose if both ergonomics and performance of code written largely as lambdas is your priority.
It's not so much the GC, as it is "in a GC'd language, everything is on the heap and a lot of information (from Rust's perspective) is erased." You may be able to get rid of GC in some sense, but you'd end up with something halfway to Rust, not the whole way.
That makes sense, thanks for explaining that, I am used to Haskell but never used Rust.
That said, -XLinearTypes in GHC 9.x will open up a world of memory management capabilities in library-space for Haskell. Very exciting stuff! If you push the heavy stuff off-heap, then the GC just becomes a slightly fancier arena allocator.
You can also have the RTS manage the memory itself, but have complete access to a pointer to raw memory. That doesn't work if you have pointers in the raw memory ofc, but it is a nice option if that's not the case.
Unfortunately we don't yet have covariant associated types, so my GC is a pain to use.
In all seriousness, I'm not sure if Rust will ever be a great fit for abstract high level programing. The syntax is just too verbose, and lifetimes are a leaky abstraction. It might make a good compiler target though. At the very least next generation FP needs to have a great Rust interop story.
If someone were to create an explicit trait for them and RFC it, they'd probably name it something like Map and Then rather than Functor and Monad.
I think this kind of "you've already used functors or monads, they are just scary names" retort is really missing the point of what folks are complaining about.
I'm not even taking a side here in this thread (although I have advocated against functors/monads in the past). I'm saying that your comment is missing the point of folks who are skeptical of things like functors and monads.
No. They are Option and Result and Future.
> You've probably used them without even knowing, because we didn't give it some weird, unlearnable category theory name.
No. I have not used them without even knowing. I have used Option, Result and Future. I do not need some meta-universe which just makes easy things more complicated by stating some laws which types must hold just for the sake of discussing them and have the one ring to rule them all.
Don't you find it intriguing and interesting that these seemingly different types have these commonalities?
And I have to admit that I find these commonalities very fascinating. I can still remember how I enjoyed applicatives and the like when I started with scalaz back then. Whether the model becomes simpler? I do not really know. I just found out for myself that I do not need this knowledge at work and that it didn't really help me to solve my daily problems. This is actually what drove me to Rust which in my perception is a nice practical "in between" (I know it also is no silver bullet).
It's not a ring, it's a monoid; there's (in general) no negation.
The most advanced crate for such use-case is `indexing` [0] but it is pretty complex inside, requires closures and seems more like an experiment.
In Java(!), I have a class `Database<$Database>` whose constructor is private and whose sole static factory returns `Database<?>`. Thus, the caller must assume that every instance they create is parameterized over a distinct unknown type -- even if, in the implementation, $Database = Object. The dollar-sign prefix is a hint to the reader that this type parameter is meant to be a type correlated with a unique value.
You can get an `Index<$Database>` from any database, and it's parametrized over the owning database's unique existential type. Indexes over two different databases can't be confused.
The only issue (so far) is that you have to be careful not to "forget" the correlation between the type parameters on an index and its owning database. If the type parameter decays back into a wildcard, the correlation is lost.
[1] https://varkor.github.io/blog/2018/07/03/existential-types-i...
It’s not about making things “more like Haskell”, that just happens to be a consequence of making more low-level operations representable in safe rust code, which if you think about it is the way rust has operated from the very beginning.
Well, careful -- I'm sure you know this by your name, but the naive treatment of Java's native array type as A[] <: B[] when A <: B leads to well-known soundness issues.
Fundamentally that's because you can both read and write to Java's arrays; if you assume arrays are read-only then A[] <: B[] is indeed sound.
I would start with http://learnyouahaskell.com - Its Haskell based but it covers them pretty well.
Otherwise I use https://typelevel.org/cats/ for Scala daily and they have an _ok_ guide. But the best guide I've found for it was http://eed3si9n.com/herding-cats/
Alternately, "Learn You a Haskell" will provide a decent base.
Either of these will give you the base needed to explore higher-level concepts that can easily be used in Haskell, Scala, Purescript, OCaml, a few statically-typed functional languages, and to some extent, Rust.
A simple rule: if you're writing for a more general audience, always spell out your initializations the first time you say them.
That said, "popular" is a very contextual term. Within certain classes of programmers (and I don't mean the obvious tautological one) Haskell is extremely popular, and fit for purpose. I assume the same is true for Dart.
And I would add that, in those circles, the introduction of advanced HKT features like RankNTypes has been extremely successful.
C++ before 1998 was a very popular language, maybe even more popular relatively than it is now.
You can get 90% there without HKT and a similar developer experience.
Current rust has some implementations which are simil-functors and simil-monads.
This is just polishing the language by lifting a restriction.
True
> You can get 90% there without HKT and a similar developer experience.
Not really though. Unless similar developer experience means you suffer like hell and still have to deal with runtime errors and worse performance.
If you want/need more than that, I can explain more, but that at least gives you the full name.
I want a language designed by one very smart person, a dictator. I'm looking forward to Jai but for now the best native language is still C++. It also has too many features like Rust but at least it works.
That said, GATs fix a real pain point not actually shown in the article. And that's for impl return types in trait methods. You can have return impl types in normal functions, and even associated methods, just not in trait methods. I think the term for this is existential types. And they occur a lot with async. So async in traits is a real pain point this feature fixes.
The team aspect alone is a deal breaker for me--when you have a technology that attracts people who are intrinsically interested in the technology, good luck trying to get them to avoid certain parts of it. That's a losing battle.