Just started with rust!

Hi Rustaceans, I am a web developer in Elixir, I just started out my journey with Rust yesterday, and started following the book on doc.rust-lang.

After going through the 3rd chapter the book told...

try building programs to do the following:

  • Convert temperatures between Fahrenheit and Celsius.

So, I thought let try writing some code, celsius to Fahrenheit should be simple and I should be done in 15min, but I was wrong it took me an hour to get it working....

After some...

cannot find tuple struct or tuple variant `ok` in this scope

A pinch of...

expected `f32`, found enum `std::result::Result`

Some...

48 |             let result: f32 = (input - 32.0) * 5 / 9.0;
                                             ^ no implementation for `f32 * {integer}`

and after some more of this and that I finally came up with...

use std::io;

fn main() {
    loop {
        println!(
            "Enter 1 for fahrenheit to celsius conversation and 2 for celsius to fahrenheit converstion"
        );

        let mut choice = String::new();
        io::stdin()
            .read_line(&mut choice)
            .expect("Failed to read line");

        let choice: u32 = match choice.trim().parse() {
            Ok(num) => {
                if num < 1 || num > 2 {
                    println!("Incorrect choice try again !");
                    continue
                } else {
                    num
                }
            }
            Err(_) => {
                println!("Incorrect choice try again !");
                continue
            }
        };

        let choice_str = if choice == 1 { "celsius" } else { "fahrenheit" };

        println!("Enter temperatur in {}", choice_str);

        let mut input = String::new();
        io::stdin()
            .read_line(&mut input)
            .expect("Failed to read line");

        let input: f32 = match input.trim().parse() {
            Ok(num) => num,

            Err(_) => {
                println!("Wrong temperature input, try again !");
                continue;
            }
        };

        if choice == 1 {
            let result: f32 = (input - 32.0) * 5.0 / 9.0;
            println!("{}F is {}°C !", input, result);
            break;
        } else {
            println!("{}°C is {}F !", input, ((input * 9.0 / 5.0) + 32.0));
            break;
        }
    }
}

So, first of all, how can the above code be improved made shorter and how can I avoid so much error handling and DRY out common blocks of code like error handling logic, etc.

Some questions...

  1. Can I call it a functional language?
  2. Is there any REPL where I can try out snippets of code, currently I use the online rust playground, it works fine but I am used to something like irb in ruby or iex in elixir.
  3. From my 2 days experience with rust, iI feel like writing code in rust is a lot of work, like I have to specify data types, arrays are fixed-length, etc. Maybe because I have been using some other languages before which felt easier I guess. :thinking:
  4. Any project idea I might make in rust(after I learn the language of course) which might give me some experience with writing code in rust and also something which has some utility, like in elixir I made this to learn about genservers in elixir.
  5. What could be the use cases of running rust code from elixir, I found this.

I hear rust can be used to design compilers, OS, system programs, web servers, etc all this sounds very daunting to me. How can I use rust for something more simple (don't know if that makes sense), like how rust can help in web development or day to day coding?

Lastly, any tips on how to proceed, study resources for a newbie...

Thanks to anyone who takes time to read this answer my questions :slightly_smiling_face:

1 Like

Nobody prevents you from doing so. These labels like "functional", "obejct-oriented", "imperative" etc. are, however, not particularly useful. Rust is not a purely (or pure, for that matter) functional language, it's not a purely OO language, it's not a purely imperative one. It borrows good ideas from many languages and paradigms. So don't worry about what you call it. Call it Rust and try to learn its idioms.

I'm assuming you mean "interactive", like a REPL. There's no REPL but you might be able to fire up programs like "miri" that work similarly, although Miri is definitely not a beginners' tool.

You have to specify types in type definitions (ie. when you are rolling your own struct or enum), and across function boundaries – this is to communicate intent and to protect you from making mistakes and exposing accidental/fragile APIs. In the owerwhelming majority of the time, Rust is smart enough to infer all other types.

Yes, Rust is statically and strongly typed. It's not a rapid prototyping language. If you want to play fast and loose, use Python or JavaScript or whatever you like. Rust is for building robust software. Once you learn the idioms though, it's not any more painful than trying to debug loosely-typed or untyped code at runtime.

I don't feel qualified to answer this one as I'm not fluent in Elixir, but probably anything of real
practical use will make you learn some of the more advanced features of the language.

Do you mean why you would want to do that? Or do you mean how you would do that? In the latter case, google "rust FFI". That will most likely involve unsafe code though – again not something I'd recommend newcomers to do.

3 Likes

Specifying data types are helpful in its own way. I can understand that it feels strange if you're used to dynamically typed languages, but it really helps to make it easy to use a library or your own function you wrote 3 months ago if the types it expects and they type it returns is in the code spelled out. Also, no ValueErrors or TypeErrors here at runtime...

Arrays being fixed length is because of Rust's "systems language" nature. The other languages you used probably hid this from you, but variables in a program can be stored in two different places in memory: the stack and the heap.

The stack is where all local variables are stored. Creating variables on the stack is fast. It is basically done at compile time, so you don't pay any runtime cost each time your code is run. Accessing them is also fast, since all the variables on the stack of a given function (the one currently executing) are next to each other in memory and thus in your CPU's cache.

However, this all is a tradeoff. The reason all this is so fast and can be done at compile time is because the size of the variables are known at compile time. The values are also only valid until the function in which they were declared returns. This means you have to copy the value to the calling function in order to return it from the called function. This is fine for small types with known size like integers and floats.

The heap, however, is generally for variables which need to live after the function that created them returned without copying the data over into the calling code or for variables with size which will only be known at runtime.

This also comes at a cost, though. It is much slower to create a variable on the heap, because your program must first call your memory allocator to ask it for a chunk of memory big enough for the value you want to store. The allocator then must use CPU cycles at runtime to search the heap for a chunk of memory big enough to give you.

Access of heap variables are also slower, because it must be done via a pointer. A pointer is a variable holding the address of another memory location. Your CPU must read the pointer first to see where to go look for the actual value it wants to read.

Also, since the heap is much bigger and your allocator might give you a chunk from any piece of it, the memory you're looking for is most probably not in cache and you have to wait for it to be loaded from main memory.

Because Rust is a systems language, Rust leaves this choice in the hands of the programmer to choose whether the stack or heap is more appropriate. Many other languages simply always allocate arrays on the heap and automatically copy all the values to a bigger block if you want to grow it and your current chunk of heap isn't big enough.

This is often not acceptable for systems programming, so Rust gives you the choice. You can have arrays, which are on the stack and have a fixed size, or you can have Vec<T>, which is a standard library type which holds a pointer to a growable array on the heap. (C and C++, both also systems languages, also make the distinction between the stack and the heap very clear, for the same reasons.)

PS Systems programming was scary before Rust. With Rust, you get a wall of compiler errors and take a day to fix them and then your app doesn't crash randomly for the next 3 months while in use before you figure out the reason. With Rust, you can try systems programming and get the speed and low-level control without being afraid of falling into your knife or shooting yourself in the foot.

1 Like

I would make a few changes to your code. First of all use pattern matching instead of ifs:

Ok(num @ 1..=2) => num,
_ => {
    println!("Incorrect choice, try again");
    continue;
}

Also I would move the break outside of the if-else to avoid repeating the break.

Use eprintln instead of println for printing errors.

1 Like

I really like your answer, @L0uisc, but for someone with a bit more experience than @Arp. Fortran is my mother tongue, so heap/stack were something I came across only years after I started writing programs. I still do most of my programming in Rust without thinking about the distinction.

The most important thing a new Rust programmer needs is a basic understanding of ownership and borrowing; you can't get anywhere in Rust without it. The example that lifted the veil for me was

fn main() {
    let foo = vec![1, 2, 3];
    let foo = print_len(foo);
    println!("{:?}", foo);
}
fn print_len(v: Vec<i32>) -> Vec<i32> {
    println!("{}", v.len());
    v
}

You give ownership to print_len() and then get it back. That looks pretty inconvenient so you let print_len() borrow the vector.

fn main() {
    let foo = vec![1, 2, 3];
    print_len(&foo);
    println!("{:?}", foo);
}
fn print_len(v: &Vec<i32>)  {
    println!("{}", v.len()); 
}

After I grokked that example I could have conversations with the borrow checker instead of feeling that it was just yelling at me. It also helped me to think of "&" as "borrow" not "pointer."

2 Likes

Agreed. Ownership and borrowing is fundamental to Rust. I didn't mean to make stack vs heap a bigger deal than it is. It is entirely possible to understand and write Rust without understanding it or even knowing about its existence. However, I can't think of a reason why arrays are fixed length in Rust other than the heap/stack distinction. My reply tried to specifically answer that perceived "weirdness" of the language.

Please note there is a REPL for Rust, quite pleasant to use: https://lib.rs/crates/evcxr_repl

3 Likes

I very rarely use array at all, except for array constants. The type that is much more common is Vec, which is a variable length array, and also HashMap.

4 Likes

As a deep BEAM enthusiast, with a some professional Erlang/Elixir experience under my belt, I'll try to cover you're general questions as much as I can -- though I've only been writing Rust for about a year, and only in a hobbyist capacity.

Not really, but it's deeply influenced by functional paradigms. It reminds me of Elixir/Erlang, in that Rust uses functional approaches as a focused tool, rather that the functional-for-the-sake-of-mathematical-purity you see in languages like Haskell.

In fact, in my opinion, Rust's killer feature is a system (ownership) which contains and makes explicit the mutability variables (rather than the functional approach of "just declare a new variable") which allows performance critical operations to be implemented in a "provably" safe way. This is not at all what I think about when I think of functional languages, but it is really cool.

That said, the type system is of the same caliber as OCaml/Haskell/Scala, so in that sense, it's very close to those functional languages. Ironically, the BEAM languages don't have these "functional" features (Dialyzer is cool but it's not type-level programming), so that might not be what you mean.

I am not aware of anything, but one thing that helped me when I moved from REPL based development to something that had to be compiled first was to write test cases and run them from my IDE in place of the poking and prodding I'd normally do from the shell. It's not a perfect solution, but you tend to build a nice little packet of tests as you go.

As mentioned, Rust's type system is world-class. I've read it's Turing complete. It's somewhat of a double edge sword, but the outward edge is sharper. In some respects, much more time is taken upfront to properly specify the data flowing through your program, but saves time later when large amounts of behavior can be generated by knowing the data specification. More over, many classes of bugs can avoided entirely, but you never notice the time you didn't waste on bugs (or at least, I don't).

I imagine learning Rust as your first serious type system might be difficult. I learned Haskell before Rust, and many of the concepts translate over nicely (also, the Haskell community is obsessed with type theory and will quickly/enthusiastically educate you on it). Stick with it, maybe brush up on some type theory? Phillip Wadler talks on youtube is where I started with it.

Once you get the hang of type-level programming, it feels so painful to leave it.

My advice is build something stupid. I've learned the most that way. I always write a command-line Capitols of Canada Quiz (since there's like 12 of 'em). You learn data structures, I/O, etc etc. But the first thing I recommend is completing the Book before setting out on too many projects. It sets you up for success and there is little to no filler.

Check out this article.

3 Likes

Thanks for the awesome explanation of how heap and stack work.
I understand that the pain of having to specify types pays off later by not having to face many bugs at runtime which happen due to types.

In general, I see that most languages are trying to incorporate some form of type checking like typescript for javascript, etc. I am sure that the benefits of specifying types outweigh the initial extra work of having to specify types.

Nice, this is great, pattern matching is something I love, I am glad rust also supports it.
Yes, using pattern matching reduces code and makes it cleaner than using ifs.

Yup break could be outside the if blocks also eprintln! is better, correct.

Thanks for reviewing the code.

The next chapter in the book talks about ownership and borrowing, looking forward to it.
Will review your example once I finish up the chapter.

Thanks!

Awesome, to find someone from the elixir community, thanks for the links the article looks interesting and I will defenitly go through it.

One thought, although I have not yet studied about concurrency and parallel programming in rust, but comming from elixir where spawning thousands of processes is so cheap, easy and powerfull, how does rust compare on this aspect?
Parallel programming, Message passing vs shared state, multiple processes, is it safe (like in elixir you let it crash and the genserver will restart it, awesome uptime).

I have no idea about elixir and what kind of threads it actually creates for those "thousands of processes". I guess they are not actual operating system "processes" or "threads" but rather some kind of light weigh "green threads"

In Rust this can be done with asynchronous code using "async" and "await".

Looking into async gets you into horribly technical discussions about "futures", "executors" and so on but at the end of the day you can use a library like tokio that handles most of that and you just write normal looking code. For example have a look at the 'tokio' TCP server example here: https://github.com/tokio-rs/tokio

The debate about type checking in programming languages has been going on for decades, strongly type vs weakly typed vs dynamic typing etc.

I have come down heavily on both sides:

I love Javascript because it is so extremely dynamically typed. I can mutate anything into anything else whenever I please. This great for a quick experimental hack of some idea I'm kicking around.

I love Rust because it is so strictly typed. It will not let me set traps for myself that will cause unexpected crashes when the code runs up an odd path in the middle of the night.

You will get frustrated by the compiler trying to get even the simplest things to compile. But generally when it compiles it works. Logical errors are mostly found quickly, especially if you are using Rust's built in tests features. Saves a lot of frustration and expense debugging code that has gone into production.

A REPL is nice in languages like JS. I don't think it is needed in Rust.Typically most of my dev cycle is not building to code, I just use 'cargo check' which does a quick check of syntax and types and so on. So it goes "edit-check-edit-check-....-build." Of course with an IDE like VS Code and using it's rust-analyser plugin that checking is all happening as you edit your code.

Also, unlike a lot of other compiled languages Rust's compiler emits very useful error messages often with suggestions as to how to fix problems.

2 Likes

Rust is a very different, much lower level approach to parallelism / concurrency. As @ZiCog mentioned, Tokio (the de facto concurrency lib in Rust) has the most similarities to BEAM-style message passing and other high level concurrency / multi-threaded strategies. It includes channels that are somewhat like mailboxes in BEAM, but terminology in the world of concurrency is terribly muddled (this is one of my favorite papers for this reason).

Currently in Rust, without libraries, concurrency is mostly limited to true parallelism and only as far as the Host OS can provide -- meaning real OS threads. I can't speak with authority about this too deeply -- it's an area I'm actively trying to understand better in terms of Rust and Operating Systems -- but I'll describe what I can (and if I get something wrong, maybe someone can point it out).

The Rust Standard Library provides a OS Threads API (std::thread is how I've interacted with it) and in that way, you can spawn new threads similarly to how BEAM processes work (there's some interesting interactions with ownership and mutability here that don't come up in Erlang/Elixir). But what BEAM has that is missing from this equation, is a capable scheduler. If you spawn more threads than the OS has cores (or virtual cores, or however that works), there is significant slow down.

As I understand it (and again, I may not be right), if several threads are stuck in blocking operations on results from another thread which is working excessively, the OS has no understanding that this is the case, and might halt the active thread from working, and allow the blocked threads CPU time to just sit there, which while eventually resolvable, is a recipe for bottlenecks -- and certainly a performance concern.

To achieve the ability to spawn thousands/millions of concurrent processes requires a very clever scheduler. BEAM has several things going for it that allow it to schedule very intelligently, it can tell when a process is blocked (i.e. waiting on an empty mailbox) and simply never gives it CPU time if it never needs it, but it also is able to "multiplex" (that might not be the right term) your thousands/millions of process on far fewer physical cores, and allows significant interleaving of multiple processes on just one core.

Now you can achieve this in Rust by implementing certain traits, such as Future and Async but this requires you to write the scheduler / "mutliplex" / interleaving logic yourself -- which is what Tokio (and maybe others?) has done. It's slightly different than BEAM's scheduler as it seems to be a work-stealing scheme, but I find work-stealing to be an awesome approach from my experiences with Parallel Haskell.

BEAM is concurrent batteries included, and it's one-of-a-kind in that way. It also pays for it with garbage collection and VM overhead. Rust is much more a DIY kit here, but there are libraries that have put the pieces together already

2 Likes

Thanks for sharing some idea, I have a question which might be silly:

Say, we have a use case where we want to do a very CPU heavy task, where a language like rust will beat elixir by a large margin. Also, say we want to do multiple instances of this task parallelly.
Can we spawn multiple elixir processes were each such process will call a rust NIF to do the heavy lifting.

So we get benefits from both the languages, I am sure it's not as simple as it looks. The CPU power is limited in the end, no matter how many tricks we try with different languages.
What are your thoughts, was this a dumb question?

P.S, Learnt about ownership in rust today, I loved the concept, how rust handles garbage collection.
Another question on this, in many functional languages we prefer to write a logic recursively whenever possible rather than using a loop, what is rust's take on this? Are there any benefits to either approach?

Studying about ownership I wonder when a function calls itself (tail recursively so the last statement is the recursive call), with a variable which resides on the heap, it basically passes the ownership of this variable to itself, is this correct?

Rust doesn’t have any formal tail-call elimination, as far as I know. When you make a recursive call in tail position, it’ll create a new stack frame instead of reusing the old one. Thus, there’s a distinct stack frame for each of the two invocations of your function. Any arguments you pass by value will be moved into the inner stack frame, just like with any other function call.

The compiler does do some aggressive inlining early in its optimzation cycle, so many of these moves will be optimized away as part of that process.

1 Like

Rust doesn't have guaranteed TCE, so it's not canonical to write things that way because they tend to stack-overflow in debug. It'll probably happen eventually as an opt-in guarantee; it comes up periodically https://github.com/rust-lang/rfcs/pull/81 https://github.com/rust-lang/rfcs/pull/1888 (Of course, LLVM will often to tail call optimization in release mode when it's profitable.)

That said, here's a post I can reuse from a while back:

1 Like