A blog about my experience exploring Rust and its ecosystem

I've had an eye on Rust for a few years, but I took more time recently to explore more seriously its ecosystem, get a feel of the community and use it a bit more to check how mature it feels. I was very pleasantly surprised, kudos to everyone involved.

The positive spirit animating the community has motivated me to blog about it. I've been mostly working on the JVM for the last decade, so I took the opportunity to expand on why Java developers (and others) should have a look at Rust. It's a bit longer than what I originally aimed at, but I wanted to insist on my impression that Rust can help us write better programs without sacrificing anything.

Have a look here:

Cheers,
Simon

17 Likes

Really liked this. One nit though. Towards the end you mention that you feel that Rust will be a great programming ecosystem in a couple of years. To me, with how fast I've seen it move and improve over the last year or so I've been interested in it, I would say it is already a great ecosystem and within 6 months will be highly competitive with C# and Java.

4 Likes

Thanks. I think the actual code running is already competitive or should be in 6 months like you said. For instance Java is dragged down by 15 years of blocking IO which makes the adoption to non-blocking/reactive difficult (for any JVM-based language I'd say). Rust being almost a clean slate, it has avoided many mistakes and jumped straight to where everyone wants to be, plus doing it better and faster.

About the last point, maybe I wasn't clear but I'm mostly talking about 1) the tooling. IntelliJ IDEA and even Eclipse provide an amazing coding, refactoring and debugging experience, 2) the marketing power that reassures decision makers, that Spring or Microsoft have. I am thinking it will take another 2-3 years to be there, but I agree with you. The previous time I had explored Rust was 2-3 years ago-- and the time before when it still had a GC!--, and each time the step has been spectacular.

4 Likes

@magnet, I enjoyed that writeup - thanks for sharing. I also agree that matching C#/Java tooling, likely the best of all languages out there, is going to take at least a few more years.

3 Likes

Thank you for writing that up! I like how you first set your "three laws" criteria, and then compare different languages to these standards.

Rust is also constantly evolving and making the compiler laxer where possible.

(Emphasis mine) I am not sure "lax" is the correct word; "lax" implies letting someone get away with rule-breaking, and that is something Rust will NEVER compromise on.
We're working on making rustc smarter and capable of deeper insights, allowing it to recognize more code as correct (side note: also more helpful, and faster) :slight_smile:
After typing all that, I do see your dilemma; there doesn't really seem to be a one-word-way to express "getting in your way less, because it understands more things as correct now". "autonomous"? "intelligent"?

Finally, thanks for including so many links to awesome rust projects, I hadn't stumbled over the kernel module yet :heart:

All in all, welcome to the Rust community, and thanks for showing your enthusiasm with your wider network!

5 Likes

Hey @juleskers

Thank you for your reply. I see there might be an ambiguity there. Indeed I do not mean that any safety guarantees are lost, but that the compiler tries to be is less strict where it can. I thought lax worked there but maybe was not the right word. I will try to find a better formulation.

Thanks for the welcome :slight_smile: . Reaching a wider network was indeed my objective. There are many great engineers out there who are busy working on cool stuff and don't dedicate time to check out new languages, because there are so many. But I if they see that Rust is getting so much positive press by peers, they might have a look and join the fun. I'm not much of a marketing guy and I don't blog, but I was very impressed by what is being achieved in Rust and thought it could be helpful to talk about it. Hopefully my next blog will be about some cool project I built :slight_smile:

Would "intuitive" be a better word than "lax" in that context? Both in the sense that the compiler will understand more code implicitly and the sense that programmers can write code in a way that makes sense to them because the compiler will understand it.

1 Like

@juleskers @stevensonmt I mixed both your suggestions :slight_smile:

6 Likes

It definitely didn't show :smile: your write-up has a great structure, good pacing and has an overall polished feel!

Yay! Thanks for taking the sensibilities of us dyed-in-the-wool Rust zealots into account :wink:
Nice addiotion with the NLL-link!

2 Likes

You forgot the zeroeth law:

  • The program must be released, because a program that never leaves development is never used.
3 Likes

Are unreleased programs, programs? :slight_smile:

Programs or not, they all cost time, money, and sanity...

Great writeup! I generally agree with everything you said, but I noticed a couple minor things which I'll point out here:

  1. Rust has a modern type system which can automatically manages memory and — along with lightweight runtime checks — ensure it is accessed safely.

    What "lightweight runtime checks" are you referring to? The lifetime system (as far as I know) has no runtime cost: it's the same as using pointers in C/C++, all of the cost is at compile-time.

    Are you referring to the bounds checking on vectors/arrays? Or the refcount cost of using Rc/Arc?

  2. Futures are very similar to an asynchronous Result<T, E> which makes them fit very well with the existing patterns.

    This used to be true, but with the very recent Futures 0.3 it's no longer true: the Future trait no longer has an error type, only a value type.

    Before, you would use Future<Item = T, Error = E>, but now you instead use Future<Output = Result<T, E>> (or you can use TryFuture<Ok = T, Error = E>, which is a shorthand that does the same thing).

    (And the same goes for streams as well, which have been split into Stream<Item = T> for streams which don't error, and Stream<Item = Result<T, E>> or TryStream<Ok = T, Error = E> for streams which can error).

    The end result is similar, but this allows for futures which don't have errors, such as Future<Output = u32>.

    These changes actually make Future and Stream more consistent with the rest of the language: a function might return a T, or it might return a Result<T, E>. Well, now the same is true with futures and streams! They're no longer forced to always have an error type, instead they can choose whether to return a T or a Result<T, E> as needed.

3 Likes

Hey @Pauan

Thanks for your message!

Yes, I was referring to bound checking. I didn't want to imply that Rust achieves memory safety entirely through its ownership system. It is possible to eliminate bound checks while retaining safety but I don't think Rust makes use of these techniques (yet), and they're not general.

Thanks for the correction! I only looked at code using Futures 0.1 before writing that blog. I can see why this change makes the design more consistent, but aren't infaillible futures risky? If a blocking function returns u32 and somehow fails, the only thing left (besides returning an error value, which is a weakly typed approach...) is to panic, but at least the panic happens in the calling thread. With an infaillible future, what is supposed to happen? Is poll() going to panic?

I see it makes a better design, but it seems to me most futures should be TryFuture (probably the same way most functions should return Results, but Futures are even more likely to run impure/IO code and run into unexpected errors...).

Thanks again for your comments!
Simon

1 Like

Perhaps you could explicitly say that in the blog post? I'm competent at Rust and I was confused, so somebody unfamiliar with Rust (your target audience) would probably be even more confused.

No, a Future<Output = u32> is supposed to never fail (or panic, or anything like that). It's the same as functions: a function that returns u32 is saying "hey, I return a u32 and I don't fail".

If it could fail then it would need to use Future<Output = Result<u32, SomeError>> instead.

I/O is indeed one of the purposes of Futures, but Futures are general-purpose and can be used for many things.

I'm using Futures in my Rust Signals crate, which lets you create zero-cost FRP values that can change over time. These values will never error (you can think of them as being like mutable variables), so a Signal<Item = u32> can be converted directly into a Future<Output = u32>, and it's guaranteed to never fail.

Another example is how in stdweb it wraps the JavaScript setTimeout function so that you can use it as a Rust Future. The setTimeout function can never fail or error, so it just has an output of (). This code was clunkier with the old Futures system (because it still had to specify an error type even though it could not fail).


Of course if a Future can fail (and most I/O operations can fail), then yes you'll need to return Future<Output = Result<T, E>> (just like how a function which can fail needs to return Result<T, E>).

For example, when you use stdweb to convert a JavaScript Promise into a Rust Future, it always returns Result, because JavaScript Promises might resolve with an error.


Part of the reason for the new system is that it improves the async/await syntax. Consider this Rust code:

fn foo() -> u32 {
    5
}

fn bar() -> Result<u32, SomeError> {
    let x = call_some_api_which_can_fail()?;
    Ok(x)
}

fn qux() -> Result<u32, SomeError> {
    let x = foo();
    let y = bar()?;
    Ok(x + y)
}

Here we have three ordinary functions: foo never fails, but bar and qux can fail with SomeError.

Notice that it uses the ? syntax for propagating errors (which is idiomatic in Rust). Also notice that it uses bar()? (because bar can fail), but it uses foo() (because foo cannot fail, so it does not need the ?).

Now let's convert those into async functions:

async fn foo() -> u32 {
    5
}

async fn bar() -> Result<u32, SomeError> {
    let x = call_some_api_which_can_fail()?;
    Ok(x)
}

async fn qux() -> Result<u32, SomeError> {
    let x = await!(foo());
    let y = await!(bar())?;
    Ok(x + y)
}

The foo function returns Future<Output = u32>, the bar function returns Future<Output = Result<u32, SomeError>>, and the qux function also returns Future<Output = Result<u32, SomeError>>

As you can see, the code is almost identical. We only had to make two changes:

  1. We added an async marker to each function.

  2. When calling an async function we need to use await!.

In particular, notice that it uses ? for await!(bar())?, but it doesn't use ? for await!(foo()) (this is exactly the same as the non-async functions!)

With the old system, because Futures must always have an error type, the foo function would have needed to be written like this:

async fn foo() -> Result<u32, !> {
    Ok(5)
}

And qux would have needed to use await!(foo())? (even though foo cannot fail).

On the other hand, the new system is consistent with functions, so it's easier to understand, and it's easier to convert a non-async function into an async function.

7 Likes

@Pauan Thank you for your extensive reply. It is much appreciated.

I was busy the last days but I just updated my blog with your suggestions.

Thanks again for your comments and interesting link on that FRP crate of yours :slight_smile:.

1 Like