I think the article makes a good point, but the actual example isn’t Rust’s worst, not even close. It gets really hard to follow code when multiple generic types are combined with lifetime markers. Then it truly becomes a mess.
I always, always forget what `'a: 'b` means, because I remember it always being the opposite of what I think it is, but memorizing that obviously doesn't work because then it will just flip again the next time. It's so annoying.
IMHO the mentioned examples of complexity like multiple type variables and lifetimes with bounds are for who "really" wants compile-time contracts. These are mostly opt-in so higher level use cases(like writing backend business logics) should not care about that, just wrapping everything with Boxes and Arcs.
Of course Rust is not perfect; there is some 'leakages' of low level aspects to high level like async caveats(recursion, pinning, etc.). I'm not sure how these can be avoided. Maybe just trial-and-errors for all..?
That's why I am a huge fan of Rust but at the same time at the end of the day all I want is the features of the language that Rust has minus the memory management and a GC. That would be my dream language. If only ReasonML/Rescript were more popular... Or I guess Elixir
For my own situation, the articles present the right way to express all possible performance/error handling (which is expected in a standard lib) and then goes on to show how I actually code it in my own softawre where I don't really need the level of detail/finetuning of the standard lib.
Interestingly, my life starts at the end of the article, with the simple verison of the code, and as my understanding of rust widens, I go up to the beginning of the article and better define my function...
I've only learned a tiny bit of Rust, and I feel the same. Going from the bottom up, makes it all make so much sense. (Albeit I still like the Rattlesnake syntax haha)
If I understood all the semantic properties, including the separate compilation requirements, correctly, here’s how I think it would be done in Swift with the proposed nonescapable types features (needed to safely express the AsRef concept here).
(Note that this doesn’t quite compile today and the syntax for nonescaping types is still a proposal.)
@usableFromInline
func _read(pathView: PathView) throws(IOError) -> [UInt8] {
var file = try File(pathView)
var bytes: [UInt8] = []
try file.readToEnd(into: &bytes)
return bytes
}
@inlinable
public func read<Path>(path: borrowing Path) throws(IOError) -> [UInt8] where Path: PathViewable, Path: ~Copyable {
try _read(pathView: path.view())
}
// Definitions...
public enum IOError: Error {}
public protocol PathViewable: ~Copyable {
func view() -> PathView
}
public struct PathView: ~Escapable {}
public struct File: ~Copyable {
public init(_ pathView: borrowing PathView) throws(IOError) {
fatalError("unimplemented")
}
public mutating func readToEnd(into buffer: inout [UInt8]) throws(IOError) {
fatalError("unimplemented")
}
}
> The next noisy element is the <P: AsRef<Path>> constraint. It is needed because Rust loves exposing physical layout of bytes in memory as an interface, specifically for cases where that brings performance. In particular, the meaning of Path is not that it is some abstract representation of a file path, but that it is just literally a bunch of contiguous bytes in memory.
I can't understand this. Isn't this for polymorphism like what we do this:
Rust needs to know the exact size, layout, and alignment of every argument passed to a function to determine how it gets passed(register(s) or spilled to stack) and used. For example PathBuf and String can both be turned into a reference to a Path, and while they have the same size their layout and implementation of `as_ref` differ.
As for `impl`,
fn foo(a: impl ToString)
is syntactic sugar for
fn foo<S: ToString>(a: S)
The reason the standard library doesn't use this is because the code predates the introduction of `impl` in argument position.
The reason the function takes `AsRef<Path>` instead of `&Path` is callsite ergonomics. If it took `&Path` all callsites need to be turned into `read(path.as_ref())` or equivalent. With `AsRef<Path>` it transparently works with any type that can be turned into a `&Path` including `&Path` itself.
Wanted to say the same. He straight up conjured a good looking code in CrabML as an example of similar level of "ugliness", while it has about three times less syntax noise.
public byte[] Read(string path) => File.ReadAllBytes(path);
I think the article’s trying to explain a concept using an arbitrary piece of code from stdlib, not necessarily that specific scenario (opening and reading all bytes from a file).
C++ is relatively easy to read.
The only troubles I had is reading STL's sources because of all _ and __ prefixes, and understanding template errors from compiler, but that will soon be fixed with concepts.
Kinda disingenuous, you don't reskin one language in another to make an argument about syntax -- you develop a clear syntax for a given semantics. That's what rust did not do -- it copied c++/java-ish, and that style did not support the weight.
When type signatures are so complex it makes vastly more sense to separate them out,
Rust does allow you to split out generic specifications into a separate "declaration block" so things don't get too busy. Like you could write the original Rust code as:
Personally I don't find your example with the type signature completely separate to be easier to read. Having to look in more than one place doesn't really make sense to me.
Funny, though, Rust's 'where' syntax sorta vaguely superficially reminds me of K&R pre-ANSI C:
Plus, your example does not have the same semantics as the Rust code. You omitted generics entirely, so it would be ambiguous if you want monomorphization or dynamic dispatch. Your `bytes` and `file` variables aren't declared mutable. The `try` operator is suddenly a statement, which precludes things like `foo()?.bar()?.baz()?` (somewhat normal with `Option`/`Error`). And you weirdly turned a perfectly clear `&mut` into a cryptic `&!`.
Please don't assume that the syntax of Rust has been given no thought.
I strongly support your point, but the example is still sand-in-the-eyes for me. I hold that one symbol should not alter the semantics of a program and there should never ever be sequences of one-symbol syntactic elements.
In an Ada-like language, it would be something like
generic
type Path_Type implements As_Path_Ref;
type Reader implements IO.File_Reader;
function Read(Path: Path_Type) return Reader.Result_Vector_Type|Reader.Error_Type is
function Inner_Read(P: Path) return Read'Result_Type is
begin
File: mutable auto := try IO.Open_File(P);
Bytes: mutable auto := Reader.Result_Vector_Type.Create();
try Reader.Read_To_End(File, in out Bytes);
return Bytes;
end;
begin
return Inner_Read(Path.As_Ref());
end Read;
People may disagree on specifics, but you're definitely right that being able to separate the function signature from its definition would be very helpful in complex cases.
The final version is still ugly. Why `pub fn'? Why is public not the default and why do you have to specify that it's a function? Why `: type' and `-> type', why can't type go before the identifier? Why do you need `File::' and `Bytes::'? What is that question mark? Why does the last statement not need a semicolon? It's like the opposite of everything people are used to.
I prefer it this way; defaults should be conservative or more common: I write many many more private functions than public. I'm not sure what your objection to 'fn' is... seems like a superficial problem. It likely makes the language easier for the compiler to parse, and to me, it makes it easier to read.
> Why `: type' and `-> type', why can't type go before the identifier?
Because putting the type afterward is more ergonomic. If you're used to C/C++/Java/etc. it feels weird, but once you start writing code with the type after the declaration, it feels much more natural.
> Why do you need `File::' and `Bytes::'?
I'm not sure what you mean here. They're types. You have to specify types.
> What is that question mark?
The question mark is basically "if the Result is Ok, unwrap it; if it's an Err, return it immediately."
> Why does the last statement not need a semicolon?
Leaving off the semicolon returns the last expression from the block.
> It's like the opposite of everything people are used to.
Maybe if your experience with programming languages is fairly limited...
Short answer for the type ordering and `fn`: because C/C++/Java tried that type of syntax and the result was an ambiguous grammar that is way too hard to parse, not to mention C's overly complicated pointer syntax.
As someone who doesn't think it is pretty, but knows Rust I went through all your points and let me assure you except for the one where you wonder why the syntax can't be more like C/C++ where it comes down to taste, all of your questions have an answer that really makes sense if you understand the language.
E.g. making pub default is precisely the decision a language would make that values concise code over what the code actually does.
sedatk|1 year ago
WhyNotHugo|1 year ago
Source: https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/v2.0.0-beta...
LoganDark|1 year ago
namjh|1 year ago
Of course Rust is not perfect; there is some 'leakages' of low level aspects to high level like async caveats(recursion, pinning, etc.). I'm not sure how these can be avoided. Maybe just trial-and-errors for all..?
yulaow|1 year ago
nazka|1 year ago
hckr1292|1 year ago
wiz21c|1 year ago
Interestingly, my life starts at the end of the article, with the simple verison of the code, and as my understanding of rust widens, I go up to the beginning of the article and better define my function...
awesomebytes|1 year ago
tmtvl|1 year ago
MetricExpansion|1 year ago
jiwangcdi|1 year ago
I can't understand this. Isn't this for polymorphism like what we do this:
```rust fn some_function(a: impl ToString) -> String { a.to_string(); } ```
What to do with memory layout? Thanks for any explanation.
K0nserv|1 year ago
As for `impl`,
is syntactic sugar for The reason the standard library doesn't use this is because the code predates the introduction of `impl` in argument position.The reason the function takes `AsRef<Path>` instead of `&Path` is callsite ergonomics. If it took `&Path` all callsites need to be turned into `read(path.as_ref())` or equivalent. With `AsRef<Path>` it transparently works with any type that can be turned into a `&Path` including `&Path` itself.
eterps|1 year ago
frankie_t|1 year ago
stavros|1 year ago
baq|1 year ago
awesomebytes|1 year ago
anonymous2024|1 year ago
librasteve|1 year ago
sedatk|1 year ago
qalmakka|1 year ago
olologin|1 year ago
tevelee|1 year ago
apatheticonion|1 year ago
carlmr|1 year ago
mgaunard|1 year ago
AxelLuktarGott|1 year ago
treyd|1 year ago
unknown|1 year ago
[deleted]
Woshiwuja|1 year ago
pieresqi|1 year ago
[deleted]
macmac|1 year ago
kzrdude|1 year ago
mjburgess|1 year ago
When type signatures are so complex it makes vastly more sense to separate them out,
Consider,
kelnos|1 year ago
Funny, though, Rust's 'where' syntax sorta vaguely superficially reminds me of K&R pre-ANSI C:
_answ|1 year ago
Please don't assume that the syntax of Rust has been given no thought.
tcfhgj|1 year ago
blksv|1 year ago
In an Ada-like language, it would be something like
degurechaff|1 year ago
demurgos|1 year ago
timeon|1 year ago
remcob|1 year ago
oneshtein|1 year ago
singularity2001|1 year ago
Woshiwuja|1 year ago
oguz-ismail|1 year ago
kelnos|1 year ago
I prefer it this way; defaults should be conservative or more common: I write many many more private functions than public. I'm not sure what your objection to 'fn' is... seems like a superficial problem. It likely makes the language easier for the compiler to parse, and to me, it makes it easier to read.
> Why `: type' and `-> type', why can't type go before the identifier?
Because putting the type afterward is more ergonomic. If you're used to C/C++/Java/etc. it feels weird, but once you start writing code with the type after the declaration, it feels much more natural.
> Why do you need `File::' and `Bytes::'?
I'm not sure what you mean here. They're types. You have to specify types.
> What is that question mark?
The question mark is basically "if the Result is Ok, unwrap it; if it's an Err, return it immediately."
> Why does the last statement not need a semicolon?
Leaving off the semicolon returns the last expression from the block.
> It's like the opposite of everything people are used to.
Maybe if your experience with programming languages is fairly limited...
pta2002|1 year ago
atoav|1 year ago
E.g. making pub default is precisely the decision a language would make that values concise code over what the code actually does.
uasi|1 year ago
> Why `pub fn'? Why is public not the default and why do you have to specify that it's a function?
If public were the default, you'd end up having to make other functions `priv fn` instead.
> Why `: type' and `-> type', why can't type go before the identifier?
It's easier to parse, and most major typed languages other than C/C++/C#/Java put the type after the identifier.
> Why do you need `File::' and `Bytes::'?
Seriously?
> What is that question mark?
The final version doesn't use a question mark.
> Why does the last statement not need a semicolon?
This is a legitimate question. In Rust, the last statement without a semicolon becomes the return value.