if let Some(child) = fork() {
do_only_child_stuff();
} else {
do_only_parent_stuff();
}
or if let Some(ForkResult::Child) = fork() {
do_only_child_stuff();
} else {
do_only_parent_stuff();
}
Now, if you're about to tell me that the examples above are totally stupid and no
developer would do such a thing, then you know how I feel about the sloppy
C versions. Doing a system call and not checking for error is totally
stupid as well.By the way, you can also write your own wrapper functions in C, that transform the return value into something like
struct fork_status {
enum { ERROR, PARENT, CHILD } state;
int ret;
};
Then Clang and GCC will warn you about missing switch cases.That said, the libc bindings in Rust are pretty low-level and a project that offers higher-level wrappers can be very helpful, so I hope my comment doesn't create the impression that I'm ripping on the project itself.
The point here is that the language's tools and APIs can significantly better drive the developer towards the safe/right solution, that's a large point of type theory and static type systems after all. In this case rust's type system is used to split out the various "result cases" and notify the developer upfront of the various cases to handle. The return type pretty much tells you how the function will behave and what you need to take care of as the caller.
That aside, why would you unwrap_or(default_value) if you're in a hurry when unwrap() is shorter (and you can later grep for "unwrap()" to find dodgy/hurried code, whereas unwrap_or is a perfectly legitimate recovery strategy).
> Now, if you're about to tell me that the examples above are totally stupid and no developer would do such a thing, then you know how I feel about the sloppy C versions. Doing a system call and not checking for error is totally stupid as well.
The issue being that even though you have a compiled statically typed language it's of absolutely no help in "checking for error", and interactions between syscalls can be hard to predict, not checking for fork(2)'s error isn't the end of the world... until you pass its result to kill(2) for instance (it might also give strange results if you pass specific pids to waidpid)
Sure, you can have the same higher level API in C. Though Rust will not compile if there's no exhaustive match, and you cannot access the `int ret` if it's in the wrong state, but it's close enough.
struct fork_status {
enum { ERROR, PARENT, CHILD } state;
int ret;
};
I think the point of tagged unions (as in OP's example) is that you cannot access `child` if the call failed. Your example does have an exhaustive check (by a compiler warning), but it doesn't prevent you from misinterpreting `ret` as a pid. Does C have a method to enforce this restriction?Similarly, neither of your "if let Some() = fork()" examples would fail as badly as the sloppy C program, right?
All the examples you could come up with of sloppy Rust code fail less poorly or make it harder to fail as poorly as the sloppy C code, and you were deliberately trying make your Rust code sloppy, whereas the sloppy C code was based on examples from the wild: http://rachelbythebay.com/w/2014/08/19/fork/
Still seems like a good argument that Rust's design is an improvement.
An example program written like that is not endemic to C or a logical consequence of its design. It's ignorance. Such examples frustrate me too. It's practically a straw-man argument.
> By the way, you can also write your own wrapper functions in C
And your own abstractions to provide more safety guarantees... it really depends on your judgement to balance risk, reward, and goals. It's possible to write a system that does all of the checks for you but you'll sacrifice the time and energy to do so (and will still find users blowing off their feet regardless of your best efforts).
Rust is doing very cool things but it's not going to make systems programming magically easier. Someone still had to write nix and you should still understand your programs.
Half the time the same developer has made a mistake in the same code.
With software being so complex, so full of human error -- almost any tool and practice that can help remedy this situation is welcome in my books.
So I no longer worry about it.
It's actually about a library, nix, whose mission is "to provide ‘Rust friendly bindings to *nix APIs’".
I thought "A match made in heaven"
But it will probably never happen, because Cargo is too good, haha
The nix package manager has (admittedly, undocumented) support for Rust / Cargo projects.
Because nix and Rust are both great.
Adding new abstraction layers rarely helps when doing systems programming. You (as in "the developer") want to be as near to the machine as possible. C does this pretty well.
Perhaps I'm just getting old :-(
The original API is not "broken" per se, it's just limited by the language features ("magical" return values vs. tagged unions or whatever they're called in Rust, I don't remember.)
In this day and age with big software packages, security being an increased concern it really is high time programming languages do more to help us avoid bugs which expose us to hackers and crackers.
I do have an affinity for C, but as a Objective-C programmer currently coding in Swift, I am really seeing how many more bugs the compiler helps me uncover.
I think Rust is on the right track. It is a long overdue change to systems programming.
In the example given it's possible to write a similar library in C to protect against unwanted side effects or bad API design. I'm sure several have been written over the years.
Rust is a great language with lots of improvements over other system programming languages, but that is not going to be enough to get people to switch. You have to show that it's good enough to be worth throwing away 40 odd years of experience and well understood best practice. Something that is going to take a long time and big public projects to do. If just being better was good enough Plan 9 would have been a roaring success and Linux (if it happened) would probably be a footnote in history.
C and UNIX have survived as long as they have not because better alternatives haven't come along, but because the alternatives haven't offered a compelling reason to switch. Unfortunately at least now Rust is falling into the same category.
See also: Niccolo Machiavelli, The Prince
pid_t p;
if (defined(p = fork()) { // parent / child stuff here } else { // fork error here }
It's pretty much the same as try / catch, we just implemented it as part of the variable. And any scalar or complex type can be undefined.
I suspect if C had this a lot of these code samples would be a little more clear. Maybe? Dunno, it's worked well for us. And we like C a lot.
The function of kill is to kill a given pid, so there are two failure modes : "the pid didn't exist" or "the pid didn't die"
thread '<main>' panicked at 'kill failed: Sys(ESRCH)', ../src/libcore/result.rs:746
So you know that it failed because of ESRCH (no such process).1. there are more failure modes than these (POSIX kill(2) can set 3 different errnos)
2. kill(2) signals process, it doesn't usually kill them (let alone kill them outright in such a way that this information could ever be returned)
3. kill(2) can signal process sets of cardinality > 1
4. the `unwrap` panic message will print the unwrapped value, which includes the ERRNO
pid_t childPid;
switch (childPid = fork()) {
case -1: ... /error handling /;
case 0: ... /Child Specific/
default: sleep (5); }
edit - seems to mangle formatting but something like that seems fairly clean.
Nobody is claiming that C makes it impossible to cleanly do the right thing—obviously the whole world runs on C.
The point is that nothing about the C language, libraries, or toolchain discourage the example given in the blogpost compared to your more correct code. Unless you remember exactly the right details from the manpages, there's nothing about the example in the blogpost that's less natural to write than your more correct code. (And people do forget those details: http://rachelbythebay.com/w/2014/08/19/fork/ )
By contrast, as illustrated in the blogpost, the most natural way to do the same thing in Rust turns out to be the more correct thing. If you wanted the bad behavior to happen, you'd have to go out of your way to pass -1 to kill(). Hence in this example, Rust's design is an improvement.
It's great that C gives you enough rope to hang yourself with, but it's even better if tying yourself to things safely is easy, and to hang yourself you have to really go out of your way.
In context, "typically" means your devops people get to work on Christmas to patch a critical CVE being actively exploited in the wild pissing off all your customers that one time someone didn't do the typical pattern anywhere in your codebase or the codebases of any of your 3rd party libaries, frameworks, applications...
Where "didn't do the typical pattern" might be if conditions as shown, or forgetting the "case -1", or missed a key and typed "case 1", or elided the "break;" from the case above (I note no "break;"s in your switch ;)), or didn't rtfm closely enough to see -1 was a special exit code, or mis-assumed kill(-1,...) was a noop, or ...
Great intro to Rust and Unix post!
Oh by the way, I accidentally hit downvote on your post and HN doesn't let me undo that action... I was just trying to hide it! Sorry!
Someone never had to bit-pack their programs to save memory, disk space, or bandwidth. In fact, it's a huge waste of memory; if you only need 3 bits, a 'char' would have sufficed. Saves 24 bits!
Of course, we could use nibbles to make data structures where the fork return value only takes up 3 bits instead of a whole byte, but that could be considered micro-optimizing. (the compiler may do this for us anyway, though)
But regardless, the point of that sentence is nothing to do with memory usage, but with semantics. Whether you or the compiler packs all the information into 3 bits or 3 words, that's fine, as long as the language helps you distinguish the parts.