One of the exercises I like to program when learning a new language is a chat client for the console. I've programmed it in many languages (Perl, Python, C, C++, JavaScript, ...). One of the versions I like most for its terseness is Ruby:
#!/usr/bin/ruby
require 'socket'
s = TCPSocket.new(ARGV[0], ARGV[1].to_i)
nick = ARGV[2]
# send nick to server
s.puts(nick)
# input thread
kbd = Thread.new {
STDIN.each_line { |line|
s.puts("033円[31m" + nick + "> " + "033円[0m" + line)
}
s.close_write
}
# output thread
con = Thread.new {
s.each_line { |line|
STDOUT.puts(line)
}
}
kbd.join
con.join
The initial Rust equivalent would be something similar to:
use std::os;
use std::io;
use std::io::{TcpStream, BufferedReader};
fn main() {
let args = os::args();
let mut s = TcpStream::connect(args[1], from_str(args[2]));
// send nick to server
nick = args[3];
s.write_line(nick);
// input thread
spawn(proc() {
for line in io::stdin().lines() {
s.write_str(format!("\x1b[31m{}> \x1b[0m{}", nick, line));
}
s.close_write();
});
// output thread
spawn(proc() {
for line in BufferedReader::new(s).lines() {
io::stdout().write_str(line);
}
});
}
which obviously has some compile errors. The corrected version is:
use std::os;
use std::io;
use std::io::{TcpStream, BufferedReader};
fn main() {
let args = os::args();
let mut s = TcpStream::connect(args[1].as_slice(),from_str(args[2].as_slice()).unwrap()).unwrap();
// send nick to server
s.write_line(args[3].as_slice()).unwrap();
let nick = args[3].clone();
let mut sin = s.clone();
// input
spawn(proc() {
for line in io::stdin().lines() {
sin.write_str(format!("\x1b[31m{}> \x1b[0m{}", nick, line.unwrap().as_slice()).as_slice()).unwrap();
}
sin.close_write().unwrap();
});
// output
spawn(proc() {
for line in BufferedReader::new(s).lines() {
io::stdout().write_str(line.unwrap().as_slice()).unwrap();
}
});
}
That's too verbose, isn't it? Perhaps it's due to the fact that the compiler is still beta (0.12-nightly)? Can this terseness be simplified?
1 Answer 1
The verbosity in your code actually come from a number of different sources.
Error handling
There is a lot of calls to .unwrap
in your code.
This is because rust is a low-level language and thus you need to be able to predictably be able to handle all errors your code might run into. Traditionally this have been done using exceptions (as one might in c++) or error values (as in c), but since these often come with their own set of problems, rust have chosen to do it differently. This has the disadvantage of having slightly more verbosity (in some cases), but more often giving rise to the desired solution without bugs.
Your way of handling it by calling .unwrap
on every Option
or Result
you encounter. This is not a particularly smart solution, as it will simply cause the thread to fail on error, but if that is your desired outcome, then I guess it is okay.
The typically used ways of handling these errors are as far as I know either to use the try!
macro or pattern matching.
str/String
Again, since rust is a low-level language without forced garbage collection, it needs to keep track of where its values are actually placed and how they are freed. In this case, this is apparent because of your need to do .as_slice
in a lot of places.
This is because rust has at least two different types of strings. The first is String
, which is a wrapper around a growable heap-vector of bytes. If you have such a value, you own it and is responsible for freeing it.
The second is a &str
, which is just a pointer into a string. A function cannot free or alter this string. This is the type of string that most libraries take as arguments (to prevent cloning values all the time).
The extra verbosity comes from the fact that you need to convert the first kind into the second kind. It might be an idea to do this coercion automatically, but I have not heard about this being in the pipeline.
Extra cloning
Finally you needed to add a few calls to .clone
. In this case, you send the values to a thread, which would make it unavailable in the original thread.
This is because you cannot easily access the same value from multiple places at the same time as you can in ruby. This is again because rust is a low-level language without forced garbage collection with the need to control error handling and memory allocations.
There are multiple ways of handling this problem, but the easiest (though not necessarily the best) is to simply call .clone
before sending the value to a different thread.
Conclusion
While it is possible to make improvements, most of the verbosity actually comes from rusts increased need for control, and this is not going away in the future.
sin.write_str(format!("\x1b[31m{}> \x1b[0m{}", nick, line.unwrap().as_slice()).as_slice()).unwrap();
would be better written(write!(sin, "\x1b[31m{}> \x1b[0m{}", nick, line.unwrap())).unwrap()
. \$\endgroup\$(write!(io::stdout(), "{}", line.unwrap())).unwrap();
as well \$\endgroup\$