Hacker News new | ask | show | jobs
by scaleout1 2726 days ago
Recently started introducing Rust in my team and here are my notes on some of the issues we have run into so far. We are primarily a Scala/JVM shop with a little bit of Python.

- `Error Handling` is a bit of a dumpster fire right now. Rust has something called a `Result` enum which is similar to `Either[Error,T]` monad in Scala however every single Rust crate I have used so far its creating its own `Error` enum which makes it really hard to compose `Result`. Ideally I would like to chain `Results` as `e1.and_then(e2).and_then(e3)` but its not possible due to incompatible error enums. Ended up using `https://docs.rs/crate/custom_error/1.3.0` to align the types.

- A lot of basic things are still influx and community wide standards have not been established. For example: I needed to externalize some environment specific settings in a Config but couldnt figure out where to put non-code assets in a cargo project and then how to reliable read them. In JVM world `src/main/resources` acts like a standard place for stuff like this but that patterns has not been established yet.

- Distributing code inside the company is hard because there is no integration with Artifactory or similar tools. We are directly referring git sha in Cargo right now and waiting for better solutions.

- Rust comes with a default unit test framework but it pretty bare bones. I havent seen examples of test fixture, setup/teardown support and loading test configs etc.

- I really like Rust compiler because of really good error messages it produces but its really slow and you start to notice it as you add more code/external crates

-IDE support is good but not great. I am using IntelliJ with Rust plugin as we use IntelliJ for Scala/JVM and it is nowhere as good as even Scala plugin which is pretty mediocre in itself.

Overall I am pretty happy with the language (except for Error issue) and most of my gripes are around the ecosystem and tooling around the language. Hopefully these will be resolved as language gains more momentum

7 comments

You should try using `map_err` and the `?` operator, with your own error enum defined using the `failure_derive` crate, for example:

    foo().map_err(|e| MyError::FooError(e))?
      .bar().map_err(|e| MyError::BarError(e))?;
A tip - you usually don't need lambdas in such cases; this should work:

    foo().map_err(MyError::FooError)?
      .bar().map_err(MyError::BarError)?;
Oh, nice! I did not realize tuple constructors implemented `FnOnce`! Thanks for the tip!
d'oh - you beat me to it ;)
For error handling, check out failure if you haven't already: https://github.com/rust-lang-nursery/failure
Does it help with Error enum coming for third party crates? I can see how it will help with my own code though
> Rust comes with a default unit test framework but it pretty bare bones. I havent seen examples of test fixture, setup/teardown support and loading test configs etc.

This is one of the things that I really love about Rust - it has a testing framework that is so simple that it subtly pushes you to write better tests and better code.

What I mean by that is - it really wants you to write simple tests with no mocking and no convoluted state using simple asserts, and it really wants you to write code which is trivially testable. In other languages this might be problematic, but in Rust this synergises really well with other language features such as #[derive(...)], sum types and pattern matching. If you write good, idiomatic Rust code then all of those extra features of other testing frameworks are usually totally unnecessary.

I can see your point for unit testing but I am struggling to see how it will work for Integration testing. For example I would like to initialize a connection pool (or other heavy object) only once for the entire test, I .. havent figured out how to handle that except for creating a new one in each test. Any pointer on how you are handling those? Thanks
I know a little about two of the big open source Rust projects, Servo and the Rust compiler.

For integration tests, both the Rust compiler as well as the Servo web engine have written their own test runners with an accompanying set of tools. This allows the test format to be as flexible as the target domain of the program. In fact, servo can just simply use the cross-browser wpt [1] test suite.

As for avoiding to initialize heavy objects, I only know about the Rust compiler's test runner initializing the entire compiler every time from zero for every single test file. The result is a quite long testing-time. Suggestions by me to use a shared driver were rejected by them because one test might affect the other one.

But in theory you could write your own custom test runner that has as much shared state as possible. You might also want to check out cucumber-rust [2] which I think allows for some way of sharing. You could also just group smaller tests into a small set of bigger tests where each group of tests shares some resource.

[1]: https://github.com/web-platform-tests/wpt [2]: https://github.com/bbqsrc/cucumber-rust

If it's something that doesn't take too much wall time I just simply initialize those from scratch for every test. If I have multiple tests which need some complex setup I just put that down in a separate function and call that in every test. If it's something reaaally expensive to initialize I just use lazy_static:

https://crates.io/crates/lazy_static

Have you looked at stainless?
> every single Rust crate I have used so far its creating its own `Error` enum which makes it really hard to compose `Result`.

Is there a reason why `map_err()` doesn't achieve what you need - i.e. to get everything into your error type no matter where it came from?

For example: OtherLibResult.map_err(|e| MyError::from(e))

Unless I'm mistaken - you ultimately are doing something like this in one form or another, because if you never match on the different Error variants that happened somewhere along the pipeline- then those details are being swallowed.

Implementing a From trait at least forces you to make that choice (all in one place too)- and then you can keep composing the results as needed since it's all in your MyError type

Yes thats the pattern I ended up using with the help of `custom_error` crate which automates the implementation of `From` for your custom errors. I guess the disconnect for me was that in Java/Scala all custom exceptions extend `Throwable` so types always line up, in rust custom Errors are disjointed so you have to wrap everything yourself to align the types.
`Throwable` in Rust is `dyn std::error::Error`.

If you make your functions return `Box<dyn Error>` (or `failure::Error`), they will all convert out of the box. The `?` operator performs conversion itself. For chains `.map_err(From::from)` does it.

> - Rust comes with a default unit test framework but it pretty bare bones. I havent seen examples of test fixture, setup/teardown support and loading test configs etc.

There is an eRFC for better integration of custom test frameworks.

https://github.com/rust-lang/rfcs/pull/2318

For non-code assets, do you know about `include_bytes`?
I found rust-embed recently[0], which lets you embed an entire directory in release mode while deferring to the file system in debug mode.

[0]: https://github.com/pyros2097/rust-embed

Have you also evaluated Scala/Rust interop?

That would be an ideal combination for me: do performance critical stuff in Rust, use Scala everywhere else.