It does in rust. An `unsafe fn()` is a different type than a (implicitly safe by the lack of keyword) `fn()`.
The difference is that unsafe fn's can be encapsulated in safe wrappers, where as IO functions sort of fundamentally can't be encapsulated in non-IO wrappers. This makes the IO tagged type signatures viral throughout your program (and as a result annoying), while the safety tagged type signatures are things you only have to think about if you're touching the non-encapsulated unsafe code yourself.
>The difference is that unsafe fn's can be encapsulated in safe wrappers
This is the koolaid I am not willing to drink.
If you can add safety very carefully on top of unsafe stuff (without any help from compiler), why not just use `c` and add safety by just being very careful?
> IO tagged type signatures viral throughout your program (and as a result annoying)..
Well, that is what good type systems do. Carry information about the types "virally".
Anything short is a flawed system.
> If you can add safety very carefully on top of unsafe stuff (without any help from compiler), why not just use `c` and add safety by just being very careful?
Y'know people complain a lot about Rust zealots and how they come into discussions and irrationally talk about how Rust's safety is our lord and savior and can eliminate all bugs or whatever...
But your take (and every one like it) is one of the weakest I've heard as a retort.
At the end of the day "adding safety very carefully atop of unsafe stuff" is the entire point of abstractions in software. We're just flipping bits at the end of the day. Abstractions must do unsafe things in order to expose safe wrappers. In fact that's literally the whole point of abstractions in the first place: They allow you to solve one problem at a time, so you can ignore details when solving higher level problems.
"Hiding a raw pointer behind safe array-like semantics" is the whole point of a vector, for instance. You literally can't implement one without being able to do unsafe pointer dereferencing somewhere. What would satisfy your requirement for not doing unsafe stuff in the implementation? Even if you built a vector into the compiler, it's still ultimately emitting "unsafe" code in order to implement the safe boundary.
If you want user-defined types that expose things with safe interfaces, they have to be implemented somehow.
As for why this is qualitatively different from "why not just use c", it's because unsafety is something you have to opt into in rust, and isn't something you can just do by accident. I've been developing in rust every day at $dayjob for ~2 years now and I've never needed to type the unsafe keyword outside of a toy project I made that FFI'd to GTK APIs. I've never "accidentally" done something unsafe (using Rust's definition of it.)
It's an enormous difference to something like C, where simply copying a string is so rife with danger you have a dozen different strcpy-like functions each of which have their own footguns and have caused countless overflow bugs: https://man.archlinux.org/man/string_copying.7.en
1. In `c` one have to remember a few, fairly intutive things, and enforce them without fail.
2. In rust, one have to learn, remember ever increasing number of things and constantly deal with non-intutive borrow-checker shenanigans that can hit your project at any point of the development forcing you to re-architecture your project, despite doing everything to ensure "safety". But the borrow-checker can't be convinced.
I have had enough of 2. I might use rust if I want to build a critical system with careless programmers, but who would do such a thing? For open source dependencies, one will have to go by community vouching or auditing themselves. Can't count something to be "Safe" just because it is in rust, right? So what is the point. I just don't see it. I mean, if you look a bit deeper, It just does not make any sense.
What is the point. If I share something, someone is going to come along and say. That is not how you are "supposed" to do it in rust.
And that is exactly my point. You need to learn a zillion rust specific patterns for doing every little thing to work around the borrow-checker and would be kind of unable to come up with your own designs with trade-offs that you choose.
And that becomes very mechanical and hence boring. I get that it would be safe.
So yes, if I am doing brain surgery, I would use tools that prevent me from making quick arbitrary movements. But for everything else a glove would do.
To learn something is generally the point. Either me, or you. I’ve been developing in rust for half a decade now and genuinely do not know what you were talking about here. I haven’t experienced it.
So either there are pain points that I’m not familiar with (which I’m totally open to), or you might be mistaken about how rust works. Either way, one or both of us might learn something today.
All lessons are not equally valuable. Seemingly arbitrary reasoning for some borrow checker behavior is not interesting enough for me to learn.
In the past, I would come across something and would lookup and the reasoning for it often would be "What if another thread do blah blah balh", but my program is single threaded.
Borrow checker issues do not require multiple threads or async execution to be realized. For example, a common error in C++ is to take a reference/interator into vector, then append/push onto the end of that vector, then access the original error. If that causes reallocation, the reference is no longer valid and this is UB. Rust catches this because append requires a mutable reference, and the borrow checker ensures there are no other outstanding references (read only or mutable) before taking the &mut self reference for appending.
This is generally my experience with Rust: write something the way I would in C++, get frustrated at borrow checker errors, then look into it and learn my C++ code has hidden bugs all these years, and appreciate the rust compiler’s complaints.
>If that causes reallocation, the reference is no longer valid
Doesn't the append/push function return a pointer in that case? At least in `c` there are special functions that reallocate and is not done by implicitly (but I understand someone could write a function that does it).
Thus it appears that borrow checker's behavior is guided by bad designs in other languages. When bad design is patched with more design, the latter often becomes non-intuitive and restricting. That seems to have happened with the rust's borrow checker.
In C++? No. The vector container is auto resizing. When it hits capacity limits it doubles the size of the allocation and copies the contents to the new memory. An insertion operation will give you an iterator reference to the newly inserted value, but all existing references may or may not remain valid after the call.
This meant “guided by bad design.” The borrow checker wasn’t written to handle this one use case. It was designed to make all such errors categorically impossible.
> If you can add safety very carefully on top of unsafe stuff (without any help from compiler), why not just use `c` and add safety by just being very careful?
There is help from the compiler - the compiler lets the safe code expose an interface that creates strict requirements about how it is being called with and interacted with. The C language isn't expressive enough to define the same safe interface and have the compiler check it.
You can absolutely write the unsafe part in C. Rust is as good at encapsulating C into a safe rust interface as it is at encapsulating unsafe-rust into a safe rust interface. Just about every non-embedded rust program depends on C code encapsulated in this manner.
> Well, that is what good type systems do. Carry information about the types "virally". Anything short is a flawed system.
Good type systems describe the interface, not every implementation detail. Virality is the consequence of implementation details showing up in the interface.
Good type systems minimize the amount of work needed to use them.
IO is arguably part of the interface, but without further description of what IO it's a pretty useless detail of the interface. Meanwhile exposing a viral detail like this as part of the type system results in lots of work. It's a tradeoff that I think is generally not worth it.
>the compiler lets the safe code expose an interface that creates strict requirements about how it is being called with and interacted with..
The compiler does not and cannot check if these strict requirements are enough for the intended "safety". Right? It is the judgement of the programmer.
And what is stopping a `c` function with such requirements to be wrapped in some code that actually checks these requirements are met? The only thing that the rust compiler enables is to include a feature to mark a specific function as unsafe.
In both cases there is zero help from the compiler to actually verify that the checks that are done on top are sufficient.
And if you want to mark a `c` function as unsafe, just follow some naming convention...
>but without further description of what IO it's a pretty useless detail of the interface..
Take a look at effect-system libraries which can actually encode "What IO" at the type level and make it available everywhere. It is a pretty basic and widely used thing.
> The compiler does not and cannot check if these strict requirements are enough for the intended "safety". Right? It is the judgement of the programmer.
Yes*. It's up to the programmer to check that the safe abstraction they create around unsafe code guarantees all the requirements the unsafe code needs are upheld. The point is that that's done once, and then all the safe code using that safe abstraction can't possibly fail to meet those requirements - or in other words any safety related bug is always in the relatively small amount of code that uses unsafe and builds those safe abstraction.
> And what is stopping a `c` function with such requirements to be wrapped in some code that [doesn't] actually checks these requirements are met?
Assuming my edit to your comment is correct - nothing. It's merely the case that any such bug would be in the small amount of clearly labelled (with the unsafe keyword) binding code instead of "anywhere".
> The only thing that the rust compiler enables is to include a feature to mark a specific function as unsafe.
No, the rust compiler has a lot more features than just a way to mark specific functions as unsafe. The borrow checker, and it's associated lifetime constraints, enforcing that variables that are moved out of (and aren't `Copy`) aren't used, is one obvious example.
Another example is marking how data can be used across threads with traits like `Send` and `Sync`. Another - when compared to C anyways - is simply having a visibility system so that you can create structs with fields that aren't directly accessible via other code (so you can control every single function that directly accesses them and maintain invariants in those functions).
> In both cases there is zero help from the compiler to actually verify that the checks that are done on top are sufficient.
Yes and no, "unsafe" in rust is synonymous with "the compiler isn't able to verify this for you". Typically rust docs do a pretty good job of enumerating exactly what the programmer must verify. There are tools that try to help the programmer do this, from simple things like being able to enable a lint that checks every time you wrote unsafe you left a comment saying why it's ok, and that you actually wrote something the compiler couldn't verify in the first place. To complex things like having a (very slow) interpreter that carefully checks that in at least one specific execution every required invariant is maintained (with the exception of some FFI stuff that it fails on as it is unable to see across language boundaries sufficiently well).
The rust ecosystem is very interested in tools that make it easier to write correct unsafe code. It's just rather fundamentally a hard problem.
* Technically there are very experimental proof systems that can check some cases these days. But I wouldn't say they are ready for prime time use yet.
It does in rust. An `unsafe fn()` is a different type than a (implicitly safe by the lack of keyword) `fn()`.
The difference is that unsafe fn's can be encapsulated in safe wrappers, where as IO functions sort of fundamentally can't be encapsulated in non-IO wrappers. This makes the IO tagged type signatures viral throughout your program (and as a result annoying), while the safety tagged type signatures are things you only have to think about if you're touching the non-encapsulated unsafe code yourself.