I stopped with rust

Wow, that was quite the stream of consciousness. Sadly I have no idea what you were trying to say there.

3 Likes

I like C# but there are quite some crazy weaknesses. The nullable context is a mess. Something which in Rust is (of course) insanely clean.
The amount of possibilities for storing a sequence of elements is countless.
From array to readonlylisit, spans, readonlyspans, fixed buffers etc. etc.
Emergency solutions in my opinion.
I like the cleanness of Rust: array or vec. That's it!
Memory management is a drama because of the GC. So low level performance is very difficult if not impossible. I stopped trying that.
But I must admit, that language has become faster. 10 years ago it was slow.

That's true for many languages - notably Javascript. Python not so much, but each new release is usually faster than the one before...

At a tiny company (I was the only employee :slight_smile: ), I did NewtonScrip (that I loved). Then Steve Jobs came back to Apple and soon axed the Newton. And I had to do Web applications for Oracle-DB in PL/SQL. I showed him WebObjects (that was just a joy for exactly the same thing). As I had to admit that WO is from Apple, that option was already over before it even started.
I quit that job, because PL/SQL was driving me crazy.

Then I made WebObjects for some time as a freelancer.

Note that ReadOnlySpan<T> in C# is basically just a (slightly worse) &[T] from Rust.

Basically it's proof that any language that aims at high performance needs things like the difference between &str and String, and that it's not just a Rust-ism. (See also String vs StringBuilder in C#.)

5 Likes

This is more of a library thing than a language thing. This it true that standard library gently but sturdily pushes towards using &[T]. Mostly thanks to designing many of its APIs around slices and making using them in return really nice to use. And of course by extension its owned counterpart is Vec<T>, so you often end up holding Vecs in structs and taking slices as function arguments.

However all of this is entirely optional. Even in the standard library there is the collections module. And if you don't need dynamic reallocation you can use Box<[T]> or [A]Rc<[T]> to hold owned/shared data. And when you go to the third party libraries, there are thousands crates implementing various data-structures.

What is great about Rust, is that you can use the defaults most of the time, and end up with a really performant code, but when you need something custom-tailored, nothing in the language will stop you to do that.

1 Like

Just a status update:
After a few days of using Zig, I am a big fan already.
Now I was wondering: Why is that exactly?

I already mentioned the unreadable syntax of Rust, the BorrowChecker etc.

Probably the main answer is: simplicity.
For example error handling in Zig is extremely elegant. As well as null's (Options<> in Rust).

Another big thing is references. I just like pointers. They are fast, clear and readable (and of course unsafe).
In computerworld it is already very difficult to model situations to structs etc.
Rust makes this even more difficult in my experience.
Storing indexes instead of pointers does not solve anything.

Of course I am not a Rust expert or even a programming-expert: there are many people who are better and have more knowledge than I will ever have.

But I just wanted to share this feelings.

2 Likes

It's funny how we are all different. I do agree that Rust syntax can get horribly noisy with all kind of random seeming punctuation but none of that exists in any Rust code I write. Being simple minded my source looks pretty straightforward, any Javascript or user of C like languages would not have difficulty with it. I almost never need to use lifetime tick marks and so on.

I did understand your comment about pointers/references. I use references pretty much as I would pointers in C. I'm not going to worry about using & instead of *.

Strong indexes certainly does solve problems. It does so as much in C (or likely Zig) as it does in Rust. It's all about gaining performance by using arrays of things rather than pointers to things allocated in random memory locations.

I'm sure Zig is fine, many respected developers report so. But my brain is too small to accommodate yet another language.

2 Likes

Ha, know the feeling.

Being simple minded my source looks pretty straightforward
That is good. I saw one library for TT fonts which I really liked in that regard.

Strong indexes certainly does solve problems. It does so as much in C (or likely Zig) as it does in Rust. It's all about gaining performance by using arrays of things rather than pointers to things allocated in random memory locations.

In most if not all of my cases it is a reference to an array-item somewhere else.
My point was that there is not much difference storing an invalid pointer or an invalid index.
The index is checkable of course....

That is true. Using a pointer to nowhere in C is a screw up as much as using an index to the wrong element in an array in Rust.

Most of us don't need to go as far using such techniques. References (pointers) work well. I'm grateful that Rust then prevents me from making silly mistakes with them.

Thing is, if you use a language that allows unsafe use of pointers you have to do all that tedious checking for correctness yourself. Or do all that tedious debugging later.

The simplified canonical example of indices being better is:

for item in list {
  if ... {
    list.push(...); // Might re-allocate list!
  }
  item.froble(); // Boom! use of pointer to freed data
}

Sure, you can use incorrect indices, but you can also do that with pointers, so indices are strictly less dangerous.

Iterators can't use incorrect values, but can be invalidated like pointers (because they're just pointers internally), so they're something of a trade-off.

Yeah but that kind of childish mistakes you smell from 1 km distance.

BTW there was a funnny thing on youtube about Parameter Reference Optimization where this kind of error could occur if you're a lousy programmer.
list.add(list.items[0]);

Sure, you can use incorrect indices, but you can also do that with pointers, so indices are strictly less dangerous.

true

Yes, in such a simple example. Problems come when that kind of reallocation happens somewhere but the use of the resulting incorrect pointer happens a long way away in a different function, module, file, thread and perhaps a long time later. On large projects these mistakes can get very hard to track down.

4 Likes

I am not sure I am following the discussion about indices (or properly understand your snippet), but the following code does not compile:

fn main() {
    let mut list = vec![0u32; 10];
    for (k, item) in list.iter_mut().enumerate() {
        println!("{}", item);
        if k == 2 {
            list.push(3);  // comment this out and the code is fine
        }
    }
}

Edit : correct if I am wrong, but I understood you were implying your code snippet would create a run-time bug. This main() shows that such bug would not be possible, because the borrow checker would not allow it at compile time. Did I misunderstand something?

They were showing the safety issue when not using indexes, which is prevented by the borrow checker. Using indexes would be:

let mut list = vec![0u32; 10];
let len = list.len();
for k in 0..len {
    let item = list[k];
    println!("{}", item);
    if k == 2 {
        list.push(3);
    }
}

Well... let's say you have objects that come into existence and disappear later, coming and going in some arbitrary order, as your program runs. Imagine also that these objects can refer to each other some how, then you have some kind of linked list or tree or other data structure.

How are you going to store them and keep track of them? How are you going to implement the way you want them to refer to each other?

  1. Allocate them one at a time and keep a pointer to each one some place. Use pointers (memory addresses) as references that an object can hold to reference some other object(s).

  2. Have a big array each element of which is your object type. Just store your objects data in those elements. Use array indices within each object to refer to the objects which are also somewhere in that array.

So basically it is objects hanging around in random memory locations accessed by memory addresses. Or it's objects stored in an array accessed by indices into that array.

Case 1 will likely show up all kind of issues in Rust as it does not like multiple mutable references to things and all that hassle with lifetimes and ownership. You will be doing lots of memory allocations and deallocations as the objects are born and die.

Case 2 is easy. Any number of array indices can refer to a single element within the array. Rust does not care.

Except of course in case 2 you have to keep track of what array elements are in use and hold valid objects and what array elements are out of use and can be reused later.

And in case 2 if you make mistakes you may well have live objects holding indices that refer to dead objects. Rust cannot help you like it normally does.

In general I do avoid pointers as the plague when the data (vec) is volatile (added and disappearing during processing). And I would avoid - if possible - using 'reference' indexes as well in that case.

To be clear, in my description above I expect the Vec in which all this is happening to exist for the life of the program (or at least the bunch of objects in question) and need never change size. The upshot is that no objects ever actually get created after Vec creation and they never get destroyed. So the borrow checker never has anything to say about it. As long as we are referencing, object to object, via indices.

Thing is, if you want to build a complex data structure, a linked list, tree, graph, whatever you need nodes that reference other nodes. You need references/pointers or you can do it with indices. What else have we?

If you never want it to change size, then you can use a Box<[T]>.

The situation where it's trivially easy to use indices is an append only list, but yes, maintaining indices can be a pain in the butt. But the point is that it's no more than they are with pointers into a vector.

If you really want easy access to an object without lifetime concerns or the runtime cost of Rc/Arc, you always have Box::leak

One thing you give up with usize for indexes is having a strongly typed reference. A newtype wrapper can address that. And if you can tolerate larger index values, generational indexes can be used to detect accidental use of an index for an element that has been removed.

1 Like