Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

To a large extent yes, but Rust adds more dimensions to the type system: ownership, shared vs exclusive access, thread safety, mutually-exclusive fields (sum types).

Ownership/borrowing clarifies whether function arguments are given only temporarily to view during the call, or whether they're given to the function to keep and use exclusively. This ensures there won't be any surprise action at distance when the data is mutated, because it's always clear who can do that. In large programs, and when using 3rd party libraries, this is incredibly useful. Compare that to that golang, which has types for slices, but the type system has no opinion on whether data can be appended to a slice or not (what happens depends on capacity at runtime), and you can't lend a slice as a temporary read-only view (without hiding it behind an abstraction that isn't a slice type any more).

Thread safety in the type system reliably catches at compile time a class of data race errors that in other languages could be nearly impossible to find and debug, or at very least would require catching at run time under a sanitizer.





What annoys me about borrowing is, that my default mode of operating is to not mutate things if I can avoid it, and I go to some length in avoiding it, but Rust then forces me to copy or clone, to be able to use things, that I won't mutate anyway, after passing them to another procedure. That creates a lot of mental and syntactical overhead. While in an FP language you are passing values and the assumption is already, that you will not mutate things you pass as arguments and as such there is no need to have extra stuff to do, in order to pass things and later still use them.

Basically, I don't need ownership, if I don't mutate things. It would be nice to have ownership as a concept, in case I do decide to mutate things, but it sucks to have to pay attention to it, when I don't mutate and to carry that around all the time in the code.


It sounds like you may not actually know Rust then because non-owning borrow and ownership are directly expressible within the type system:

Non-owning non mutating borrow that doesn’t require you to clone/copy:

    fn foo(v: &SomeValue)
Transfer of ownership, no clone/copy needed, non mutating:

    fn foo(v: SomeValue)
Transfer of ownership, foo can mutate:

    fn foo(mut v: SomeValue)

AFAIK rust already supports all the different expressivity you’re asking for. But if you need two things to maintain ownership over a value, then you have to clone by definition, wrapping in Rc/Arc as needed if you want a single version of the underlying value. You may need to do more syntax juggling than with F# (I don’t know the language so I can’t speak to it) but that’s a tradeoff of being a system engineering language and targeting a completely different spot on the perf target.

Can you give examples of the calls for these procedures? Because in my experience when I pass a value (not a reference), then I must borrow the value and cannot use it later in the calling procedure. Passing a reference of course is something different. That comes with its own additional syntax that is needed for when you want to do something with the thing that is referred to.

> Because in my experience when I pass a value (not a reference), then I must borrow the value and cannot use it later in the calling procedure.

Ah, you are confused on terminology. Borrowing is a thing that only happens when you make references. What you are doing when you pass a non-copy value is moving it.

Generally, anything that is not copy you pass to a function should be a (non-mut) reference unless it's specifically needed to be something else. This allows you to borrow it in the callee, which means the caller gets it back after the call. That's the workflow that the type system works best with, thanks to autoref having all your functions use borrowed values is the most convenient way to write code.

Note that when you pass a value type to a function, in Rust that is always a copy. For non-copy types, that just means move semantics meaning you also must stop using it at the call site. You should not deal with this in general by calling clone on everything, but instead should derive copy on the types for which it makes sense (small, value semantics), and use borrowed references for the rest.


It is not possible then to pass a value (not a reference) and not implement or derive Copy or Clone, if I understand you correctly. That was my impression earlier. Other languages let you pass a value, and I just don't mutate that, if I can help it. I usually don't want to pass a reference, as that involves syntactical "work" when wanting to use the referenced thing in the callee. In many other languages I get that at no syntactical cost. I pass the thing by its name and I can use it in the callee and in the caller after the call.

What I would prefer is, that Rust only cares about whether I use it in the caller after the call, if I pass a mutable value, because in that case, of course it could be unsafe, if the callee mutates it.

Sometimes Copy cannot be derived and then one needs to implement it or Clone. A few months ago I used Rust again for a short duration, and I had that case. If I recall correctly it was some Serde struct and Copy could not be derived, because the struct had a String or &str inside it. That should a be fairly common case.


You can pass a value that is neither copy or clone, but then it gets moved into the callee, and is no longer available in the caller.

Note that calling by value is expensive for large types. What those other languages do is just always call by reference, which you seem to confuse for calling by value.

Rust can certainly not do what you would prefer. In order to typecheck a function, Rust only needs the code of that function, and the type defitions of everything else, the contents of the functions don't matter. This is a very good rule, which makes code much easier to read.


Yes, Rust will not automatically turn a value into a reference for you. A reference is the semantic you desire. If you have a value, you’re gonna have to toss & on it. That’s the idiomatic way to do this, not to pass a value and clone it.

&str is Copy, String is not.


> Other languages let you pass a value, and I just don't mutate that, if I can help it

How do they do that without either taking a reference or copying/cloning automatically for you? Would be helpful if you provide an example.


I did not state, that they don't automatically copy or clone things.

I might be wrong what they actually do though. It seems I merely dislike the need to specify & for arguments and then having to deal with the fact, that inside procedures I cannot treat them as values, but need to stay aware, that they are merely references.


C++ auto copies as well, it's just a feature of value semantics. References must be taken manually - versus Java or C#, where we assume reference and then have to explicitly say copy. Rust, I believe, usually moves by default - not copy, but close - for most types.

The nice thing about value semantics is they are very safe and can be very performant. Like in PHP, if we take array that's a copy. But not really - it's secretly COW under the hood. So it's actually very fast if we don't mutate, but we get the safety of value semantics anyway.


Rust will transparently copy values for types that declare the Copy trait. But the default is move which is probably what C++ would have chosen had they had the 30+ years of language research to experiment with that Rust did + some other language to observe what worked and what didn't.

The pattern you're looking for is:

``` fn operate_on_a(a: A) -> A { // do whatever as long as this scope still owns A a } ```


If all you're doing is immutable access, you are perfectly free to immutably borrow the value as many times as you want (even in a multithreaded environment provided T is Send):

    let v = SomeValue { ... }
    foo(&v);
    foo(&v);
    eprintln!("{}", v.xyz);
You have to take a reference. I'm not sure how you'd like to represent "I pass a non-reference value to a function but still retain ownership without copying" - like what if foo stored the value somewhere? Without a clone/copy to give an isolated instance, you've potentially now got two owners - foo and the caller of foo which isn't legal as ownership is strictly unique. If F# lets you do this, it's likely only because it's generating an implicit copy for you (which Rust is will do transparently for you when you declare your type inheriting Copy).

But otherwise I'm not clear what ownership semantics you're trying to express - would be helpful if you could give an example.


I share the same pet peeve, it's not that it's not possible. It's that I would prefer copy and or move to be the default when assigning stuff. Kind of like the experience you get using STL stuff in c++.

Copy can’t be for types that aren’t copyable because there could be huge performance cliffs hiding (eg copying a huge vector which is the default in c++).

But Rust always moves by default when assigning so I’m not sure what your complaint is. If the type declares it implements Copy then Rust will automatically copy it on assignment if there’s conflicting ownership.


Borrowing isn't for mutability, but for memory management and limiting data access to a static scope. It just happens that there's an easy syntax for borrowing as shared or exclusive at the same time.

Owned objects are exclusively owned by default, but wrapping them in Rc/Arc makes them shared too.

Shared mutable state is the root of all evil. FP languages solve it by banning mutation, but Rust can flip between banning mutation or banning sharing. Mutable objects that aren't shared can't cause unexpected side effects (at least not any more than Rust's shared references).


Depends on the FP language though, they are only values in the logic sense, they can be reference as well, hence the various ways to do equality.

Ownership serves another important purpose: it determines when a value is freed.

I guess it is then a necessary complication of the language, as it doesn't have garbage collection, and as such doesn't notice, when values go out of scope of all closures that reference them?

Yes, but it’s more subtle than that. What Rust does is track when the object goes out of scope, and will make sure that any closures that reference it live for a shorter time than that. Sort of backwards of what you’re asking.

The borrow checker is a compile-time garbage collector. If you think about it in that sense, you can understand a lot of the ways it restricts you.

> While in an FP language you are passing values

By passing values do you mean 'moving'? Like not passing reference?


Yes, I guess in Rust terms, that is called moving. However, when I have some code that "moves" the value into another procedure, then the code after that call, can no longer use the moved value.

So I want to move a value, but also be able to use it after moving it, because I don't mutate it in that other function, where it got moved to. So it is actually more like copying, but without making a copy in memory.

It would be good, if Rust realized, that I don't have mutating calls anywhere and just lets me use the value. When I have a mutation going on, then of course the compiler should throw error, because that would be unsafe business.


I'm not sure how what you're describing is different from passing an immutable/shared reference.

If you call `foo(&value)` then `value` remains available in your calling scope after `foo` returns. If you don't mutate `value` in foo, and foo doesn't do anything other than derive a new value from `value`, then it sounds like a shared reference works for what you're describing?

Rust makes you be explicit as to whether you want to lend out the value or give the value away, which is a design decision, and Rust chooses that the bare syntax `value` is for moving and the `&value` syntax is for borrowing. Perhaps you're arguing that a shared immutable borrow should be the default syntax.

Apologies if I'm misunderstanding!


Couldn't you just pass a reference to your value (i.e. `&T`)? If you absolutely _need_ ownership the function you call could return back the owned value or you could use one of the mechanisms for shared ownership like `Rc<T>`. In a GC'd functional language, you're effectively getting the latter (although usually a different form of GC instead of reference counting)

I think I could. But then I would need to put & in front of every argument in every procedure definition and also deal with working with references inside the procedure, with the syntax that brings along.

Fair to be annoyed by this, but not very interesting: This is just a minor syntactical pattern that exists for a very good reason.

Syntax is generally the least interesting/important part of languages.


When you pass &variable, I don't think it affects the syntax inside the called function, does it?

Correct. If you then want to subsequently re-reference or dereference that reference (this happens sometimes), you'll need to accordingly `&` or `*` it, but if you're just using it as is, the bare syntactical `name` (whatever it happens to be) already refers to a reference.

Also, Rust does implicit dereferencing, so it's not that much of an issue in practice.

Readers would benefit from distinguishing effects systems from type systems - error handling, async code, ownership, pointers escaping, etc. are all better understood as effects because they pertain to usage of a value/type (though the usage constraints can depend on the type properties).

Similarly, Java sidesteps many of these issues in mostly using reference types, but ends up with a different classes of errors. So the C/pointer family static analysis can be quite distinct from that for JVM languages.

Swift is roughly on par with Rust wrt exclusivity and data-race safety, and is catching up on ownership.

Rust traits and macros are really a distinguishing feature, because they enable programmer-defined constraints (instead of just compiler-defined), which makes the standard library smaller.


Swift has such a long way to go in general ergonomics of its type system, it's so far behind compared to Rust. The fact that the type checker will just churn until it times out and asks the user to refactor so that it can make progress is such a joke to me, I don't understand how they shipped that with a straight face.

There's nothing wrong with this in principle, every type system must reject some valid programs. There's no such thing as a "100%" type system that will both accept all valid code and reject all non-valid code.

I'm not questioning the principle, I'm critiquing the implementation versus the competition. Swift doesn't do a good job, it throws up its hands too often.

If you solve 80% of the problems by spending 20% of the time, is it worth spending the 80% to solve 20% of the problems? Or even if it is, is it more valuable to ship the 80% complete solution first to get it into the hands of users while you work on the thornier version?

If someone else ships a 100% solution, or a solution that doesn't have the problems your potentially half-baked "80% solution" does, then you might be in trouble.

There's a fine line here: it matters a lot whether we're talking about a "sloppy" 80% solution that later causes problems and is incredibly hard to fix, or if it's a clean minimal subset, which restricts you (by being the minimal thing everyone agrees on) but doesn't have any serious design flaws.


Sure. And I'm not sure the type checker failing on certain corner cases and asking you to alter the code to be friendlier is a huge roadblock if it rarely comes up in practice for the vast majority of developers.

Swift even if catching up a bit is probably not going to impose strict border between safe and unsafe.

I think tagged unions with exhaustive type checking and no nulls are the two killer features for correctness

Apologies for the non sequitur

Do you think Zig is a valid challenger to Rust for this kind of programming?


Zig's trying to be a "nicer C" that's easy to learn and fast to compile. It's a great language with a lot of neat design, and definitely setting itself up to be a "valid challenger" in a lot of the systems-y, performance-focused domains Rust targets. But it's not trying to compete with Rust on the safety/program correctness front.

Almost none of the Rust features discussed in this subthread are present in Zig, such as ownership, borrowing, shared vs. exclusive access, lifetimes, traits, RAII, or statically checked thread safety.


Thank you



Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: