Uncategorized

Issues I abominate about Rust, redux

Programming, philosophy, pedaling.


Mar 10, 2022

   


Tags:

programming,

rant,

rust

   

Two years within the past, I wrote a submit
with a handful of grievances about Rust, a language that I then
(and restful) have my licensed compiled language.

Within the two years since I’ve gone from intriguing about myself familiar with Rust,
to gratified in it, to thinking in Rust even when writing in other languages
(usually to my detriment). So, worship two years within the past, this submit can have to be be taught
from a web page of worship for Rust, and no longer a price-efficient attempt to knock it.

IntoIterator is simply too overloaded

Here is how the IntoIterator docs
existing the trait:

Conversion into an Iterator.

By implementing IntoIterator for a form, you outline how this might possibly be converted to an iterator.
That is total for forms which listing a collection of some kind.

If that sounds extraordinarily generic to you, it’s due to it is! Listed below are unbiased just some of the ways
IntoIterator is extinct within the wild, the use of a generic Container for motivation:

  • For producing “long-established” borrowing iterators: &T for T in Container

  • For producing iterators over mutable references: &mut T for T in Container

  • For producing “drinking” (i.e., by-rate) iterators: T for T in Container

  • For producing “owned” (i.e., copying or cloning) iterators: T for T in Container1

Each and every of those will seemingly be a helpful iterator to have, which is why container forms steadily have
multiple Merchandise-variant IntoIterator implementations. These implementations are, in turn,
every so usually (optionally!) disambiguated with aliases: iter_mut(), drain()2, &c.

The arrangement back is comprehension: absent of context, an into_iter() will seemingly be doing any of the
above3, leaving it to me (or any other miserable soul) to be taught further into the iterator’s
person to settle what’s in fact occurring. It’s by no formulation ambiguous (most interesting one need is
doable at assemble time!), however it no doubt can be stressful to all of a sudden comprehend within the formulation that
Rust otherwise facilitates.

IntoIterator is already firmly baked into Rust’s core, so it’s presumably too slack to devolve it
into the half dozen traits that it conceptually covers. But if I might turn relieve time:

  • IntoIterator itself will seemingly be spelled AsIterator or ToIterator as an different, to forestall
    the misleading possession connotation of
    Into.

  • OwningIterator and BorrowingIterator would clear up the possession overlap, offering
    iter_owned() and iter() respectively. I’m no longer obvious how successfully this might possibly play
    with the general soundness of Rust’s traits and forms, however I’m in a position to dream.

It’s stressful to write “excessive-assurance” Rust

Rust’s safety is a create of inverted Faustian low cost: in alternate for a runt amount
of adjust over memory layout, we catch entire spatial and temporal memory safety,
computerized memory administration and not utilizing a garbage collector, and nil-rate abstractions
that allow us to settle fats revenue of our optimizing compilers.

As such, after I bid that “excessive-assurance” Rust is stressful, I don’t imply Safe Rust.
What I imply is that we’ve made a alternate: in alternate for all of this safety, we’ve licensed
a positive amount of important invariant enforcement — the Rust long-established library
will horror when an invariant would create unsafety, and neighborhood maintained
libraries will use horror!,
converse!, and the wish to alternate
the occasional uncontrolled program termination for a bit greater programming
ergonomics (fewer Options and Results).

Invariant enforcement is an correct thing and, by and gigantic, each and every Rust’s internal
and neighborhood makes use of of panics are judicious: by convention, panicking functions are inclined to
have either (1) a non-panicking Result or Option different, or (2) failure prerequisites
which will seemingly be environmental in a technique that mandates program termination anyways (e.g., stack exhaustion).

The tip consequence: the Rust long-established library and ecosystem are fats of panics that nearly by no formulation
happen, panics which will seemingly be most interesting specified informally (i.e., in human-readable documentation).
But “nearly by no formulation” isn’t repeatedly loyal passable: it’s usually good to have the peace of mind that
no code being achieved can presumably horror.

To the correct of my knowledge, there are most interesting immoral alternatives to this:

  • It’s top to use clippy to ban supply-degree panics
    to your have code, basically by the use of the
    expect_used,
    unwrap_used,
    and horror lints. Each and every of those
    is disabled by default, so users must explicitly opt into them.

    These lints work excellently for first-occasion code! But they can’t prevent panics
    in third-occasion code4, due to clippy most interesting analyzes the supply of the active
    crate. In other words, clippy won’t assemble the next below any conditions:

    1
    2
    3
    4
    5
    
    use thirdparty;
    
    fn foo() {
      thirdparty:: calls_unwrap_internally();
    }
    

    Rust prides itself on its rich equipment ecosystem, which formulation that unbiased about any third-occasion
    dependency can introduce implicit panics. No longer so loyal.

  • It’s top to use a crate worship no_panic5
    to assemble panics by selling them into compiler (in point of fact linker) errors. That is amazingly entertaining,
    however with a good deal of downsides:

    • It fundamentally depends on the compiler to optimize away unreachable panics, making
      it unreliable at decrease optimization ranges (seriously, the default debug manufacture degree).
      Identical, any tweaking of Rust’s panicking habits (e.g., a determined panicking approach
      worship horror = "abort") can rupture the linker trick being extinct here.

    • It doesn’t work directly on library crates, since library crates don’t directly invoke the
      linker. In present an explanation for to be efficient, the “leaf” manufacture desires to be one thing that requires the linker,
      worship an executable or shared object.

    • Since the errors happen at hyperlink-time as an different of assemble time, they’re largely stripped
      of their supply context. no_panic is entertaining and makes use of a procedural macro to parse the operate
      signature and existing it as piece of the linker error6, however that’s unbiased about the restrict
      of the context it will provide.

      The instance within the README demonstrates this inscrutability:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      
         Compiling no-horror-demo v0.0.1
      error: linking with `cc` failed: exit code: 1
        |
        = display cloak: /no-horror-demo/target/open/deps/no_panic_demo-7170785b672ae322.no_p
      anic_demo1-cba7f4b666ccdbcbbf02b7348e5df1b2.rs.rcgu.o: In operate `_$LT$no_pani
      c_demo..demo..__NoPanic$u20$as$u20$core..ops..drop..Plunge$GT$::drop::h72f8f423002
      b8d9f':
                no_panic_demo1-cba7f4b666ccdbcbbf02b7348e5df1b2.rs:(.textual recount material._ZN72_$LT$no
      _panic_demo..demo..__NoPanic$u20$as$u20$core..ops..drop..Plunge$GT$4drop17h72f8f42
      3002b8d9fE+0x2): undefined reference to `
      
            ERROR[no-panic]: detected horror in operate `demo`
            '
            collect2: error: ld returned 1 exit home
      

      You can perchance ogle the inner callsite to blame for the horror, however no longer without problems.

In sum, it’s very stressful to write provably non-panicking code in Rust in 2022. Warding off
explicit panics in first-occasion code is perfectly doable (and even ergonomic!); it’s the panics
embedded in third-occasion dependencies and runtime code which will seemingly be nearly very no longer going to trace.

I even have some tips for bettering this, ones which will seemingly be commence air the scope of this gripe-fest.
Presumably yet again.

Integration tests feel bolted on

Integration tests are undoubtedly one of Cargo’s extra indirect sides: as well to to hosting your tests in-tree
(i.e., in a mod tests in every foo.rs file), you might presumably furthermore furthermore form a parallel tests/ tree
for tests whose scope reaches past the unit degree.

In other words, if your supply tree appears to be like worship this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
src/
├── kbs2
│   ├── agent.rs
│   ├── backend.rs
│   ├── uncover.rs
│   ├── config.rs
│   ├── generator.rs
│   ├── enter.rs
│   ├── mod.rs
│   ├── memoir.rs
│   ├── session.rs
│   └── util.rs
└── predominant.rs
tests/
├── total
│   └── mod.rs
├── test_kbs2_init.rs
└── test_kbs2.rs

…then your cargo take a look at output might gaze one thing worship this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
william@janus kbs2 [0:0] integration-tests $ cargo take a look at
   Compiling kbs2 v0.6.0-rc.1 (/dwelling/william/devel/self/kbs2)
    Performed take a look at [unoptimized + debuginfo] target(s) in 4.25s
     Working unittests (target/debug/deps/kbs2-2dd9eb541b527992)

operating XX tests
take a look at kbs2::backend::tests::test_ragelib_create_keypair ... ok
take a look at kbs2::config::tests::test_initialize_wrapped ... ok
take a look at kbs2::backend::tests::test_ragelib_create_wrapped_keypair ... ok
take a look at kbs2::backend::tests::test_ragelib_rewrap_keyfile ... ok

take a look at consequence: ok. XX handed; 0 failed; 0 missed; 0 measured; 0 filtered out; accomplished in 8.75s

     Working tests/test_kbs2.rs (target/debug/deps/test_kbs2-4f1d8387af33e18c)

operating 3 tests
take a look at test_kbs2_version ... ok
take a look at test_kbs2_help ... ok
take a look at test_kbs2_completions ... ok

take a look at consequence: ok. 3 handed; 0 failed; 0 missed; 0 measured; 0 filtered out; accomplished in 0.03s

     Working tests/test_kbs2_init.rs (target/debug/deps/test_kbs2_init-d890a2d5d4f7537d)

operating 1 take a look at
take a look at test_kbs2_init ... ok

take a look at consequence: ok. 1 handed; 0 failed; 0 missed; 0 measured; 0 filtered out; accomplished in 0.01s

That is a ravishing characteristic: you don’t must cease the leisure special to cease integration checking out
on a Rust codebase!

Besides for…

  • Cargo doesn’t realize easy the correct formulation to jog integration tests in opposition to a binary-most interesting crate: if your
    src tree has most interesting predominant.rs and no lib.rs, then you definately won’t be ready to use some::mod
    from below take a look at/. That is a
    known difficulty, one without
    an spectacular fix or workaround that doesn’t involve turning your binary’s APIs loyal into a public
    interface7.

  • Each and every file within the tests directory is a separate crate, compiled to its have executable.
    That is an inexpensive willpower, with undesirable penalties:

    • There might be no longer any such thing as a naive formulation to worth a file below tests/ as no longer containing integration tests.

      As the documentation notes,
      adding tests/total.rs to administer shared helpers will add a total portion to your
      cargo take a look at output. The “first rate” workaround is to plot total.rs loyal into a directory-style
      module as an different (total.rs -> total/mod.rs), which cargo take a look at then it sounds as if ignores
      for take a look at collection functions. It’s no longer the extinguish of the field, however it no doubt feels worship an incidental
      hack (it presumably works due to cargo take a look at doesn’t recurse by tests/, which
      doesn’t seem like explicitly documented anyplace).

    • More annoyingly: due to every file below tests/ is its have binary, Rust’s otherwise
      unbiased ineffective code detection doesn’t work wisely on integration tests.

      This difficulty contains the fats ingredient, however
      to summarize: if test_foo.rs and test_bar.rs plot disjoint use of total/mod.rs, then
      rustc will ogle “unused” code within the compilations of each and every test_foo and test_bar, in spite of
      the totality of all integration tests having entire coverage for total/mod.rs.

      That is yet again mentioned most interesting obliquely within the documentation: you ought to know that separate
      compilations imply that Cargo won’t tune ineffective code to your helper modules, regardless that
      the “pattern” of submodules below tests/ is one that Cargo otherwise knows about.

      The fix? I don’t say it’s an correct one, however I ended up striking #![allow(dead_code)]
      on the extinguish of my total integration take a look at module.

These are trivial quality-of-developer-lifestyles things, every of which has a very loyal reason
for no longer being assorted8. But they’re restful a proceed!

Bonus: cargo install is simply too interested

cargo install is the main interface for installing user-going by executables from the crates
ecosystem. Since it’s built unbiased into the Rust toolchain, a entire bunch initiatives listing cargo install $FOO
as a counseled set up formulation. As much as now, so loyal.

What’s no longer so loyal is how cargo install chooses to cease builds. Not like cargo manufacture,
cargo install ignores Cargo.lock by default, which formulation that a determined however “like minded”
(per SemVer) model might be chosen for the absolute top compiled product.

There are (as a minimum) two issues with this:

  1. It violates one of the most important (presumably incorrectly) presumed consistency of telling users to
    jog cargo install to install your program: every user might possibly furthermore merely have a a bit assorted dependency tree
    reckoning on when they ran cargo install. Debugging runt compatibility errors then turns into
    an exercise in frustration, as users and maintainers settle the relevant differences in their
    dependency bushes.

  2. More perniciously: cargo’s interpretation of semantic versioning diverges from the long-established interpretation:

    • cargo install (and other cargo subcommands?) treat 0.X.Y and 0.X.Z as like minded
      releases, in spite of the SemVer spec explicitly asserting otherwise.

    • cargo install treats pre-open variations (e.g. 2.0.0-pre.1) as like minded with each and every
      their main open (i.e. 2.0.0) and all other pre-releases within the same range
      (e.g. 2.0.0-pre.2), in spite of the SemVer spec warning that prereleases
      have to be treated as unstable and non-API-conforming.

The weak habits will seemingly be frustrating, however is one way or the other justifiable in an ecosystem that largely
respects semantic versioning: it nearly repeatedly is excellent to install foo 1.2.4 as an different of
foo 1.2.3. When a equipment misbehaves (i.e., fails to computer screen SemVer) or this habits merely isn’t
desired for whatever reason, cargo install --locked offers an rupture out hatch (albeit no longer
a default one).

The latter habits is, in my impress, unjustifiable: it’s inconsistent with the compatibility
requirements established by SemVer and otherwise respected by Cargo (and the overwhelming majority
of crates within the ecosystem), and directly interferes with any makes an attempt to utilize pre-releases
(as well to open candidates, betas, &c.) in a trusty formulation in programs that traditional
users are expected to install.

The umbrella difficulty for this has been commence since 2019, and is tracked
here. Prominent initiatives which have had
cargo install failures due to it embody (in no explicit present an explanation for):

  • bat (SemVer violation)
  • cargo-lengthen (SemVer violation)
  • xsv (Dependencies require a more moderen compiler9)
  • sqlx (Unsuitable beta/rc upgrade)
  • cargo-squawk (SemVer violation)
  • cargo-geiger (Dependencies require a more moderen compiler)
  • c2rust (Dependencies require a more moderen compiler)
  • rage (Unsuitable beta/rc upgrade)

Wrapup and honorable(?) mentions

On the extinguish of the day, Rust is restful my most smartly-appreciated compiled language and beauty ecosystem.
I ogle the magnify in visible issues as a operate of my elevated familiarity with the language,
no longer as insurmountable flaws — in spite of every thing, identical issues exist in unbiased about every language
(and packaging ecosystem).

I didn’t wish to bloat this submit with too many grievances, so here’s a smattering of different
(extra minor?) things that I’ve seen through the years:

  • The static diagnosis tale for unwanted effects and accidental records use restful isn’t mountainous in Rust
    — it’s remarkably easy to discipline off accidental unwanted effects by forgetting to utilize closures in
    long “fluent” capacity compositions, or to accidentally drop records at some stage in I/O by shedding a buffered
    I/O tackle that restful has pending recount material.

  • Pin and co. aren’t very ergonomic, and self-referential structs are even much less ergonomic.
    I would entirely worship to ogle a 'self lifetime that doesn’t require a third occasion crate
    worship ouroboros.

  • Procedural macros are onerous to write, more difficult than they have to be.
    Crystal has an fine and extraordinarily ergonomic
    macro machine that
    Rust might be taught from, one that doesn’t require advert-hoc reinterpretation of language tokens and
    that integrates seamlessly with syntax highlighting in editors.




Discussions:

Reddit

Twitter


Content Protection by DMCA.com

Back to top button