Common newbie mistakes or bad practices

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.

6 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.

6 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.

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.

12 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.

8 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).

As you said fn(u32) is a function pointer to a function with signature fn foo(u32) -> (). This excludes closures. Under the hood a closure is something like an anonymous struct that holds the captured variables and implements the Fn trait (or FnMut or FnOnce). Therefore, you can't put a closure where a function pointer is explicitly requested.
Interpreting a closure as an anonymous struct, you can also see why the Box is required. As you can't tell what exact type a closure is the Rust compiler can't tell how much memory the closure will require either. Accordingly, you need to allocate memory dynamically at runtime.

4 Likes

The only way your callback can have state is by using a closure to close over other variables. We could technically use global variables, but that isn't a path I want to go down for all the usual reasons.

As @farnbams says, we need to box the closure and erase it's type if we want to accept different callbacks because each closure has its own unique type.

Keep in mind that this is exactly what languages like JavaScript, C#, and Haskell do when you pass a function to another function. In Rust, it's just that the allocation is explicit and in your face.

2 Likes

Box does not allocate when the boxed value is zero-sized (for example, a closure with no captures, or a fn item). So types that can coerce to fn() can also be used allocation-free as Box<dyn Fn()>.

Playground

9 Likes

Thanks, this make sense now.

I was able to understand the allocation when capturing variables by using some sizeof prints.

1 Like