Common newbie mistakes or bad practices

I am glad I am not the only person to struggle with From. The problem I run into is slightly different: auto completion:

Foo::from_blah(<TAB>
==> IntelliJ starts showing me argument names + types

Foo::from<TAB>
==> InteliJ shows me from_blah from_cat from_dog ...

Foo::from(<TAB>
==> IntelliJ can't complete for me, and I don't, from memory remember all the things we can From from

I reckon it’s reasonable when combined with #[cfg_attr].

Another place where it would be nice to use #[path] is with build script code generation:

#[path = concat!(env!("OUT_DIR"), "/generated.rs")]
mod generated;

Unfortunately, this still doesn’t work (it was initially said to be implemented in Rust 1.54, but it turned out not to be the case), so you have to write that like this:

mod generated {
    include!(concat!(env!("OUT_DIR"), "/generated.rs"));
}

I'm not sure if this belongs here or in some other place, but if novice in question is not a total novice then probably the most common mistake they do is an attempt to write JavaScript in Rust or Python in Rust or some other language in Rust.
Rust is usually presented as “multi-paradigm general-purpose programming language” which somehow convinces people who know some other mainstream language that they should be able to just write code in Java or C# and then, somehow, mechanically translate that into Rust.
Somehow C++ people rarely do that mistake, even if many C++ styles don't easily translate to Rust, too.
Rust maybe “multi-paradigm general-purpose programming language” but it's also incredibly opinionated and tries very hard to stir you toward “great APIs” (good APIs are easy to use, great APIs are hard to abuse).
Very often that make direct translation of other language to Rust either hard to do or impossible to do.
Besides: if you want to write JavaScript then why do you want to do that in Rust? JavaScript is perfectly viable language on it's own.

7 Likes

That deserves an answer:

Quite a lot of my Rust code looks like Javascript. Thing is I want to avoid as many as possible of Rust syntax features that are likely weird and alien to those readers of my code that don't know Rust, Javascripters and the like. Even if they don't have any Rust chops to be able to hack on my code I don't want to totally confuse them and frighten them away from Rust at first sight. So, for example if I have to use lifetime tick marks in my code I have failed.

Speed. Often I feel the need for speed. I can't make JS outrun compiled code.

See little example here: Writing Javascript in Rust ... almost

Scale. Javascript is really not suitable when programs get large and there are multiple people working on it. Rusts type checking helps enormously in keeping everything in order. It makes one far more confident when modifying/refactoring code knowing that Rust will prevent the myriad of ways that one can silently break things in other languages.

Politic: Before we had written a line of code our former employer threatened to sue us thinking we had stolen the code we wrote for them before they went bust. We had not. I thought it would be prudent to implement the similar functionality we needed in a different language, just in case. So what was JS and node.js became Rust.

On the other hand I write a lot of my Rust code as if it were C. As much as the compiler will allow. Because C like languages are what I understand.

Speaking of "mechanical translation". A vendor supplied us with a couple of thousand lines of C# to show how to communicate with their device. In lieu of documentation. I pretty much did a line by line translation of that in to Rust. Worked a treat.

4 Likes

I'd disagree with that. Rust gives us many tools with which one can solve problems using different approaches. You can write simple, linearly flowing procedural code. You can write "classic" OO-looking code with dynamic dispatch (although you'll be frowned upon by the community). You can encode sophisticated type-level constraint systems using traits and generics. You can write functional-looking recursive combinators, you can use iterators or plain old array indexing, and you can wrap your entire main function into an unsafe block and call out to C functions.

The only thing that Rust is really opinionated about is memory management. That's for the better, though – I don't really see any valid arguments against wanting to manage memory correctly, so I don't think that's a question of style or opinion.

7 Likes

See little example here: Writing Javascript in Rust ... almost

Saw. The old adage the determined Real Programmer can write FORTRAN programs in any language certainly is still valid. And I guess I agree: if, for some reason, you can't write Fortran in an actual Fortran then Rust is probably more suitable for that than JavaScript.

But let me remind a cite from the first Rust presenation: the syntax is, really, about the last concern.

When I was talking about JavaScript-in-Rust or Python-In-Rust I wasn't talking about attempts to mimic syntax of JavaScript or Python (some crates actually achieve that with help of macros). I was talking about capabilities of these languages.

How would you imitate change of the object class (JavaScript's prototype) after-the-fact in Rust? How would you do XML-driven dependency injection? How would you do something like C++ template meta programming?

All that is doable with macros and other such tricks, but code quickly become incomprehensible for both Rust pros and some-other-language pros. You, basically, need to know two languages and your glue code in addition.

3 Likes

Good question.

I would not.

On the other hand I have almost never changed object class or done XML-driven dependency injection in Javascript or template meta programming in C++. Despite having used those languages quite a lot.

All in all I'm not seeing Rust limit what I want to do.

1 Like

I would actually appreciate if the compiler only spat out a single error message at a time by default.

This. Especially since multiple errors can arise from a single source, and fixing one error can lead to another.

2 Likes

what's wrong with clap? It supports annotating input/config structs so you get a strongly typed parser. I'm a noob and don't understand. Is the community moving toward structopt?

1 Like

Clap's custom derive isn't out as a major release yet (the only released versions with it are clap 3.0.0-beta.2 to 3.0.0-beta.4), so I doubt many people know about it or have used it too much.

The structopt crate originally came up with the idea of using a custom derive to provide the ergonomics of writing declarative input/config with clap's excellent argument parser. I'm guessing over time they'll be merging a lot of structopt's functionality and lessons into clap itself.

4 Likes
  1. I wouldn't, as it's an artifact specific to Javascript, and not some goal to be achieved.
  2. I wouldn't, because XML for configuration is an antipattern (it isn't particularly human-friendly, and eg TOML does that much better). But even if we replace XML by TOML, I still wouldn't do it. Dependency injection is not something I actually want in my code, since it involves runtime reflection.
    In addition, I'd much rather be explicit about dependencies rather than having them injected¹.
  3. Rust has generics, so I'd just use that.

¹ I once had to read through a Java project that used dependency injection. Finding the actual classes that were injected (rather than the interfaces used in the injection APIs) was a true nightmare. Never again.

7 Likes

I'm glad you said all that. I was beginning to think it was just me that thought that way.

Actually, I'm trying to understand what "XML-driven dependency" injection is. A quick skim of Google tells me it's a Java thing. A fancy way of configuring a framework from XML whilst having all hope of knowing your control flow obfuscated. Sounds awful.

1 Like

I have not thought deeply about dependecy injection, its really magical the first time I used it in a Spring Boot(Java based web framework blessed by the enterprises) application. Debugging wastes a lot of time in Java in general. I wish Rocket/Actix were as mature as Spring Boot(for enterprise apps) or Diesel as powerful as Hibernate, but it should be only a matter of time before ecosystem matures. :grinning:

Edit:

I believe what @VorfeedCanal said about XML driven dependency injection is mostly about the way things are done in Spring Boot. The framework uses XML/YAML/Properties files to configure itself. But dependency injection part doesn't really have to be tied to only configuration. In Spring Boot, the framework passes prepared objects for you in annotated methods and classes. Its like asking the framework for any specific component and you can start working with it without having to creating/managing instances manually. It would be nice if we could see something like this on Rust side(increases productivity, but less control over app) but I heard the tricks(Reflection) used to provide this are ugly under the hood.

After a superficial sniff around I still don't get it.

Seems one can write high-level Java code without regard for the lower level classes it may use. You just work to some interface. Then some XML config pulls in and sets up those classes for you at run time.

So one can swap out lower level implementations without recompiling code. Or at least changing any code. Just tweak the XML. Am I right?

Seems to just move things from Java code to XML.

What problem does XML-dependency injection solve?

1 Like

The idea is to decouple components (i.e. normal dependency injection) and use configuration to automate the task of constructing all the dependencies an object may have (and their dependencies, and so on.) without needing to recompile your code.

I'd argue that being able to change your code without touching your code is actually a negative because you can no longer just look at the Java source code and figure out what is going on. Needing to automate how you construct dependencies also feels like a code smell... if you have such a large/complex set of dependencies that you need to automate how they are constructed, then maybe you should try reducing complexity or coupling instead?

2 Likes

Not only this, but I'll argue that dependency injection, after the injection mechanism itself is accounted for, is actually more complex than providing the dependency objects manually.

So not only is it a code smell, it adds a big mountain of complexity and then papers over it by having the programmer only look at, and manipulate, the surface.

2 Likes

I believe you are right. DI may not be the most sound idea. But it is something that increases accessibility. I may have some bias here due to past experiences.

But we will have to take costs involved due to human labor into consideration as well. In the past, I have had to interview a lot of people and what I found truly scared me, it was the understanding that how few people can really write correct code. One strategy I found to get people be more productive is to decrease the cognitive bar for the people, this involves creating abstractions in form of frameworks/dsl to increase the number of hits for the people to correctly solve a problem. This really worked for me, I was able to remove entire months of training out of the window while keeping newly hired motivated(when they see results right from start). The bottom line was that accessibility really matters, it is the difference that will help deliver a product by translating human hours into productivity(which will open doors for their future growth and lead the project to standard and correct practices)

I consider DI to be in agreement with this bias of mine where I maximize for accessibility of technology for cognitively poor people to also contribute and be more productive. I believe technologies like JS and Java are popular only because of this aspect. Rust is also well positioned for this, macros increases accessibility.

It's arguably way too late to make this terminology distinction, but "providing the dependency objects", manually or otherwise, is what "dependency injection" originally meant, as opposed to calling some constructor/function that e.g. opened a database connection that's configured by constants or environment variables, and is therefore hard to test.

Dependency injection by itself is "just" an OO-specific angle on the well-known "don't use global state" principle. Dependency injection frameworks brought in the config file and magic automation stuff, because people wanted global-variable-like convenience.

This subthread is rather off topic of specific mistakes though.

15 Likes

Exactly this.

There's a huge difference between "I have no idea what's injected because it's doing some crazy reflection magic at setup", which I agree is often less helpful than it is annoying later, and "I pass an implementation of the data access layer into the web api layer so I can test it without a real DB", which is very useful.

And with generics you could even have the dependency injection still be static calls on a ZST if you wanted.

9 Likes

What's the reason of using type OnReceivedDamage = Box<dyn Fn(u32)>; instead of type OnReceivedDamage = fn(u32);?
I associate Box with an allocation, and I think you only need to reference function pointers (function items coerced into function pointers?) using fn(u32).