fn list_items<'life0, 'life1, 'async_trait>(
&'life0 self,
collection_href: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<Vec<ItemRef>, Error>> + Send + 'async_trait>>
where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Rendered docs: https://mirror.whynothugo.nl/vdirsyncer/v2.0.0-beta0/vstorag...Source: https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/v2.0.0-beta...
Not only is is incredibly ugly it's also rather confusing, but it fits well into how I view Rust, complicated for the sake of making the developers look smart.
It wouldn't fit the syntax of the language obviously, but why not simply have the developer prefix the variable with the keyword "lifetime", rather than assigning a symbol. It seems a little like starting functions in Go with a upper case letter to export them, dude just give us an export keyword, it's fine.
Unrelated to parent but maybe relevant to you: Rust API naming guidelines say no `get_` prefix on getters.
https://rust-lang.github.io/api-guidelines/naming.html#gette...
async fn list_items(&self, colletion_href: &str) -> Result<Vec<ItemRef>, Error>1) Every time I go through a particular door I try to push it when it's a pull door.
2) I notice this and ensconce in my brain that it's the opposite of what I think it is.
3) After a while, my brain actually starts to remember it the correct way around to start with.
4) But my brain doesn't then drop the 'opposite' rule so now I remember it the correct way around, then invert it, thus recreating step 1.
I don't claim this says anything about rust, but I think it does say something about human brains (or at least mine and apparently yours).
My sympathies.
Rust can improve this by introducing syntax like `'a contains 'b`
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..?
Cons are:
- C# has OOP (you don't have to use it heavily)
- No Hindler-Milner type inference in C#, nested generic arguments may need to be specified by hand
- Smaller amount of supported targets by CoreCLR: x86, x86_64, arm, arm64 for ISAs and Linux, Windows, macOS and FreeBSD for OSes. NativeAOT-based support is experimentally available on iOS, and is undergoing further work. As you can imagine, LLVM targets absolutely everything under the sun and above it too. no-std story in Rust is first-class. C# has bflat and zerosharp but they are niche.
- In C#, type unions will only be available in one of the coming versions. F# to the rescue
- Error handling is a combination of exceptions and e.g. int.TryParse patterns, there is no implicit returns with ? like in Rust
- Lack of associated types and the types own their interface implementations unlike traits which you can introduce without control over the source. This results in having to implement wrapper types (even if they are structs) if you want to modify their interface implementations
- You only control shallow immutability - readonly struct will not prohibit modification of the contents of a Dictionary<K, V> that it holds
- async/await is more expensive
- Big popular libraries often have features or implementation incompatible or not guaranteed to work with native compilation via NativeAOT
- Object reference nullability (e.g. 'T?') happens at the level of static analysis i.e. does not participate in type system the same way Option<T> does in Rust
Pros are:
- Has FP features like in Rust: high order functions, pattern matching (match val -> val switch { , also 'is'), records, tuples, deconstruction
- Fast to compile and run, AOT not so fast to compile but tolerable
- If you know how to use Cargo, you know how to use .NET CLI: cargo init -> dotnet new {console, classlib, etc.}, cargo run -> dotnet run, cargo build -> dotnet build/publish, cargo add {package} -> dotnet add package {package}
- Monomorphized structs generics with the same zero-cost abstraction assurance like in Rust, &mut T -> ref T, &T -> ref readonly T, sometimes in T but with caveats
- Box/Arc<T> -> class or record, Arc<Mutex<T>> -> class + lock (instance) { ... }
- Easy to use async/await but without ever having to deal with borrow checker and misuse-resistant auto-scaling threadpool. Task<T>s are hot started. Simply call two task-returning network calls and await each one when you need to. They will run in background in parallel while you do so. While they are more expensive than in Rust, you can still do massive concurrency and spawn 1M of them if you want to
- Built-in Rayon - Parallel.For and PLINQ, there is Channel<T> too, you can e.g. 'await foreach (var msg in chReader) { ... }'
- Iterator expressions -> LINQ, seq.filter(...).map(...).collect() -> seq.Where(...).Select(...).ToArray(), unfortunately come with fixed cost but improve in each version
- Rust slice that wraps arbitrary memory -> Span<T>, e.g. can write the same fast idiomatic text parsing on top of them quite easily
- Stupid fast span routines like .IndexOf, .Count, .Fill, .CopyTo which use up to AVX512
- Compiler can devirtualize what in Rust is Box<dyn Trait>
- Can make small native or relatively small JIT single-file executables that don't require users to install runtime
- Rich and fast FFI in both directions, can statically link into Rust, can statically link Rust components into itself (relatively new and advanced)
- Great tooling, Rider is very good, VSCode + base C# extension about as good as rust-analyzer
- Controversial but powerful runtime reflection and type introspection capability, can be used in a very dynamic way with JIT and compile additional code on the fly
- A bit easier to contribute to, depending on area owner and project (runtime, roslyn, aspnetcore, ...)
- CoreLib has full-blown portable SIMD API that is years ahead of portable-simd initiative in Rust
Because I occasionally use Rust and prefer its formatting choices, I carry this .editorconfig around: https://gist.github.com/neon-sunset/c78174b0ba933d61fb66b54d... to make formatting terser and more similar. Try it out if K&R and `I` prefix on interfaces annoy you.
In other words, be careful what you wish for.
Most people would probably be better served by a language that was a tiny bit slower but had better developer productivity. However, once you deviate from the goal of “as fast as possible”, then you have to choose which parts you want to sacrifice speed for productivity. Like Excel, everybody agrees that Rust is too complicated but nobody can agree on which 10% to remove.
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...
(defun read (path)
(declare (generic P (AsRef Path))
(type P path)
(returns (io:Result (Vector U8))))
(flet ((inner (path)
(declare (type (Ref Path) p)
(returns (io:Result (Vector U8))))
(try-let ((file (File:open path))
(bytes (vector)))
(declare (mutable file bytes))
(try (read-to-end file bytes)
(Ok bytes)))))
(inner (as-ref path)))) @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")
}
}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.
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.
pub fn read(path: Path) -> Bytes {
let file = File::open(path);
let bytes = Bytes::new();
file.read_to_end(bytes);
bytes
}
Here is is in raku (https://raku.org): sub read(Str:D $path --> Buf:D) {
$path.IO.slurp: :bin
}
[the `--> Buf:D` is the raku alternative to monads] 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). say $path.IO.slurp: :binOnly if you suffer from a very high level of stockholm syndrome, that is. Rust's syntax is vastly clearer than C++ in basically all circumstances.
* async closures
* async trait fns,
* `impl Trait` everywhere
are in place.
type Result<T> = std::result::Result<T, io::Error>;
So it's actually fine, since we're specifying it's an IO result. This is a fairly common pattern.When type signatures are so complex it makes vastly more sense to separate them out,
Consider,
read :: AsRef(Path) -> IO.Result(Vec(U8))
pub fn read(path):
inner :: &Path -> IO.Result(Vec(U8))
fn inner(path):
bytes := Vec.new()
return? file := File.open(path)
return? file.read_to_end(&! bytes)
return OK(bytes)
inner(path.as_ref()) pub fn read<P>(path: P) -> io::Result<Vec<u8>>
where
P: AsRef<Path>,
{
// ...
}
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:
unsigned char *read(path)
const char *path;
{
/* ... */
} pub fn read<P>(path: P) -> io::Result<Vec<u8>>
where
P: AsRef<Path>,
{
fn inner(path: &Path) -> io::Result<Vec<u8>> {
let mut bytes = Vec::new();
let mut file = File::open(path)?;
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
inner(path.as_ref())
}
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.
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;I'm not arguing the example you found sand-in-the-eyes is necessarily good but my mental skim reading algorithm copes with it much better.
pub fn read(path: Path) -> Bytes {
File::open(path).read_to_end()
} "I think that most of the time when people think they have an issue with Rust’s syntax, they actually object to Rust’s semantics."
You think wrong. Rust syntax is horrible because it is verbose and full of sigilsI 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...
E.g. making pub default is precisely the decision a language would make that values concise code over what the code actually does.
The same reasoning works for “mut” as well.
That said, I don’t like Rust’s syntax. Especially once you get to lambdas, things get hard to read.
> 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.
My guilty pleasure is Go's visibility system, where all functions that start with lowercase are private to the scope of the current class/file and all Functions that start with Uppercase are public.
It doesn't look but it would work and it's a mess when you need acronyms, but it somehow works great and the result looks nice.