While coding some Rust, I was a bit horrified by the error messages of some generic-heavy code. So I thought about, what might be viable ways to improve on it, as it was not as clear, as I feel it could have been. Consider it an ad-hoc case study in usability. I hope it is at least a bit informative.
Here is my example code fragment:
boundary_stepper_1d({
// more code...
move |grid, boundaries| /* closure body */
}, state)
To provide some context, I was in the process of refactoring it and decided to swap the two arguments to the outer function boundary_stepper_1d
. But I forgot the instance above. Nothing that was particularly complicated about it, but as I was doing too many things at once (I know), I lost track of that fact for a second.
Now here’s what the compiler told me:
error[E0277]: the trait bound `timely::dataflow::Stream<timely::dataflow::scopes::Child<'_, timely::dataflow::scopes::Root<timely_communication::allocator::generic::Generic>, u64>, ({integer}, timely_cfd::geometry::Grid<nalgebra::Matrix<f64, nalgebra::U2, nalgebra::U1, nalgebra::MatrixArray<f64, nalgebra::U2, nalgebra::U1>>>)>: std::ops::Fn<(timely_cfd::geometry::Grid<_>, [_; 2])>` is not satisfied
--> examples/isothermal_sod_shocktube.rs:19:17
|
19 | boundary_stepper_1d({
| ^^^^^^^^^^^^^^^^^^^ the trait `std::ops::Fn<(timely_cfd::geometry::Grid<_>, [_; 2])>` is not implemented for `timely::dataflow::Stream<timely::dataflow::scopes::Child<'_, timely::dataflow::scopes::Root<timely_communication::allocator::generic::Generic>, u64>, ({integer}, timely_cfd::geometry::Grid<nalgebra::Matrix<f64, nalgebra::U2, nalgebra::U1, nalgebra::MatrixArray<f64, nalgebra::U2, nalgebra::U1>>>)>`
|
= note: required because of the requirements on the impl of `std::ops::Fn<(timely_cfd::geometry::Grid<_>, [_; 2])>` for `&timely::dataflow::Stream<timely::dataflow::scopes::Child<'_, timely::dataflow::scopes::Root<timely_communication::allocator::generic::Generic>, u64>, ({integer}, timely_cfd::geometry::Grid<nalgebra::Matrix<f64, nalgebra::U2, nalgebra::U1, nalgebra::MatrixArray<f64, nalgebra::U2, nalgebra::U1>>>)>`
= note: required by `timely_cfd::dataflow::boundary_stepper_1d`
A lot of information to process there, most of which is irrelevant to me. The essence is that a Stream<_>
is not Fn(_)
. As soon as that is clear, it makes sense, that I mixed up something, as that should obviously not be true.
What is most distracting here to me are all the fully qualified paths. When I strip these out by hand, I get this:
error[E0277]: the trait bound `Stream<Child<'_, Root<Generic>, u64>, ({integer}, Grid<Matrix<f64, U2, U1, MatrixArray<f64, U2, U1>>>)>: Fn<(Grid<_>, [_; 2])>` is not satisfied
--> examples/isothermal_sod_shocktube.rs:19:17
|
19 | boundary_stepper_1d({
| ^^^^^^^^^^^^^^^^^^^ the trait `Fn<(Grid<_>, [_; 2])>` is not implemented for `Stream<Child<'_, Root<Generic>, u64>, ({integer}, Grid<Matrix<f64, U2, U1, MatrixArray<f64, U2, U1>>>)>`
|
= note: required because of the requirements on the impl of `Fn<(Grid<_>, [_; 2])>` for `&Stream<Child<'_, Root<Generic>, u64>, ({integer}, Grid<Matrix<f64, U2, U1, MatrixArray<f64, U2, U1>>>)>`
= note: required by `boundary_stepper_1d`
In this case, all the names are unique in the current context, so there is no chance to confuse them. Some of these names are already explicitly in scope, so they can not clash. And I would rarely refer to the others using their fully qualified name in practice. So the names that I see above are not the ones I’m familiar with from the code.
In this particular case, there is also a type alias in scope, Vector2<T>
that refers to the more complex Matrix<T, ...>
.
Replacing Matrix
by Vector2
it would read:
error[E0277]: the trait bound `Stream<Child<'_, Root<Generic>, u64>, ({integer}, Grid<Vector2<f64>>)>: Fn<(Grid<_>, [_; 2])>` is not satisfied
--> examples/isothermal_sod_shocktube.rs:19:17
|
19 | boundary_stepper_1d({
| ^^^^^^^^^^^^^^^^^^^ the trait `Fn<(Grid<_>, [_; 2])>` is not implemented for `Stream<Child<'_, Root<Generic>, u64>, ({integer}, Grid<Vector2<f64>>)>`
|
= note: required because of the requirements on the impl of `Fn<(Grid<_>, [_; 2])>` for `&Stream<Child<'_, Root<Generic>, u64>, ({integer}, Grid<Vector2<f64>>)>`
= note: required by `boundary_stepper_1d`
And that one actually fits on my screen again
It would be nice, if the compiler was using the same wording for those types as I usually would write down in code and only disambiguate, when necessary instead of using the fully qualified name. Do you think, it would be sensible to make the error messages more terse by leaving out the parts that the programmer does not care about? Or is there too much risk of not providing enough relevant information in other situations?