Most active commenters
  • gf000(11)
  • ModernMech(10)
  • vlovich123(9)
  • zelphirkalt(7)
  • lmm(6)
  • pjmlp(4)
  • Tuna-Fish(3)
  • throwawayffffas(3)
  • akkad33(3)

←back to thread

517 points bkolobara | 111 comments | | HN request time: 0.962s | source | bottom
1. BinaryIgor ◴[] No.45042483[source]
Don't most of the benefits just come down to using a statically typed and thus compiled language? Be it Java, Go or C++; TypeScript is trickier, because it compiles to JavaScript and inherits some issues, but it's still fine.

I know that Rust provides some additional compile-time checks because of its stricter type system, but it doesn't come for free - it's harder to learn and arguably to read

replies(17): >>45042692 #>>45043045 #>>45043105 #>>45043148 #>>45043241 #>>45043589 #>>45044559 #>>45045202 #>>45045331 #>>45046496 #>>45047159 #>>45047203 #>>45047415 #>>45048640 #>>45048825 #>>45049254 #>>45050991 #
2. ViewTrick1002 ◴[] No.45042692[source]
Neither Go, Java or C++ would catch that concurrency bug.
replies(2): >>45043212 #>>45050050 #
3. keybored ◴[] No.45043045[source]
You ask a question in your first paragraph which you answer in the second.
4. pornel ◴[] No.45043105[source]
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.

replies(4): >>45043414 #>>45043440 #>>45047837 #>>45052706 #
5. arwhatever ◴[] No.45043148[source]
I might suspect that if you are lumping all statically-typed languages into a single bucket without making particular distinction among them, then you might not have fully internalized the implications of union (aka Rust enum aka sum) typed data structures combined with exhaustive pattern matching.

I like to call it getting "union-pilled" and it's really hard to accept otherwise statically-typed languages once you become familiar.

replies(3): >>45043455 #>>45043677 #>>45044134 #
6. Const-me ◴[] No.45043212[source]
C# would catch the bug at compile time, just like Rust.

https://www.rocksolidknowledge.com/articles/locking-asyncawa...

replies(2): >>45044178 #>>45046051 #
7. ModernMech ◴[] No.45043241[source]
> statically typed and thus compiled

Statically typed does not imply compiled. You can interpret a statically typed language, for instance. And not every compiled language is all that static.

For example, C is statically typed, but also has the ability to play pointer typecasting trickery. So how much can the compiler ever guarantee anything, really? It can't, and we've seen the result is brittle artifacts from C.

Rust is statically-typed and it has all kinds of restrictions on what you can do with those types. You can't just pointer cast one thing to another in Rust, that's going to be rejected by the compiler outright. So Rust code has to meet a higher bar of "static" than most languages that call themselves "static".

Type casting is just one way Rust does this, other ways have been mentioned. They all add up and the result is Rust artifacts are safter and more secure.

replies(2): >>45043672 #>>45044463 #
8. zelphirkalt ◴[] No.45043414[source]
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.

replies(5): >>45043496 #>>45043550 #>>45043678 #>>45044243 #>>45050174 #
9. w10-1 ◴[] No.45043440[source]
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.

replies(2): >>45043497 #>>45043590 #
10. JoshTriplett ◴[] No.45043455[source]
Or the fact that Rust's type system includes things like Send and Sync, which aren't tracked and enforced in many otherwise-statically-typed languages.

C is statically typed, but its type system tracks much less.

replies(1): >>45044427 #
11. arnsholt ◴[] No.45043496{3}[source]
Ownership serves another important purpose: it determines when a value is freed.
replies(1): >>45044466 #
12. timeon ◴[] No.45043497{3}[source]
Swift even if catching up a bit is probably not going to impose strict border between safe and unsafe.
13. timeon ◴[] No.45043550{3}[source]
> While in an FP language you are passing values

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

replies(1): >>45044453 #
14. marcosdumay ◴[] No.45043589[source]
> Don't most of the benefits just come down to using a statically typed and thus compiled language?

Doesn't have to be compiled to be statically typed... but yeah, probably.

> Be it Java, Go or C++;

Lol! No. All static type systems aren't the same.

TypeScript would be the only one of your examples that brings the same benefit. But the entire system is broken due to infinite JS Wats it has to be compatible with.

> it's harder to learn and arguably to read

It's easier to learn it properly, harder to vibe pushing something into it until it seems to works. Granted, vibe pushing code into seemingly working is a huge part of initial learning to code, so yeah, don't pick Rust as your first language.

It's absolutely not harder to read.

15. ModernMech ◴[] No.45043590{3}[source]
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.
replies(2): >>45044274 #>>45048511 #
16. tialaramex ◴[] No.45043672[source]
> You can't just pointer cast one thing to another in Rust, that's going to be rejected by the compiler

You can't safely do this yourself. That is, you couldn't write safe Rust which performs this operation for two arbitrary things. But Rust of course does do this, actually quite a lot, because if we're careful it's entirely safe.

That famous Quake 3 Arena "Fast inverse square root" which involves type puns? You can just write that in safe Rust and it'll work fine. You shouldn't - on any even vaguely modern hardware the CPU can do this operation faster anyway - but if you insist it's trivial to write it, just slower.

Why can you do that? Well, on all the hardware you'd realistically run Rust on the 32-bit integer types and the 32-bit floating types are the exact same size (duh), same bit order and so on. The CPU does not actually give a shit whether this 32-bit aligned and 32-bit sized value "is" an integer or a floating point number, so "transforming" f32 to u32 or u32 to f32 emits zero CPU instructions, exactly like the rather hairier looking C. So all the Rust standard library has to do is promise that this is OK which on every supported Rust platform it is. If some day they adopted some wheezing 1980s CPU where that can't work they'd have to write custom code for that platform, but so would John Carmack under the same conditions.

replies(1): >>45043754 #
17. ModernMech ◴[] No.45043677[source]
enums + match expressions + tagged unions are the secret sauce of Rust.
replies(2): >>45047622 #>>45050213 #
18. pornel ◴[] No.45043678{3}[source]
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).

19. ModernMech ◴[] No.45043754{3}[source]
> because if we're careful it's entirely safe.

The thesis of Rust is that in aggregate, everyone can't be careful, therefore allowing anyone to do it (by default) is entirely unsafe.

Of course you can do unsafe things in Rust, but relegating that work to the room at the back of the video store labeled "adults only" has the effect of raising code quality for everyone. It turns out if you put up some hoops to jump through before you can access the footguns, people who shouldn't be wielding them don't, and average code quality goes up.

20. b_e_n_t_o_n ◴[] No.45044134[source]
Afaik they aren't true unions but sum types, which have different implications.

And fwiw I've used unions in typescript extensively and I'm not convinced that they're a good idea. They give you a certain flexibility to writing code, yes, does that flexibility lead to good design choices, idk.

replies(1): >>45044844 #
21. notfed ◴[] No.45044178{3}[source]
It's almost as if this post was written in direct response to TFA to brag about how far ahead C# has been for over a decade.
replies(1): >>45050301 #
22. vlovich123 ◴[] No.45044243{3}[source]
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.
replies(2): >>45044516 #>>45049874 #
23. vlovich123 ◴[] No.45044274{4}[source]
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?
replies(1): >>45045674 #
24. 1718627440 ◴[] No.45044427{3}[source]
My interpretation is, that C doesn't have data types, but memory layout types.
replies(1): >>45053191 #
25. zelphirkalt ◴[] No.45044453{4}[source]
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.

replies(2): >>45044767 #>>45045055 #
26. 1718627440 ◴[] No.45044463[source]
> So how much can the compiler ever guarantee anything, really?

Well, the compiler is guaranteed that no mistakes will happen. It's the programmer who looses his guarantees in this case.

27. zelphirkalt ◴[] No.45044466{4}[source]
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?
replies(2): >>45045566 #>>45051647 #
28. zelphirkalt ◴[] No.45044516{4}[source]
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.
replies(2): >>45044985 #>>45045709 #
29. adamnemecek ◴[] No.45044559[source]
Rust is way more productive than any of the listed languages.
replies(1): >>45053605 #
30. asa400 ◴[] No.45044767{5}[source]
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!

31. kibwen ◴[] No.45044844{3}[source]
TypeScript unions are very different from Rust enums, and they lead to different design decisions. IMO Rust-style tagged unions are essential in any new language coming out today, it's really a pity that people didn't pick up on this in the 70s.
replies(1): >>45048647 #
32. Tuna-Fish ◴[] No.45044985{5}[source]
> 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.

replies(1): >>45045312 #
33. NIckGeek ◴[] No.45045055{5}[source]
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)
replies(1): >>45045332 #
34. jauntywundrkind ◴[] No.45045202[source]
One other major factor I'd throw on the heap: traits / implementation traits. They act as both an interface system and as a sort of Extension Method system (as seen in c#).

But where-as with interfaces, typically they require you early define what your class implements. Rust gives you a late-bound-ish (still compile time but not defined in the original type) / Inversion of Control way to take whatever you've got and define new things for it. In most languages what types a thing has are defined by the library, but Rust not just allows but is built entirely around taking very simple abstract thing and constructing bigger and bigger toolkits of stuff around them. Very Non-zero sum in ways that languages rarely are.

There's a ton of similarity to Extension Methods, where more can get added to the type. But traits / impls are built much more deeply into rust, are how everything works. Extension Methods are also, afaik, just methods, where-as with Rust you really adding new types that an existing defined-elsewhere thing can express.

I find it super shocking (and not because duh) that Rust's borrow checking gets all the focus. Because the type system is such a refreshing open ended late-defined reversal of type system dogma, of defining everything ahead of time. It seems like such a superpower of Rust that you can keep adding typiness to a thing, keep expanding what a thing can do. The inversion here is, imo, one of the real largely unseen sources of glory for why Rust keeps succeeding: you don't need to fully consider the entire type system of your program ahead of time, you can layer in typing onto existing types as you please, as fits, as makes sense, and that is a far more dynamic static type system than the same old highly constrained static type dreck we've suffered for decades. Massive break forward: static, but still rather dynamic (at compile time).

replies(3): >>45047964 #>>45050221 #>>45053484 #
35. zelphirkalt ◴[] No.45045312{6}[source]
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.

replies(4): >>45045542 #>>45045712 #>>45045719 #>>45046488 #
36. BobbyJo ◴[] No.45045331[source]
IMO, most of the terse syntax in Rust comes from the sugar they've added for error handling.
37. zelphirkalt ◴[] No.45045332{6}[source]
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.
replies(2): >>45045629 #>>45047259 #
38. steveklabnik ◴[] No.45045542{7}[source]
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.

39. steveklabnik ◴[] No.45045566{5}[source]
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.
40. Mond_ ◴[] No.45045629{7}[source]
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.

41. Mond_ ◴[] No.45045674{5}[source]
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.

replies(1): >>45045823 #
42. vlovich123 ◴[] No.45045709{5}[source]
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.

43. Tuna-Fish ◴[] No.45045712{7}[source]
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.

replies(1): >>45062978 #
44. vlovich123 ◴[] No.45045719{7}[source]
> 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.

replies(1): >>45046876 #
45. vlovich123 ◴[] No.45045823{6}[source]
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.
46. IshKebab ◴[] No.45046051{3}[source]
I don't know C# but it looks like they added a specific check just for locks, which is far less powerful than Rust's Send/Sync.
47. theLiminator ◴[] No.45046488{7}[source]
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 } ```

48. stocksinsmocks ◴[] No.45046496[source]
Yes, but more importantly writing a program that compiles in Rust guarantees you a front page spot on HN.
49. zelphirkalt ◴[] No.45046876{8}[source]
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.

replies(1): >>45047653 #
50. lmm ◴[] No.45047159[source]
> Don't most of the benefits just come down to using a statically typed and thus compiled language? Be it Java, Go or C++; TypeScript is trickier, because it compiles to JavaScript and inherits some issues, but it's still fine.

No. You have to have a certain amount of basic functionality in your type system; in particular, sum types, which surprisingly many languages still lack.

(Note that static typing does not require compilation or vice versa)

> I know that Rust provides some additional compile-time checks because of its stricter type system, but it doesn't come for free - it's harder to learn and arguably to read

ML-family languages are generally easier to learn and read if you start from them. It's just familiarity.

51. rvz ◴[] No.45047203[source]
> Don't most of the benefits just come down to using a statically typed and thus compiled language? Be it Java, Go or C++; TypeScript is trickier, because it compiles to JavaScript and inherits some issues, but it's still fine.

Yes. The type systems of these modern compiled languages are more sound than anything that Javascript and Typescript can ever provide.

Anyone using such languages that have a totally weak type system and a dynamic typing system as well is going to run into hundreds of headaches - hence why they love properly typed-systems such as Rust which actually is a well designed language.

52. pepa65 ◴[] No.45047259{7}[source]
When you pass &variable, I don't think it affects the syntax inside the called function, does it?
replies(1): >>45047610 #
53. saghm ◴[] No.45047415[source]
Yes, all four of them will have some checks that won't be present in a dynamic language, but the differences between them are large enough to be significant. Riding a bike and driving a car are both much faster than going on foot, but if you only view this as a "benefit that comes down to using wheels", you're missing some pretty meaningful details.
54. asa400 ◴[] No.45047610{8}[source]
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.
replies(1): >>45053161 #
55. mixmastamyk ◴[] No.45047622{3}[source]
Maybe I need to read it again, but I remember the Rust book saying… you can use enums like C, but what if instead you used them in this more concise way? (Match on members, without the container.) Ok, was able to proceed but don’t feel like I understand what they really are.
56. const_cast ◴[] No.45047653{9}[source]
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.

replies(1): >>45053644 #
57. bpicolo ◴[] No.45047837[source]
I think tagged unions with exhaustive type checking and no nulls are the two killer features for correctness
58. dwaltrip ◴[] No.45047964[source]
Any good articles or blog posts that go deeper on this? Sounds very interesting
59. zozbot234 ◴[] No.45048511{4}[source]
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.
replies(1): >>45048621 #
60. ModernMech ◴[] No.45048621{5}[source]
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.
61. rendaw ◴[] No.45048640[source]
Not all static type systems are equally expressive/safe/consistent - Java falls back to `Object` and runtime casts frequently, Go doesn't have enums, and C++ variants come with significant footguns and ergonomics issues since they were hacked on and aren't first class language features (i.e. safe access requires using `try/except` which is exclusive with other control structures).
replies(1): >>45053541 #
62. b_e_n_t_o_n ◴[] No.45048647{4}[source]
What differences do they lead to? I just find union types more flexible, with the perceived downside of losing some semantics on success/error results (but I don't think it's a problem personally). But I wouldn't say I'm super knowledgeable about Rust outside of writing some toy programs.

You could create your own Result<T, Error> type in TS but people don't really do that outside of ecosystems like Effect because there isn't usually a reason to.

replies(1): >>45083611 #
63. csomar ◴[] No.45048825[source]
Yes. What Rust adds is a better type system. Statically typed is as good as your Type system (which is why TypeScript still sucks).

The concurrency/safety/memory story is only valid in a few rare cases and I wish people didn't try to sell Rust for these features.

64. eptcyka ◴[] No.45049254[source]
The specific example used wouldn’t be caught by C++ or Java.
replies(1): >>45053573 #
65. throwawayffffas ◴[] No.45049874{4}[source]
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++.
replies(1): >>45050608 #
66. lenkite ◴[] No.45050050[source]
> Neither Go, Java or C++ would catch that concurrency bug.

That is incorrect. Java enforces that a monitor lock (or Lock) must be released by the same thread that acquired it. Attempting to unlock from a different thread throws IllegalMonitorStateException.

replies(1): >>45051022 #
67. pjmlp ◴[] No.45050174{3}[source]
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.
68. pjmlp ◴[] No.45050213{3}[source]
Like this code snippet?

    (* Expressions *)

    type Exp = 
          UnMinus of Exp
        | Plus of Exp * Exp
        | Minus of Exp * Exp
        | Times of Exp * Exp
        | Divides of Exp * Exp
        | Power of Exp * Exp
        | Real of float 
        | Var of string
        | FunCall of string * Exp
        | Fix of string * Exp
        ;;


    let rec tokenizer s =
        let (ch, chs) = split s in
        match ch with
              ' ' ->    tokenizer chs
            | '(' ->    LParTk:: (tokenizer chs)
            | ')' ->    RParTk:: (tokenizer chs)
            | '+' ->    PlusTk::(tokenizer chs)
            | '-' ->    MinusTk::(tokenizer chs)
            | '*' ->    TimesTk::(tokenizer chs)
            | '^' ->    PowerTk::(tokenizer chs)
            | '/' ->    DividesTk::(tokenizer chs)
            | '=' ->    AssignTk::(tokenizer chs)
            | ch when (ch >= 'A' && ch <= 'Z') ||
                      (ch >= 'a' && ch <= 'z') ->
                        let (id_str, chs) = get_id_str s
                        in (Keyword_or_Id id_str)::(tokenizer chs) 
            | ch when (ch >= '0' && ch <= '9') ->
                        let (fl_str, chs) = get_float_str s
                        in (RealTk (float (fl_str)))::(tokenizer chs)
            | '$' ->    if chs = "" then [] else raise (SyntaxError (""))
            | _ ->      raise (SyntaxError (SyntErr ()))
        ;;
Hint, this isn't Rust.
replies(1): >>45051601 #
69. pjmlp ◴[] No.45050221[source]
An approach that is done in Standard ML with functors.
70. tialaramex ◴[] No.45050301{4}[source]
The C# is basically showing the Monitor pattern, which was also a thing in Java last century.

The Rust Mutex is an Owning Mutex which is a different feature whose benefit is that you need to take the lock to get at the protected data, which averts situations where you forget in some code but not others and create sync problems - in C# those may go undetected or may trigger a runtime exception, no guarantees.

But perhaps even more importantly, and why other languages which could do the Owning Mutex often do not, Rust's borrow checking means the compiler will spot mistakes where you gave back the lock but retained access to the data it was protecting. So you're protected both ways - you can't forget to take the lock, and you also can't give it back without also giving back the access.

Monitors prevent only the second (and only partly), an Owning Mutex in most languages prevents the first, but Rust prevents both.

71. vlovich123 ◴[] No.45050608{5}[source]
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.

replies(1): >>45058180 #
72. akkad33 ◴[] No.45050991[source]
No. Other languages don't prevent concurrency related bugs like the one in the article. Rust has reference aliasing rules and lifetimes and send and sync traits. These things do not exist in Java and others, so will not prevent such bugs
73. rapsey ◴[] No.45051022{3}[source]
At runtime instead of compile time, which is the point of the thread.
74. lmm ◴[] No.45051601{4}[source]
Yes, the secret of Rust is that it offers both a) some important but slightly subtle language features from the late '70s that were sadly not present in Algol '52 and are therefore missing from popular lineages b) a couple of party tricks, in particular the ability to outperform C on silly microbenchmarks; b) is what leads people to adopt it and a) is what makes it non-awful to program in. Yes it's a damning indictment of programming culture than people did not adopt pre-Rust ML-family languages, but it could be worse, they could be not adopting Rust either.
replies(4): >>45051692 #>>45052023 #>>45052039 #>>45053227 #
75. pclmulqdq ◴[] No.45051647{5}[source]
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.
76. hollerith ◴[] No.45051692{5}[source]
>important but slightly subtle language features from the late '70s

Programming-language researchers didn't start investigating linear (or affine) types till 1989. Without the constraint that vectors, boxes, strings, etc, are linear, Rust cannot deliver its memory-safety guarantees (unless Rust were radically changed to rely on a garbage collecting runtime).

>it's a damning indictment of programming culture than people did not adopt pre-Rust ML-family languages

In pre-Rust ML-family languages, it is harder to reason about CPU usage, memory usage and memory locality than it is in languages like C and Rust. One reason for that is the need in pre-Rust ML-family langs for a garbage collector.

In summary, there are good reasons ML, Haskell, etc, never got as popular as Rust.

replies(1): >>45051931 #
77. lmm ◴[] No.45051931{6}[source]
> Programming-language researchers didn't start investigating linear (or affine) types till 1989.

Sure, but as ModernMech said, the vast majority of Rust's benefits come from having sum types and pattern matching.

> In pre-Rust ML-family languages, it is harder to reason about CPU usage, memory usage and memory locality than it is in languages like C and Rust.

Marginally harder for the first two and significantly harder for the last, sure. None of which is enough to matter in the overwhelming majority of cases where Rust is seeing use.

replies(1): >>45053314 #
78. pjmlp ◴[] No.45052023{5}[source]
C only got to its performance state, when optimizing compilers taking advantage of UB started being common thing, during the 8 and 16 bit home computer days they were hardly any better than writing Assembly by hand, hence why books like Zen of Assembly Language left such a mark.

So if we are speaking of optimizing compilers there is MLton, while ensuring that the application doesn't blow up in strange ways.

The problem is not people getting to learn these features from Rust, glad that they do, the issue is that they think Rust invented them.

replies(1): >>45052073 #
79. ModernMech ◴[] No.45052039{5}[source]
> Yes it's a damning indictment of programming culture than people did not adopt pre-Rust ML-family languages, but it could be worse, they could be not adopting Rust either.

I'll say for a long time I've been quite pleased on the general direction of the industry in terms of language design and industry trends around things like memory safety. For a good many years we've seen functional features being integrated into popular imperative languages, probably since map/reduce became a thing thanks to Google. So I'll us all credit for coming around eventually.

I'm more dismayed by the recent AI trend of asking an AI to write Python code and then just going with whatever it outputs. I can't say that seems like a step forward.

80. ModernMech ◴[] No.45052073{6}[source]
> the issue is that they think Rust invented them

Sorry, my post wasn't to imply Rust invented those things. My point was Rust's success as a language is due to those features.

Of course there's more to it, but what Rust really does right is blend functional and imperative styles. The "match" statement is a great way to bring functional concepts to imperative programmers, because it "feels" like a familiar switch statement, but with super powers. So it's a good "gateway drug" if you will, because the benefit is quickly realized ("Oh, it caught that edge case for me before it became a problem at runtime, that would have been a headache...").

From there, you can learn how to use match as an expression, and then you start to wonder why "if" isn't an expression in every language. After that you're hooked.

81. mason_mpls ◴[] No.45052706[source]
Apologies for the non sequitur

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

replies(1): >>45053217 #
82. gf000 ◴[] No.45053161{9}[source]
Also, Rust does implicit dereferencing, so it's not that much of an issue in practice.
83. gf000 ◴[] No.45053191{4}[source]
Arguably hardly even that, as memory layout for many in-built types are target and/or compiler-specific.
84. NobodyNada ◴[] No.45053217{3}[source]
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.

replies(1): >>45053699 #
85. gf000 ◴[] No.45053227{5}[source]
I mean, "fearless concurrency", while a hyped-up phrase that is definitely exaggerated, compared to the C-world where you are already blowing off your leg in single-threaded code, let alone thinking of multiple threads Rust is an insanely huge win. And it shows on e.g. the small Unix tool rewrites.

Sure, rewrites are most often better on simply being a rewrite, but the kind of parallel processing they do may not be feasible in C.

86. gf000 ◴[] No.45053314{7}[source]
> Sure, but as ModernMech said, the vast majority of Rust's benefits come from having sum types and pattern matching.

Doubt. There were lots of languages giving you just that, and they never had this amount of hype. See Scala, OCaml, Haskell, etc.

Rust has one unique ability, and many shared by other languages. It's quite clearly popular for the former (though languages are a packages, so of course it's a well put together language all around).

replies(2): >>45054187 #>>45057698 #
87. gf000 ◴[] No.45053484[source]
I mean, is that not just [1] type classes? They are not a new concept, they could be grandparents!

[1] not trying to take away anything from the designers, getting it right in combination with all the other features is a huge feat!

88. gf000 ◴[] No.45053541[source]
> Java falls back to `Object` and runtime casts frequently

Is it frequently? Generics are definitely not as nice as they could be, but they are surprisingly "sufficient" for almost any library, e.g. a full on type-safe SQL DSL like JOOQ. Unsafe casts are very rare, and where you do need Object and stuff are very dynamic stuff where it's impossible to extend compile-time guarantees even theoretically (e.g. runtime code generation, dynamic code loading, etc - people often forget about these use cases, not everything can work in a closed universe)

replies(1): >>45076626 #
89. gf000 ◴[] No.45053573[source]
The redirect one would funnily be caught in Java. For the simple reason of not having properties, it would have to be a method call, where the author's wrong assumption would no longer apply.
90. gf000 ◴[] No.45053605[source]
Extraordinary claims require.. at least some kind of evidence.

There is very little research comparing PL productivity, because it is very hard to do them properly.

replies(1): >>45057691 #
91. vlovich123 ◴[] No.45053644{10}[source]
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.
92. mason_mpls ◴[] No.45053699{4}[source]
Thank you
93. ModernMech ◴[] No.45054187{8}[source]
Scala, OCaml, and Haskell all approach programming from a functional-first perspective. What no other language did before Rust was to bring those features to such a well-designed imperative core.

And while this was necessary to Rust's success, I don't think it was sufficient, insofar as it also needed a good deal of corporate backing, a great and welcoming community, and luck to be at the right place at the right time.

Haskell never tried to be more than a academic language targeting researchers. OCaml never had a big community or corporate backing. Scala never really had a niche; the most salient reason to use it is if you're already in the Java ecosystem and you want to write functional code. The value propositions for each are very different, so these language didn't receive the same reaction as Rust despite offering similar features.

replies(1): >>45054942 #
94. gf000 ◴[] No.45054942{9}[source]
Scala is distinctively a mixed FP and OOP language. There are functional proponents doing full monads and whatnot, but there is just as much follower of a more balanced approach (see Li Haoyi's libs). Though Scala definitely had/have quite a niche around it.
replies(1): >>45055514 #
95. ModernMech ◴[] No.45055514{10}[source]
Well I think Scala's main problem here is it can still have runtime errors with null values, so it doesn't really have the same runtime safety guarantees pattern matching brings Rust / Haskell / OCaml. For example, this will cause a runtime panic in Scala:

https://scastie.scala-lang.org/fnquHxAcThGn7Z8zistthw

This wouldn't compile in Rust. Scala is an okay language, its main benefit as far as I can tell is its a way to write JVM code without having to write Java.

replies(2): >>45055638 #>>45057714 #
96. gf000 ◴[] No.45055638{11}[source]
Scala 3 has a compiler flag that makes `null` its separate type, making it completely null-safe.

This would e.g. make

``` val a: String | Null = someLegacyJavaCodeReturningNullableString()

```

and explicit null check would only make the non-nullable string continue onward.

97. adamnemecek ◴[] No.45057691{3}[source]
Try it.
98. lmm ◴[] No.45057698{8}[source]
> There were lots of languages giving you just that, and they never had this amount of hype. See Scala, OCaml, Haskell, etc.

They weren't hyped because they didn't have a silly party trick like microbenchmark performance. But they give you all the practical benefits of Rust and more.

99. lmm ◴[] No.45057714{11}[source]
That's a theoretical problem, not a practical one. Scala programmers and Scala libraries don't use null and usually have a linter that will reject code like that. (You can hit null in Scala because you didn't check an FFI call properly, but that happens in Rust too)
replies(1): >>45058983 #
100. throwawayffffas ◴[] No.45058180{6}[source]
I have been thinking about how to express it.

My complaint is that because moves are the default, member access and container element access typically involves borrowing, and I don't like dealing with borrowed stuff.

It's a personal preference thing, I would prefer that all types were copy and only types marked as such were not.

I get why the rust devs went the other way and it makes sense given their priorities. But I don't share them.

Ps: most of the time I write python where references are the default but since I don't have to worry about lifetimes, the borrow checker, or leaks. I am much happier with that default.

replies(1): >>45065140 #
101. ModernMech ◴[] No.45058983{12}[source]
The practice of software engineering and language design have improved considerably after the realization of two important facts:

1) If something is technically possible, programmers will not only do it but abuse it.

2) You can't enforce good programming practice at scale using norms.

Linters and as the sibling points out the addition of a recent compiler flag (which is kind of an admission that it's not not an issue), is the opposite approach Rust takes, which is to design the language to not allow these things at all.

> you didn't check an FFI call properly, but that happens in Rust too)

Which is why FFI is unsafe in Rust, so nulls are opt-in rather than opt-out. Having sensible security defaults is also a key learning of good software engineering practice.

replies(1): >>45059046 #
102. lmm ◴[] No.45059046{13}[source]
> 1) If something is technically possible, programmers will not only do it but abuse it.

> 2) You can't enforce good programming practice at scale using norms.

Not quite. Programmers will take the path of least resistance, but they won't go out of their way to find a worse way to do things. `unsafe` and `mem::transmute` are part of Rust, but they don't destroy Rust's safety merits, because programmers are sufficiently nudged away from them. The same is true with unsafePerformIO in Haskell or null in Scala or OO features in OCaml. Yes it exists, but it's not actually a practical issue.

> the addition of a recent compiler flag (which is kind of an admission that it's not not an issue)

Not in the way you think; the compiler flag is an admission that null is currently unused in Scala. The flag makes it possible to use Kotlin-style null in idiomatic Scala by giving it a type. (And frankly I think it's a mistake)

> is the opposite approach Rust takes, which is to design the language to not allow these things at all.

Every language has warts, Rust included. Yes, it would be better to not have null in Scala. But it's absolutely not the kind of practical issue that affected adoption (except perhaps via FUD, particularly from Kotlin advocates). Null-related errors don't happen in real Scala codebases (just as mem::transmute-related errors don't happen in real Rust codebases). Try to find a case of a non-FFI null actually causing an issue.

103. akkad33 ◴[] No.45062978{8}[source]
Is it expensive in Rust? Normally only data in stack gets copied. Heap data is untouched
replies(1): >>45065163 #
104. vlovich123 ◴[] No.45065140{7}[source]
You're not talking about copying values. You want it to be easy to have smart references and copy them around like you do in Python and Java, but it's more complicated in Rust because it doesn't have a GC like Python and Java.

In Rust, "Copy" means that the compiler is safe to bitwise copy the value. That's not safe for something like String / Vec / Rc / Arc etc where copying the bits doesn't copy the underlying value (e.g. if you did that to String you'd get a memory safety violation with two distinct owned Strings pointing to the same underlying buffer).

It could be interesting if there were an "AutoClone" trait that acted similarly to Copy where the compiler knew to inject .clone when it needed to do so to make ownership work. That's probably unlikely because then you could have something implement AutoClone that then contains a huge Vector or huge String and take forever to clone; this would make it difficult to use Rust in a systems programming context (e.g. OS kernel) which is the primary focus for Rust.

BTW, in general Rust doesn't have memory leaks. If you want to not worry about lifetimes or the borrow checker, you would just wrap everything in Arc<Mutex<T>> (when you need the reference accessed by multiple threads) / Rc<RefCell<T>> (single thread). You could have your own type that does so and offers convenient Deref / DerefMut access so you don't have to borrow/lock every time at the expense of being slower than well-written Rust) and still have Python-like thread-safety issues (the object will be internally consistent but if you did something like r.x = 5; r.y = 6 you could observe x=5/y=old value or x=5/y=6). But you will have to clone explicitly the reference every time you need a unique ownership.

replies(1): >>45069251 #
105. Tuna-Fish ◴[] No.45065163{9}[source]
Yes, but if you have a large value type it will be on the stack unless you manually box it. Passing by value can get quite expensive quite fast, especially if the value keeps being passed up and down the call chain.
replies(1): >>45084828 #
106. throwawayffffas ◴[] No.45069251{8}[source]
No, I fully understand the difference. I am just saying since I don't have a GC, I would rather have the system do copies instead of dealing with references.

At least as long as I can afford it performance wise. Then borrowing it is. But I would prefer the default semantics to be copying.

replies(1): >>45070211 #
107. vlovich123 ◴[] No.45070211{9}[source]
> At least as long as I can afford it performance wise. Then borrowing it is. But I would prefer the default semantics to be copying.

How could/would the language know when you can and can't afford it? Default semantics can't be "copying" because in Rust copying means something very explicit that in C++ would map to `is_trivially_copyable`. The default can't be that because Rust isn't trying to position as an alternative for scripting languages (even though in practice it does appear to be happening) - it's remarkable that people accept C++'s "clone everything by default" approach but I suspect that's more around legacy & people learning to kind of make it work. BTW in C++ you have references everywhere, it just doesn't force you to be explicit (i.e. void foo(const Foo&) and void foo(Foo) and void foo(Foo&) all accepts an instance of Foo at the call site even though very different things happen).

But basically you're argument boils down to "I'd like Rust without the parts that make it Rust" and I'm not sure how to square that circle.

108. lukaseder ◴[] No.45076626{3}[source]
jOOQ's internals are full of unsafe casts, though.
replies(1): >>45081980 #
109. gf000 ◴[] No.45081980{4}[source]
Well, you have basically implemented a Java type-system level type checker for SQL. I don't believe there is any type system strong enough to express the whole thing without the escape hatches (casts), besides Lean, Coq and alia.
110. uzerfcwn ◴[] No.45083611{5}[source]
The core difference between sums and unions is that sum types have cardinality |A+B| = |A| + |B|, whereas union types have cardinality |A∪B| = |A| + |B| - |A∩B|. As you noted, type systems with union types, such as Typescript, also support sum types because you can create disjoint wrapper types (like Ok and Err) and their union type will be semantically equivalent to a sum type, since the intersection is an empty set. In this sense, union types are strictly more powerful than sum types.

The downside is that union types require some notion of subtyping, since otherwise A∩B is always empty for distinct A and B. Unfortunately, subtyping is apparently very difficult to implement in HM-like type systems (like Rust and ML) such that it plays well with type inference.[0] Hence, the downside of having union types in a language is that users have to write out types more often.

Unlike kibwen, I don't think Rust's type system is particularly essential for new languages. It's a design choice where one side has more powerful types but the other side has more powerful type inference.

[0] https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_sy...

111. akkad33 ◴[] No.45084828{10}[source]
Is this really true? What do you mean by value types? The types that implement copy or any struct types? Because I think struct types only get moved