ispilledthejava
ispilledthejava

Reputation: 193

Instantiating a struct with stdin data in Rust

I am very, very new to Rust and trying to implement some simple things to get the feel for the language. Right now, I'm stumbling over the best way to implement a class-like struct that involves casting a string to an int. I'm using a global-namespaced function and it feels wrong to my Ruby-addled brain.

What's the Rustic way of doing this?

use std::io;

struct Person {
  name: ~str,
  age: int
}

impl Person {  
  fn new(input_name: ~str) -> Person {
    Person { 
      name: input_name, 
      age: get_int_from_input(~"Please enter a number for age.")
    }
  }

  fn print_info(&self) {
    println(fmt!("%s is %i years old.", self.name, self.age));
  }

}

fn get_int_from_input(prompt_message: ~str) -> int {
  println(prompt_message);
  let my_input = io::stdin().read_line();
  let my_val =
    match from_str::<int>(my_input) {
      Some(number_string)   => number_string,
      _                     => fail!("got to put in a number.")
  };

  return my_val;
}


fn main() {
  let first_person = Person::new(~"Ohai");
  first_person.print_info();
}

This compiles and has the desired behaviour, but I am at a loss for what to do here--it's obvious I don't understand the best practices or how to implement them.

Edit: this is 0.8

Upvotes: 3

Views: 1758

Answers (3)

A.B.
A.B.

Reputation: 16630

A variation of telotortium's input reading function that doesn't fail on bad input. The loop { ... } keyword is preferred over writing while true { ... }. In this case using return is fine since the function is returning early.

fn int_from_input(prompt: &str) -> int {
    println(prompt);
    loop {
        match from_str::<int>(io::stdin().read_line()) {
            Some(x) => return x,
            None    => println("Oops, that was invalid input. Try again.")
        };
    }
}

Upvotes: 0

user1024732
user1024732

Reputation:

This isn't really rust-specifc, but try to split functionality into discrete units. Don't mix the low-level tasks of putting strings on the terminal and getting strings from the terminal with the more directly relevant (and largely implementation dependent) tasks of requesting a value, and verify it. When you do that, the design decisions you should make start to arise on their own.

For instance, you could write something like this (I haven't compiled it, and I'm new to rust myself, so they're probably at LEAST one thing wrong with this :) ).

fn validated_input_prompt<T>(prompt: ~str) {
    println(prompt);
    let res = io::stdin().read_line();

    loop {
        match res.len() {
            s if s == 0 => { continue; }

            s if s > 0 {
                match T::from_str(res) {
                    Some(t) -> {
                        return t
                    },

                    None -> {
                        println("ERROR.  Please try again.");
                        println(prompt);
                    }
                }
            }
        }
    }

}

And then use it as:

validated_input_prompt<int>("Enter a number:")

or:

validated_input_prompt<char>("Enter a Character:")

BUT, to make the latter work, you'd need to implement FromStr for chars, because (sadly) rust doesn't seem to do it by default. Something LIKE this, but again, I'm not really sure of the rust syntax for this.

 use std::from_str::*;

 impl FromStr for char {

     fn from_str(s: &str) -> Option<Self> {
         match len(s) {
             x if x >= 1 => {
                 Option<char>.None
             },
             x if x == 0 => {
                 None,
             },
         }

         return s[0];
     }

 }

Upvotes: 0

telotortium
telotortium

Reputation: 3541

Here is my version of the code, which I have made more idiomatic:

use std::io;

struct Person {
  name: ~str,
  age: int
}

impl Person {
  fn print_info(&self) {
    println!("{} is {} years old.", self.name, self.age);
  }

}

fn get_int_from_input(prompt_message: &str) -> int {
  println(prompt_message);
  let my_input = io::stdin().read_line();
  from_str::<int>(my_input).expect("got to put in a number.")
}


fn main() {
  let first_person = Person {
    name: ~"Ohai",
    age: get_int_from_input("Please enter a number for age.")
  };
  first_person.print_info();
}

fmt!/format!

First, Rust is deprecating the fmt! macro, with printf-based syntax, in favor of format!, which uses syntax similar to Python format strings. The new version, Rust 0.9, will complain about the use of fmt!. Therefore, you should replace fmt!("%s is %i years old.", self.name, self.age) with format!("{} is {} years old.", self.name, self.age). However, we have a convenience macro println!(...) that means exactly the same thing as println(format!(...)), so the most idiomatic way to write your code in Rust would be

println!("{} is {} years old.", self.name, self.age);

Initializing structs

For a simple type like Person, it is idiomatic in Rust to create instances of the type by using the struct literal syntax:

let first_person = Person {
    name: ~"Ohai",
    age:  get_int_from_input("Please enter a number for age.")
};

In cases where you do want a constructor, Person::new is the idiomatic name for a 'default' constructor (by which I mean the most commonly used constructor) for a type Person. However, it would seem strange for the default constructor to require initialization from user input. Usually, I think you would have a person module, for example (with person::Person exported by the module). In this case, I think it would be most idiomatic to use a module-level function fn person::prompt_for_age(name: ~str) -> person::Person. Alternatively, you could use a static method on Person -- Person::prompt_for_age(name: ~str).

&str vs. ~str in function parameters

I've changed the signature of get_int_from_input to take a &str instead of ~str. ~str denotes a string allocated on the exchange heap -- in other words, the heap that malloc/free in C, or new/delete in C++ operate on. Unlike in C/C++, however, Rust enforces the requirement that values on the exchange heap can only be owned by one variable at a time. Therefore, taking a ~str as a function parameter means that the caller of the function can't reuse the ~str argument that it passed in -- it would have to make a copy of the ~str using the .clone method.

On the other hand, &str is a slice into the string, which is just a reference to a range of characters in the string, so it doesn't require a new copy of the string to be allocated when a function with a &str parameter is called.

The reason to use &str rather than ~str for prompt_message in get_int_from_input is that the function doesn't need to hold onto the message past the end of the function. It only uses the prompt message in order to print it (and println takes a &str, not a ~str). Once you change the function to take &str, you can call it like get_int_from_input("Prompt") instead of get_int_from_input(~"Prompt"), which avoids the unnecessary allocation of "Prompt" on the heap (and similarly, you can avoid having to clone s in the code below):

let s: ~str = ~"Prompt";
let i = get_int_from_input(s.clone());
println(s);    // Would complain that `s` is no longer valid without cloning it above
               // if `get_int_from_input` takes `~str`, but not if it takes `&str`.

Option<T>::expect

The Option<T>::expect method is the idiomatic shortcut for the match statement you have, where you want to either return x if you get Some(x) or fail with a message if you get None.

Returning without return

In Rust, it is idiomatic (following the example of functional languages like Haskell and OCaml) to return a value without explicitly writing a return statement. In fact, the return value of a function is the result of the last expression in the function, unless the expression is followed by a semicolon (in which case it returns (), a.k.a. unit, which is essentially an empty placeholder value -- () is also what is returned by functions without an explicit return type, such as main or print_info).

Conclusion

I'm not a great expert on Rust by any means. If you want help on anything related to Rust, you can try, in addition to Stack Overflow, the #rust IRC channel on irc.mozilla.org or the Rust subreddit.

Upvotes: 8

Related Questions