DEV Community

Cover image for My first impressions of Rust

My first impressions of Rust

Deepu K Sasidharan on November 07, 2019

Originally published at deepu.tech. So I started learning Rust a while ago and since my post about what I thought of Go was popular, I decided to ...
Collapse
 
ghost profile image
Ghost

About the complex String data type, give it another look, is not unnecessary complexity but a result of the language focus, performance is important for Rust and the difference between str and String is not a trivial one, I'm a noob too and it also felt akward at first but now kinda make sense to me and is better understood when compared to an array and a vector. The array is stored in the stack, wich is much faster but you have to know their size (which mean type AND length) at compile time, making it more efficient but unable to grow or shrink at runtime; on the other hand a Vector is stored in the heap, slower but can be resized at runtime; the same goes to str, which is a reference or "pointer" to a memory space part of another str (stack) or a String (heap), that reference size is known at compile time (size of a memoty reference) hence it can be stored in the stack (faster). You could work just with Vecs and Strings and you would get the same "complexity" of more "high level" programming languages but you would lose a lot of potential performance. Is not an unnecessary complexity is just extra complexity because is a "lower level" language, it let you deal with memory management. Give it another read I came from Python and felt useless complexity but after some extra reading and thought it became clear, simple, elegant and awesome.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Of course I understand that difference and I think also coz of the fact there is no GC its important to make the distinction. So yes I agree that it is a required compromise but I still dislike it 😬

Collapse
 
ghost profile image
Ghost

I'm not sure is about the absence of GC, AFAIK GC is about when to drop data, the distinction of arrays/vectors and str/Strings is more about where in memory the data is stored: fastest stack or runtime resizable heap. I'm not sure you can have the performance required for a systems PL without this extra complexity. You can't control something that you can't distinguish, I guess

Thread Thread
 
deepu105 profile image
Deepu K Sasidharan

I thought I read that somewhere. Need to double-check. It would be interesting to know how Go does it, which is also quite performant. Also to benchmark.

Thread Thread
 
ghost profile image
Ghost

I don't see how could you avoid putting everything on the heap or having some runtime check; in both cases performance would be hit, we have to remmember that Golang and Rust have 2 different targets; Rust is a system PL first and general later while Golang is more a fast general PL first and maybe could be systems PL?, if your OS or browser is as little as 20% slower, you'll really notice. So for systems PL performance is not a luxury; I see Golang more like a power Python and Rust as a modern C; in realms like backend web dev they may overlap but the core target is very different

Thread Thread
 
deepu105 profile image
Deepu K Sasidharan

You are absolutely right. It is not really fair to compare them in the same scale. The problem is many people who learn one of these end up using it for everything that is thrown at them.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Maybe I'll start to like it more if I use it more 🙏

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Also, I'll probably move it to Nitpick

Collapse
 
turbopape profile image
Rafik Naccache

I think rust must be put in context. This is intended to be a modern systems programming language(I've seen people have developed Operating Systems with it). This is definitely not for Web or Scripting, and so what seems to be complex is just an approach to tackle what's inherently complex in systems programming, I think. You would definitely not use it to write web apps, but if you have to write firewalls or network probes, I guess this is your best choice nowadays.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Yes, you are right, but I was not talking from building a web app per see, I was purely talking from the language perspective. Btw, Rust is the most impressive language I have come across.

Collapse
 
turbopape profile image
Rafik Naccache

Yep, it is impressive :) I know you weren't mentioning any application type, but to understand the language spirit, one must keep in his back-mind what problems it was meant to address. This was my point :)

Thread Thread
 
deepu105 profile image
Deepu K Sasidharan

I understand, but I feel some of my criticism are still valid even in a systems programming sense also Rust is definitely being used in more than systems now, like web assembly, CLIs and so on

Thread Thread
 
turbopape profile image
Rafik Naccache

Absolutely

Collapse
 
mohamedelidrissi_98 profile image
Mohamed ELIDRISSI • Edited

You didn't mention "The Book"! It covers pretty much the whole language and its all what a rust beginner needs, oh and you can access it offline using

rustup docs --book
Collapse
 
deepu105 profile image
Deepu K Sasidharan

I didn't know this. Will check it out. Thanks

Collapse
 
eugenebabichenko profile image
Yevhenii Babichenko • Edited

About functions not being 1st class citizens: you can use types like impl Fn(u32, bool) -> u32. This means "return/take something that implements Fn(u32, bool) -> u32 interface". The underlying implementation can be either a function or a closure.

What is unfortunate, that you cannot write something like type MyFn = impl Fn(u32, bool) -> u32. This is a nightly-only feature right now.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

But functions and closures are not interchangeable right? So functions still aren't first class citizens IMO

Collapse
 
grayjack profile image
GrayJack • Edited

They are, the thing is, in Rust, callables are much more complicated than usual languages, at least what is exposed to the programmers.

We have 3 traits for callables, FnOnce, Fn and FnMut.
All functions, function pointers and closures implements FnOnce.
All functions, function pointers and closures which does not move out of any captured variables implements FnMut, indicating that it can be called by mutable reference.
All functions, function pointers and closures which does not mutate or move out of any captured variables implements Fn, indicating that it can be called by shared reference.

The only way to have pass either function or function pointers or closures as parameter are using these traits, so on every context they are interchangeable.

Thread Thread
 
deepu105 profile image
Deepu K Sasidharan

Ok, I need to dwelve into this more. Now its more confusing to be honest. If they are same or interchangeable why have 2 concept/syntax? Why not keep it simple? Was there any specific reason to have both function and closures?

Thread Thread
 
grayjack profile image
GrayJack

Well that's the bit it gets really confusing, they are not the same, but are interchangeable, when we say "function as first class citizen", we meant "callables as first class citizen", my hypothesis is because when the term was created, most major languages at the time had no more than one type of callable down on it's AST level that was functions, but that's not the point.

In Rust all functions have a type, and all closures are a type on it's own, but both are callables.

I have no knowledge why this way, but my instinct is that maybe something related to closures ability to capture the outer environment variables and the type system and optimizations.

I found the blog post that are talks about this:
stevedonovan.github.io/rustificati...

Collapse
 
eugenebabichenko profile image
Yevhenii Babichenko

What do you mean by "interchangeable"? If a function accepts a parameter of type impl Fn(...) -> ..., then this parameter can be either a function or a closure (so they are interchangeable). You can do many other other things with such types (return them, assign to variables, etc). IMO this is something that looks like 1st class functions.

Thread Thread
 
deepu105 profile image
Deepu K Sasidharan

See my comment above

Collapse
 
tensorprogramming profile image
Tensor-Programming • Edited

Shadowing is a bit of a misunderstood feature in rust. It came from ocaml which is the language rust was originally built on top of. You mention that shadowing is like mutation but it really isn't. Shadowing is not mutation because a shadowed let expression isn't an assignment. Each let binding binds a new value and it's irrelevant as to whether or not that variable name is the same as another one. In other words, the original immutable value can still exist especially when we are talking about multiple scopes (from functions and closures etc) while another value is shadowing it. It's sort of like how closures can freeze an environment and it starts to make more sense when you consider move semantics and stuff like that. Just as with any other immutable value, rather then change the value you replace it a different one.

Perhaps this is a feature that can be abused and used to generate bad habits but its not nearly as bad as you make it sound. Anyhow, you should look into it a bit more and consider the potential use cases (both good and bad). Even in inexperienced hands it's not that big of a deal.

I agree with most of your assessment though. I have taught rust for almost 3 years now, it's very easy to see the pros and the cons of the language especially when stacked against something like go. I like rust and I also like go, but when it comes to building something quickly, I'll choose go 9 times out of 10. Either way both are great tools to have in your programmer toolbelt. They are languages that will continue to define the future of the discipline going forward.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Hi thanks for the detailed response. As I said I do understand the reasoning behind it and I have no problems with shadowing in different scopes, my problem, as mentioned in post, is with shadowing in the same scope. I see more cons than pro in that case. Yes its not exactly mutation in theoritcal sense but can achieve the same result of accidental mutation with this.

Collapse
 
l0uisc profile image
Louis Cloete

Shadowing is most useful when you are parsing keyboard input:

use std::io;

let mut num = String::new()
io::stdin().read_line(&mut num).expect("Failed to read line");
let num: usize = num.trim().parse().expect("Failed to parse number");
// Continue to use num as immutable usize. The mutable String is now not accessible any more.
Collapse
 
gisleburt profile image
Daniel Mason

I'm not sure why implicit return would be error prone since you have to specify the return type. Do you have an example?

Regarding this, it might be easier to think about everything in Rust having a type and a value. Thats why you can do things like:

let x = match { ... };
let y = if ... { ... } else { ... };

This is true even when you don't explicitly return a type. A function without a specific return type actually returns a unit type ().

Shadowing is a bit more complex than you've mentioned here, and does have its uses, though I think it's more often than not a bit of a smell if you're using it. Below is an example of how it can be used with scoping blocks, note that when the second x goes out of scope, the first one still exists. This is because you're not resetting it, you're creating a new var with let and using that instead.

let x = 1;
{
  let x = 2;
  println!("{}", x); // 2
}
println!("{}", x); // 1

The differences between &str and String are complex (not to mention &String and all the other types such as OsString or raw data in a &[u8]). It definitely took me some time to get my head around, but all these different types actually perform very important functions.

Lots of languages provide iterators as well as loops (also things like generators which Rust also provides but only in nightly atm), but I do agree again that having 3 different types of function is confusing, I still struggle with it.

TL;DR: I think your frustration at the complexities of the language are sound, however the more you use it I think the more you'll understand why they made those decisions (also, just wait until you hit lifetimes).

Collapse
 
deepu105 profile image
Deepu K Sasidharan • Edited

Thanks for the response. The reason I thought the implicit return is error-prone is as below

    let number = {
        5 + 6;
    };

    let number = { 
        5 + 6 
    };

Both are valid as per compiler but have different meanings. I wasn't aware of the () type and it makes a bit more sense, and ya seems like in most cases compiler would catch if you accidentally miss a ; or add one in a block scope. Anyway, it was a nitpick and I think I'll retain it as such as I still don't like it. Maybe having a keyword like yield would have been much nicer and explicit. I'm not a fan of anything implicit.

For shadowing, I think you missed my point. I have no problem with shadowing in a different scope, it is indeed a good thing. My problem as mentioned in post is with same scope shadowing, see below

fn main() {
    let foo = "hello";

    // do stuff here

    let foo = "world";

    println!("The value of number is: {}", foo);
}

How is it any different from below in actual practice, it gives you a way to work around an immutable variable without marking it immutable. Again its a bit implicit IMO

fn main() {
    let mut foo = "hello";

    // do stuff here

    foo = "world";

    println!("The value of number is: {}", foo);
}

About iterators and stuff, I would still prefer to have fewer ways to that

Also, I agree, as I use the language more I'm sure I would come to terms with these and I might even like some of them more, the same case with JavaScript, there is a lot of things I don't like about it but it is still one of my favorite languages.

Ya, I found the lifetime stuff to be strange but didn't form an opinion on them yet. So far it seems like a necessity due to language design.

Collapse
 
l0uisc profile image
Louis Cloete

The compiler will tell you exactly why your number, which is of the unit type (()) won't work in a context expecting an integer. I don't think that's really so much of an issue. Also, you can post on users.rust-lang.org and probably be helped within 30 minutes with a very detailed explanation of your mistake and its fixes.

Thread Thread
 
deepu105 profile image
Deepu K Sasidharan

Yes, I agree that Rust compiler is quite wonderful and when actually using it, it will not be a big deal. But from a readability standpoint I would still prefer explicit intent. To me good code should be readable even to someone not familiar with a language. Of course no language is perfect in that sense but one can dream of it right.

Collapse
 
louy2 profile image
Yufan Lou • Edited

Thank you for sharing your experience of trying out Rust!

For optional struct fields, there's derive-new which gives you various T::new() constructors. Derive is borrowed from Haskell, similar to Project Lombok for Java.

The three Fn* traits are a consequence of Rust's memory safety model built on linear logic, commonly known as lifetime / ownership. They are still first class values, meaning you can use let to bind them with a name and pass them in parameters like any other. They are just more complicated than function in JavaScript or closure in Java.

Rust has Fn, FnMut and FnOnce in order to enforce at compile time rules which Java or JavaScript can only panic at run time:

  • FnOnce can only run once. These can be functions like free() in C or close() in Closable. Calling FnOnce twice on the same object or context would not compile. thread::spawn() uses FnOnce to enforce that no references are passed to the new thread.
  • FnMut can mutate its captured state. This is most similar to a normal function in Java and JavaScript. There can be only one FnMut capturing the same binding existing at one time. If more, it would not compile.
  • Fn cannot mutate its captured state. This is somewhat analogous to const function in C++.

There's an order: FnOnce < FnMut < Fn. A Fn* can be used in the place of ones to its left.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Wow. Thanks this is so far the most simple and straightforward explanation I got and now it makes more sense. I guess I would have to update the post to reflect what I learned about functions.

Collapse
Collapse
 
jeikabu profile image
jeikabu

It comes down to personal preference, but I like the variable shadowing. When working with immutable types and stricter type system you tend to end up with a lot of temporary locals:

let s = String::new(something);
let stripped = a.trim();
let s_cstr = CString::new(stripped);
let bytes = s_cstr.as_bytes_with_nul();

As in this case, you can often try to do more on one line, etc. But it’s handy to avoid creating a block expression.

Another thing I like about Rust is how it moves by default. This is similar to move semantics in c++11.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Agreed it is useful but to me it can be abused and can cause accidental errors as well. Maybe once you are experienced enough it might not be a problem, but then the same argument can be used for languages like JavaScript that lets you mutate by default. I can say that I have not had that issue as I'm experienced and I do not do it.

Collapse
 
mmstick profile image
Michael Murphy • Edited

Concurrency in Rust is not tied to a particular threading module, and may not even be tied to threads at all. It is up to the programmer to choose how to concurrently execute their tasks.

Using futures, it is possible to join multiple futures together so that they execute concurrently from the same thread. When one future is blocked, the next future in the queue will be executed.

// Concurrently execute two tasks on the current thread
let (result1, result2) = join!(future1, future2);

// Concurrently execute tasks with a common error type.
let result =  try_join!(future1, future2, future3);

It's also possible to distribute them across a M:N thread pool, and interchangeably mix and match both approaches. Spawning a future will schedule it for execution on your executor. The executor may be based on a thread pool, or run on the same thread. Depending on which you choose, they may have Sync / Send restrictions.

task::block_on(async move {
    // Each spawn returns a JoinHandle future to the result.
    let future1 = task::spawn(future1);
    let future2 = task::spawn(future2);
    let future3 = task::spawn(future3);

    // Concurrently wait for all three threads to complete.
    let (result1, result2, result3) = join!(future1, future2, future3);
});

Often times there's two different pools to spawn tasks to: non-blocking thread pools, and blocking thread pools. Tasks which block should be spawned on blocking pools so that they avoid blocking tasks on the non-blocking thread pool. The async-std crate provides spawn_blocking for that.

Without futures, you may use crates like rayon to distribute blocking tasks across a thread pool. This used to be the preferred threading model before the introduction of async/await with futures.


There is an issue with your assertion that functions aren't first class. You can accept both functions and closures as input arguments, and return functions and closures as output arguments. Generics is required for returning closures, however, because closures are dynamically-created types.

fn function_returning_function() -> fn()  {
    name_of_other_function
}

fn function_returning_closure() -> impl Fn() {
    let mut var1 = ...;
    let mut var2 = ...;
    move || {
        ...
    }
}
Collapse
 
deepu105 profile image
Deepu K Sasidharan

Yes, concurrency in Rust is quite interesting

Collapse
 
davidwhit profile image
DavidWhit • Edited

I didn't double check myself but I'm sure you can't shadow a const. I'll just add that they are also inlined to be accessible anywhere so there address changes. That and they live for the live of the program like static.

Collapse
 
l0uisc profile image
Louis Cloete

You can't shadow a const, but you can shadow an immutable let binding.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Thanks

Collapse
 
martinhaeusler profile image
Martin Häusler

Rust has very interesting concepts (e.g. ownership), but coming from Java/Kotlin the syntax makes me want to scratch my eyes out. Rust is the only programming language I've ever seen that manages to have single-quotes (') NOT showing up pair-wise. Why..?

Collapse
 
deepu105 profile image
Deepu K Sasidharan • Edited

Yes, the syntax does seem a bit strange. But I have to admit Rust syntax was much easier for me to stomach than Scala 😜

The single quote has special meaning and its never used for strings, its more of a marker for tagging stuff

Collapse
 
louy2 profile image
Yufan Lou

A preceding apostrophe has been shorthand for QUOTE in Lisp since 1974.

Collapse
 
simonvoid profile image
Stephan Schröder

The big advantage of traits over interfaces, is that you can implement (your) traits on structs that you don't have source control over (-> structs from any lib you use).
Orphan rules apply: you can't implement other people's traits on other people's structs (because those could clash with other people's implementations), but all other combinations of who wrote trait and struct work.

Collapse
 
paulyc profile image
paulyc • Edited

You have a much clearer understanding and more nuanced intuition for programming languages than practically everyone else I've read pontificating on the subject. So many times I read these kind of things and want to ragepost a very long bulleted list, or subsectioned subparagraphed outline, of all the reasons why they are 100% completely wrong with personal anecdotes (all company and employee names have been changed) of the destruction invariably wrought when people do exactly that thing.

So it's very refreshing to read what someone thinks who clearly has a lot of experience in the trenches fighting fire.. and I more or less agree. Go sacrifices usability for maintainability. Few places i ever worked had much maintainable code (or used Go) so it's very much the unsung hero of successful software companies and the Achilles' Heel of unsuccessful ones.

Rust sacrifices everything at the altar of correctness.... Which is very much a double edged sword when ultimately it's what your software does right and how fast you did it much more than what it does wrong that anyone (i.e., your boss, not as much the other engineers but still them too) cares about in the real world.

And I kid, rust sure doesn't sacrifice a lot of things, it's like a more readable Scala, it does everything reasonably well. But not at the cost of possible errors however minor. All that said, I'd much rather be writing Rust any day of the week, but I'd probably get paid more and have more job security to write Go.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Thanks and yes I agree. There is no silver bullet. Every language has strengths and weaknesses and that is why I'm learning more languages when I can so that I can use the most appropriate one for the usecase in hand.

Collapse
 
guyinpv profile image
Zack

You compared Rust and Go with the likes of JS, Java, others. I'm curious, would they also make for good replacements as a web server-side language in the place of PHP as an example? Or are they meant for some other purposes like AI or games or data manipulation or something?

It would be nice if you left a very general thought about where using the language would make the most sense. i.e. "If I were building an X, Go would be a great choice."

Collapse
 
deepu105 profile image
Deepu K Sasidharan

I did touch upon that in my other post about Go. But I do have plans to write a post covering multiple languages that I have used in my career where I could atleast highlight why I think certain language fits well for certain usecases. But that post would need some hard work as I need substanciate with usecases and benchmark to keep it fair. Keep tuned.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Logic wise yes, but my point is purely based on semantics. In Go if I want to iterate there is only one syntax for that. If I want to write a procedure there is only one syntax. In Rust you always have to make a choice. I know its not a huge deal once you are comfortable with the language but still would be nice if it was simpler. I like it that way and is a personal opinion.

Collapse
 
infensus profile image
Dominic • Edited

I really wanted like Rust and I do like several things. Honestly I just find the compiler annoying, it treats all code as if it could be multi-threaded even if it's not - so you have to jump through hoops and hours of learning about things like Arc, Rc, Cell, RefCell, Box, RwLock etc, not to mention the infuriating rules around lifetimes and borrowing.

Simple fact is that these data structures are inefficient and Rust advocates get aggressively defensive over anything Rust, they will tell you things like "A reference counter and dynamic borrow checking is hardly any difference"... yes it is. And they will tell you "Just copy it, it's just 2 fast shallow copies to read and write the value back". We are often talking about systems where nano seconds matter here, otherwise I'd just go and use something like Go. So yes it matters.

C and C++ is much more intuitive to me (ok C++ has a lot of features, some that's missing in Rust), but they are much easier to build a mental model about. I don't need a compiler to whine at me for holding 2 references to an object - that is organic and necessary in many situations (e.g. two data structures that can reference an object efficiently for different operations). And 99% safe on a single thread but Rust will force you to spend hours looking for less efficient solutions. The module system in Rust is also bizarrely over engineered.

The best thing about Rust is Cargo. If C++ had a Cargo equivalent there would be no contest for me.

To me Zig is a much more promising language but not stable yet - ziglang.org

Collapse
 
unicodex profile image
UNIcodeX

I'd be interested to read one of these for Nim.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

I'm pretty sure that will appear in time. It's new so need time for that

Collapse
 
saleh_rahimzadeh profile image
Saleh Rahimzadeh

Thanks, your article introduce some aspect of Rust to me simply

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Glad to be of service

Collapse
 
karrq profile image
Karrq

Regarding your point on functions not being first class citizens:

play.rust-lang.org/?version=stable...

Keep rusting!

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Yes, I loved it

Collapse
 
kip13 profile image
kip

Good perspective.

This is my recommendation related to closures and first-class functions.

Collapse
 
steeve profile image
Steeve

Thanks for your review, Rust reminds me a lot C language but it is much safer about memory, I have to try it out!

Collapse
 
jeffbski profile image
Jeff Barczewski

Great article! I echo much of the same thoughts, though I would choose Rust over Go as my default systems language of choice due to better functional, immutable capabilities.

Collapse
 
l0uisc profile image
Louis Cloete

Your picture at the top is technically not true most of the time since NLL (Non-lexical lifetimes) were implemented in the borrow checker.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Well, I borrowed the picture from an official Rust blog so 😬

 
mohamedelidrissi_98 profile image
Mohamed ELIDRISSI

Wanna see some rust code in production? Checkout bloom.sh

Thread Thread
 
deepu105 profile image
Deepu K Sasidharan

Seems interesting

Collapse
 
richardeschloss profile image
Richard Schloss

Have you taken the Servo browser for a spin yet? How well does it render DEV.to? I've been meaning to give that one a go again.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Not yet, unfortunately

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Btw, I have used Rust for some real use cases and some of my opinions have changed. Take a look at my follow up post deepu.tech/concurrency-in-modern-l...

 
deepu105 profile image
Deepu K Sasidharan

Thanks, that makes more sense. I'll update the post accordingly

Collapse
 
jkgan profile image
Gan Jun Kai • Edited

Thanks for sharing! Actually I like the "Implicit implementation of traits", it allows me to add extension into primitive types so that I can do something like: github.com/jk-gan/aion

 
deepu105 profile image
Deepu K Sasidharan

True, Go is too simple in other aspects. I think having Generics and error propagation alone will make Go twice better and I'll gladly choose it as one of my primary language

Collapse
 
deepu105 profile image
Deepu K Sasidharan

How is it different from having a top level immutable let statement in terms of usage? May be I'm missing something obvious

Collapse
 
ofergal profile image
Ofer Gal

Why even have yet another language?

Collapse
 
deepu105 profile image
Deepu K Sasidharan

I don't think Rust is just another language. It fills the gap for a systems programming language that is easier to work with than C++ and IMO is a much needed one.