How does transitive dependency work with type inference and compilation

I have a project using workspaces. I have a package, let us call it Client, that depends on another package in the workspace, let us call that Engine. Engine has DataFusion, an external library as a dependency.

Engine exposes a function that returns a value of type DataFusion

What I am finding out is that if, in Client package, I call this function in Engine that returns DataFusion, I can call methods on the values and everything works fine.

But If I define a function in Client package, when takes DataFusion as input, then compilation fails with the error that DataFusion is not found in scope.

To illustrate. This works fine

// engine.transform is defined in the Engine package
let df = engine.transform();
// I can use the df type alright
df.show().await?;

But this does not compile

fn do_show(df: DataFrame) -> Result<()> {
  df.show().await?;
}

Error is cannot find typeDataFrame in this scope even if I add

use datafusion::dataframe::DataFrame;

at the top, error is

  |
8 | use datafusion::dataframe::DataFrame;
  |     ^^^^^^^^^^ use of undeclared crate or module `datafusion`

Which is correct. Client does not have DataFrame as a dependency, it is Engine that has it.

But the thing I find curious is that, when I did not have to explicitly have to define DataFrame as a type everything works. The compiler was able to figure out the type of the df value and it checks that the right methods are called etc.

How is this possible?

And is there a way to have the type available without explicit specifying it as a direct dependency? (I guess not?)

The import and dependency resolution system of Rust is designed to be robust. It wants you to write code that can't be broken by spooky action at a distance.

When you are importing a type alone from a crate, the compiler wants you to have an explicit, direct dependency on that crate. You are not allowed to import items from transitive dependencies because this could break your code, if a crate in the middle of the dependency chain got rid of your transitive dependency in a new release. (And removing a dependency should not be a breaking change.)

On the other hand, if you are using the return type
of a method, that means the method's defining crate is obviously depending on the crate defining the return type. Thus, it's logically impossible for your code to break out of thin air, without the direct dependency also introducing a breaking change (if that direct dependency itself removed the indirect dependency, it would have to change the return type since it would no longer be allowed to rely on its existence).

So all in all, the rules are exactly as strict as necessary to ensure no mysterious breakage, and no stricter.

No. Some crates publicly re-export items from their own dependencies (eg. serde exposes its derive_macros crate through the main serde crate as well), but without this, there's no way to do it. (That's kind of the point.)

1 Like

Thanks for the answer

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.