One note, the author notes that they’re not sure why File has to be mutable in order to read its contents. The reason is that reading from the file modifies the file descriptor (Unix and Redox) / handle (Windows) contained within the File type, which requires File to be mutable. This could be theoretically fixed with some interior mutability mechanism (Cell), although I’m sure there is some good reason as to why that’s not done here.
For reference, here is the type declaration for File, and for the File internals for Unix, Windows, and Redox, from the latest commit as of the writing of this post.
If it took a const File you would expect File f; read(f); read(f); to return the same thing both times.
File f; read(f); read(f);
What do you mean by “a const File”? All variables are immutable by default in Rust. You can get an immutable File, but then you can’t call any methods on that file which require the File to be mutable. That is, if the function takes self by as &mut self, and the variable isn’t declared mut, you can’t call the function. There is no “returns the same thing both times.” If you can’t mutate it, you can’t call functions that require mutation.
I think you could make the argument that you could read the file without depending on mutated file descriptor state. For instance, you could constrain your I/O to use only positional system calls, like pread(2) and pwrite(2).
On Unix, libstd ships with an extension that does just that:
Note that they take &self instead of &mut self.
Mutability of File in Rust is really mutability of the info wrapped, mostly the file pointer and its positions.
skade has already addressed how this can be done using the Unix-specific extensions. My guess would be that there isn’t a nice cross-platform API for that sort of non-mutating operation, and that’s why it’s not provided on File directly.
As is typical for rust beginners, the author struggles with rust’s error handling. I love rust’s error handling but you need to know a lot about the language to use it right. The author also expects to be guided by the compiler to use it correctly which it does not do.
Somewhere else I saw the following suggestion for teaching error handling in different levels of sophistication:
That’s basically what the current book does, sans error_chain. It just requires a lot of explanation if you are starting from first principles.
Safety requirements aren’t handled that well by dynamic languages. In many such cases, using a language that eliminates errors at compile time, like Haskell, Rust, dependently-typed languages,… makes a lot of sense and it seems this is a usecase people have. I’d also distinguish between ressource usage and performance: If my code does a mundane thing, I want it to spend a mundane amount of my computing resources. This can get harder than necessary with some of the options you listed (albeit Haskell is notorious for being hard when it comes to reasoning about performance).
This doesn’t contradict what I was saying. Rust is far more annoying to use than python for me - But i wouldn’t use python in places where rust has other benefits that better fit the needed trade offs.
I like Rust a lot, and I’m familiar with more experimental approaches like effect system monads, but it really does seem like monad transformers, despite their shortcomings, are a practical and fairly elegant solution to these exact problems.
If your code might fail (like opening a non-existent file), just throw an ExceptT in your transformer stack. If you’re interfacing with code that returns a Result (or Either) type, just write a function that lifts m (Either e a) to T m a where T is your transformer. Boom, you’ve just nuked all your unwraps, try!s, question marks, and other boilerplate.
m (Either e a)
T m a
Am I falling victim to the blub paradox here? Are there some superior ways to address the kind of stuff the OP is talking about?
Can you elaborate with an example?
Personally, I don’t think that you need a lot of unnecessary error handling code when using the ? operator. Not much to eliminate from.