For real Beginners to programming Rust can be hard, because concepts like pointers are hard in most languages that use them.
For Programmers that try Rust out Rust can be hard because they are stubornly trying to program Rust as if it were Python/Java/C/C++/Foo – but it isn't.
Rust as a language makes certain types of approaches (or anti patterns) nearly impossible. In the beginning it happens quite often that after a day of struggle you will erase a Rube-Goldberg-Machine that does something simple and replace it with the right line of code that does exactly the same while beeing way more extensible.
If you find yourself building more and more elaborate structures just to get something very simple done, the problem is very likely your approach (e.g. trying to implement a OOP structure instead of solving the problem) – at least that is what happend to me a lot.
Rust's borrowing and ownership concepts, as well as traits and generics make it extremely powerful, but these concepts lend themselves better to certain ways of structuring code than to others. Learning something is in my opinion always about allowing something to change your perspective. If you are not allowing the thing you are learning to change you, chances are that you are not learning, but judging.
From my friends that have tried rust, creating a graph or doubly linked list without using some special structures is fairly painful or impossible. You can create a graph by using some sort of adjacency matrix or some other kind of structure that keeps ownership in some sort of tree without much pain, but that has it's own downsides.
This was also my experience. It's a fine language if you can't (or more usually "won't" for spurious reasons) use a garbage collector. However a lot of things are just easier to do using a GC, and I suspect in many, not all, of the use cases of Rust, a GC would be just fine.
(Also I have to rant here a bit: Yes I know the GC you used in Java or Emacs in 1997 was terrible, but modern GCs are very good indeed)
The garbage collector in 80s MS BASIC was incredibly inefficient. The routine was written on to minimize the amount of memory to perform the GC, something like 8 bytes.
It would simply iterate through every entry in the temp string stack and then all the entries in the symbol table to find the string with the lowest address above whatever was the previous lowest address. After that full sweep, it would move that string to its new home and adjust pointers to it, then set the new low bound to the string just identified. It would keep iterating until all strings had been visited.
As a result, it required (n2)/2 sweeps of memory (n=# of strings). Having BASIC lock up for 30 seconds every once in awhile was just how it went.
[ and in case someone is going to correct me, it might have swept from top of memory to bottom, I have forgotten the details ]
Ref counting is a poor kind of garbage collection. In particular it has a very large overhead: typical GC has an overhead of a couple of bits per allocation, but ref counting usually wants to allocate an extra word (eg. 32 or even 64 bits) per allocation.
Another actually more serious problem is that typical ref counting implementations mix the allocated data with the references, so that when you inc/dec the reference you dirty a cache line containing the data. There exist implementations which improve on this by storing the references in a separate part of memory, but they are not how it's commonly implemented.
...annnnd this is probably a good example of the frustration that comes from solving familiar problems in a new paradigm, before the lightbulb clicks on.
Disclaimers: I'm only lightly familiar with Rust, and linked-lists are perfectly reasonable solutions _in the proper context and paradigm_.
So the problem to be solved is that we want to a) have collection of items which have a very strict ordering; b) be able to directly access the first and last item in that order; c) given a particular item, find the item which is immediately before/after it; d) when removing or adding an item, the relative ordering of the other items is undisturbed. Oh yeah, and e) we want it simple and efficient.
Doubly-linked lists are, of course, a very common solution used for these kinds of problems. They are perfectly suitable _if our paradigm is_ "I hereby assert that somehow, external to the compiler, I've verified that all list manipulation is being done correctly in all circumstances." If, however, our paradigm is "I want the compiler to automatically guarentee lots of these invariants" we run into some problems:
1) The data structure itself (each item has two pointers, and there are two global pointers for "head" and "tail") makes very few guarentees. Just one, actually: "each pointer will either point to an object, or will be null (or its moral equivalent)". There just isn't much compile-time meat for the compiler to chew on.
2) Linked-lists typically have (relatively) persistent scope, and exist outside of the lexical scope of whatever code block is immediately operating upon them. Again, it doesn't give the compiler much to go on.
3) Without managed memory (GC), there's no way for the compiler to guarentee that a pointed-to object still exists.
4) There's no built-in guarentee that the "head" and "tail" pointers actually point to the first/last object.
5) There's no guarentee of overall ordering (if a.next == b, then b.prev == a).
6) There's no guarentee of even a consistent view of the items in the collection (head == a, a.next == b, but b.prev == null/end-of-list).
7) There's no way for the compiler to guarentee, when viewing the collection as a whole, that modifications are atomic or thread-safe.
Yes, it's possible to take care of these issues in a non-Rust paradigm by making the structure of the list itself opaque, and only exposing insert/remove functions which enforce all the invariants. (I think Rust itself can do this with unsafe code.) However, you're still left with difficulties:
8) If list items are opaque containers, how do you guarentee that the payload object continues to exist?
9) How do you guarentee payload object ownership, atomicity, mutability, or other properties?
10) How do you guarentee that these needed invariants are held transitively?
Now the Rust paradigm, of course, isn't the only way to deal with these issues. But whether you're using Rust, managed-memory, C++ smart pointers, immutability guarentees, etc., you're going to need to do things that are unnatural in the other paradigms. Rust has the benefit of automatically enforcing lots of these invariants without having to do copying, worrying about shallow vs. deep copying, transitivey, etc.
Have not used rust but I'm assuming it's because when you're down a rat hole rust stops you cold at every turn. You need to back up and rethink what you're trying to do. But when you don't have a good handle on what rust is complaining about you can't see that.
The books are incredibly well written. When people are “stopped cold at every turn” they are usually programmers who come to Rust with a metric ton of existing assumptions how to solve certain problems, without really thinking about the concepts of the language itself.
When a C++ programmer starts with python they will write incedibly “unpythonic” code, when they do the same thing in Rust, it just won’t compile.
IMO Rust is very straightforward and the std library is incredibly good. But you have to really understand certain core concepts and what they mean in terms of structuring your code.
Just like that C++ programmer needed to learn what is ideomatic python looks like and why it makes a lot more sense to write python that way, you need to find the rust way of things. But this is the same for every progtamming language out there..
One of the reasons why when I learn a new language I deliberately research the idiomatic way to write that language, it saves a bunch of trying to hammer a square peg into a round hole time.
Also some languages purely because of the domain they are in can do things in ways that others can't (trivially) do so if you treat each language as "foo" with different syntax you end up hobbling yourself to the lowest common denominator of "foo"'s features across every language you use.
Sure. I think it's one of Go's strongest points. But it's still a counter-example to the parent's point. Learning a new language does not inherently require a lot of effort, and so expecting it to be reasonably easy, especially when you're an experienced developer, doesn't seem unreasonable to me.
Go does still suffer from people bringing idioms and assumptions from other languages, and that has leaked somewhat into common libraries. I've been away from it for a while (mostly doing very low-level C on microcontrollers these days), but there was a lot of Go code I came across that just screamed "This was written by a Java developer who is learning Go!"
I had a great time writing Go web services, and I definitely agree that it was easy to pick up, both for me and other teammates. Figuring out the idiomatic way to do things wasn't always straightforward though, although getting up and running was easy.
There's plenty of languages which take limited / little efforts to learn. So I'd say more that the unfamiliar concepts need to stew or sink in.
One way is to just keep at it until it clicks. An other way is to not do that, but once you've got the words for something you start seeing the issue everywhere, and next time around it makes a lot more sense.