I stumbled my way through some Rust yesterday and today to make a simple ASCII art API. It can draw lines, circles, and a canvas.
use std::collections::HashMap;
#[derive(PartialEq, PartialOrd, Debug)]
struct Point(u32, u32);
#[derive(PartialEq, PartialOrd, Debug)]
struct Dimension(u32, u32);
#[derive(PartialEq, PartialOrd, Debug)]
struct Rectangle(Point, Point);
#[derive(PartialEq, PartialOrd, Debug, Hash, Eq)]
struct Coordinate(u32, u32);
#[derive(Clone, Copy, Debug)]
enum Shape {
Canvas,
Circle,
HorizontalLine,
VerticalLine,
DiagonalLineLeftToRight,
DiagonalLineRightToLeft,
}
fn canvas_index_to_coords(i: u32, num: u32) -> Coordinate {
if i < num { Coordinate(i, 0) }
else { Coordinate(i % num, i / num) }
}
fn write(coords: &Coordinate, chr: char, num: u32) {
if coords.0 == num - 1 { println!("{}", chr); }
else { print!("{} ", chr); }
}
fn combine(a: HashMap<Coordinate, Shape>, b: HashMap<Coordinate, Shape>) -> HashMap<Coordinate, Shape> {
let mut combined = HashMap::new();
for (key, val) in a {
combined.insert(key, val);
}
for (key, val) in b {
combined.insert(key, val);
}
combined
}
fn canvas(size: Dimension) -> HashMap<Coordinate, Shape> {
let mut canvas_coords = HashMap::new();
for i in 0..(size.0 * size.1) {
canvas_coords.insert(canvas_index_to_coords(i, size.0), Shape::Canvas);
}
canvas_coords
}
fn circle(radius: u32, point: Point) -> HashMap<Coordinate, Shape> {
let x0 = point.0;
let y0 = point.1;
let mut x = radius;
let mut y = 0;
let mut err: i32 = 0;
let mut coords = HashMap::new();
while x >= y {
coords.insert(Coordinate(x0 + x, y0 + y), Shape::Circle);
coords.insert(Coordinate(x0 + y, y0 + x), Shape::Circle);
coords.insert(Coordinate(x0 - y, y0 + x), Shape::Circle);
coords.insert(Coordinate(x0 - x, y0 + y), Shape::Circle);
coords.insert(Coordinate(x0 - x, y0 - y), Shape::Circle);
coords.insert(Coordinate(x0 - y, y0 - x), Shape::Circle);
coords.insert(Coordinate(x0 + y, y0 - x), Shape::Circle);
coords.insert(Coordinate(x0 + x, y0 - y), Shape::Circle);
y += 1;
err += 1 + 2 * y as i32;
if 2 * (err - x as i32) + 1 > 0
{
x -= 1;
err += 1 - 2 * x as i32;
}
}
coords
}
fn line_shape(rectangle: Rectangle) -> Shape {
let x0 = (rectangle.0).0;
let y0 = (rectangle.0).1;
let x1 = (rectangle.1).0;
let y1 = (rectangle.1).1;
if x0 != x1 && y0 > y1 { Shape::DiagonalLineLeftToRight }
else if x0 != x1 && y0 < y1 { Shape::DiagonalLineRightToLeft }
else if y0 == y1 { Shape::HorizontalLine }
else { Shape::VerticalLine }
}
fn line(rectangle: Rectangle) -> HashMap<Coordinate, Shape> {
let x0 = (rectangle.0).0 as i32;
let y0 = (rectangle.0).1 as i32;
let x1 = (rectangle.1).0 as i32;
let y1 = (rectangle.1).1 as i32;
let dx = ((x1 - x0)).abs();
let sx: i32 = if x0 < x1 { 1 } else { -1 };
let dy = ((y1 - y0)).abs();
let sy: i32 = if y0 < y1 { 1 } else { -1 };
let tmp = if dx > dy { dx } else { -dy };
let mut err = tmp / 2;
let mut e2;
let mut x0_m = x0;
let mut y0_m = y0;
let mut coords = HashMap::new();
let line_shape = line_shape(rectangle);
loop {
coords.insert(Coordinate(x0_m as u32, y0_m as u32), line_shape);
if x0_m == x1 as i32 && y0_m == y1 as i32 {
break;
}
e2 = err;
if e2 > -dx {
err -= dy;
x0_m += sx;
}
if e2 < dy {
err += dx;
y0_m += sy;
}
}
coords
}
fn draw(num: u32, coords: HashMap<Coordinate, Shape>) {
let mut vec = Vec::new();
for (key, value) in &coords {
vec.push((key, value));
}
vec.sort_by_key(|&(coord, _)| coord.0);
vec.sort_by_key(|&(coord, _)| (coord.1 as i32) * -1);
for (coord, shape) in vec {
match shape {
&Shape::Canvas => write(coord, ' ', num),
&Shape::Circle => write(coord, 'o', num),
&Shape::HorizontalLine => write(coord, '-', num),
&Shape::VerticalLine => write(coord, '|', num),
&Shape::DiagonalLineLeftToRight => write(coord, '\\', num),
&Shape::DiagonalLineRightToLeft => write(coord, '/', num),
}
}
}
fn main() {
let num = 10;
let canvas_size = Dimension(num, num);
let point_1 = Point(2, 2);
let point_2 = Point(3, 4);
let point_3 = Point(7, 7);
let rectangle = Rectangle(Point(0, 0), Point(0, 9));
draw(num, combine(canvas(canvas_size), combine(circle(1, point_3), combine(circle(1, point_2), combine(circle(1, point_1), line(rectangle))))));
}
I'm sure there are some obvious errors here, like bounds checking when drawing the canvas, but I would be more interested in feedback of general functional style (never worked with a FP language) and also references/copy, which I'm not sure I got correctly. Also the line/circle algorithms are really hard to build nicely in FP I think.
2 Answers 2
canvas_index_to_coords
would sound better as a factory method onCoordinate
, i.e.Coordinate::from_canvas_index
.- In
combine
, you can turn theHashMap
s into iterators,chain
the two iterators together andcollect
that into a newHashMap
.collect
defers toFromIter::from_iter
;HashMap
implementsFromIter
and uses the iterator'ssize_hint
to reserve enough memory for the reported minimum number of items at once, whereas repeated calls toinsert
may need to reallocate a few times (which may mean copying theHashMap
's items every time). (Note: we don't need to callinto_iter
onb
, aschain
will do it for us. However, you could still do it if you like the visual symmetry; it works because iterators implementIntoIterator
.) - In
canvas
, you can usemap
on the range iterator to turn it into an iterator of key-value pairs, then collect that into aHashMap
. - In
circle
andline_shape
, you separately assign tuple struct fields to local variables. You can use a tuple struct pattern in alet
statement to destructure the tuple struct and assign all fields to local variables at once. (This wouldn't work inline
because of the casts.) The patterns could also be used in the parameter list, but I find that they're too long here. - In
line_shape
, you repeat thex0 != x1
condition. I would reorder the conditions to avoid that. - In
draw
, you can constructvec
by getting an iterator from theHashMap
and collecting it into aVec
. This works becauseHashMap
's iterators iterate on key-value tuples, which is exactly what you're putting in your vector! - In
draw
, you want to order byy
in descending order. However, the way you do it will panic when overflow checks are enabled of they
coordinate is equal tostd::i32::MIN
. A safe alternative is to just perform a bitwise not on the value (this is written!y
in Rust); we don't even need to cast toi32
! - In
draw
, you can combine the two calls tosort_by_key
into one: make the closure return a tuple. This works because tuples implementOrd
(for up to 12-tuples). So instead of sorting byx
, then by!y
, we can simply sort by(!y, x)
. - When
match
ing on a reference, it's typical to use the dereferencing operator in the match expression instead of repeating a reference pattern on all arms. - In
draw
, each arm repeats the call towrite
with only the character value differing between each arm. I'd make a method onShape
that maps a shape to a character, then use that method to determine the character to write.
use std::collections::HashMap;
#[derive(PartialEq, PartialOrd, Debug)]
struct Point(u32, u32);
#[derive(PartialEq, PartialOrd, Debug)]
struct Dimension(u32, u32);
#[derive(PartialEq, PartialOrd, Debug)]
struct Rectangle(Point, Point);
#[derive(PartialEq, PartialOrd, Debug, Hash, Eq)]
struct Coordinate(u32, u32);
#[derive(Clone, Copy, Debug)]
enum Shape {
Canvas,
Circle,
HorizontalLine,
VerticalLine,
DiagonalLineLeftToRight,
DiagonalLineRightToLeft,
}
impl Coordinate {
fn from_canvas_index(i: u32, num: u32) -> Coordinate {
if i < num {
Coordinate(i, 0)
} else {
Coordinate(i % num, i / num)
}
}
}
impl Shape {
fn to_char(&self) -> char {
match *self {
Shape::Canvas => ' ',
Shape::Circle => 'o',
Shape::HorizontalLine => '-',
Shape::VerticalLine => '|',
Shape::DiagonalLineLeftToRight => '\\',
Shape::DiagonalLineRightToLeft => '/',
}
}
}
fn write(coords: &Coordinate, chr: char, num: u32) {
if coords.0 == num - 1 {
println!("{}", chr);
} else {
print!("{} ", chr);
}
}
fn combine(a: HashMap<Coordinate, Shape>, b: HashMap<Coordinate, Shape>) -> HashMap<Coordinate, Shape> {
a.into_iter().chain(b).collect()
}
fn canvas(size: Dimension) -> HashMap<Coordinate, Shape> {
(0..(size.0 * size.1))
.map(|i| (Coordinate::from_canvas_index(i, size.0), Shape::Canvas))
.collect()
}
fn circle(radius: u32, point: Point) -> HashMap<Coordinate, Shape> {
let Point(x0, y0) = point;
let mut x = radius;
let mut y = 0;
let mut err: i32 = 0;
let mut coords = HashMap::new();
while x >= y {
coords.insert(Coordinate(x0 + x, y0 + y), Shape::Circle);
coords.insert(Coordinate(x0 + y, y0 + x), Shape::Circle);
coords.insert(Coordinate(x0 - y, y0 + x), Shape::Circle);
coords.insert(Coordinate(x0 - x, y0 + y), Shape::Circle);
coords.insert(Coordinate(x0 - x, y0 - y), Shape::Circle);
coords.insert(Coordinate(x0 - y, y0 - x), Shape::Circle);
coords.insert(Coordinate(x0 + y, y0 - x), Shape::Circle);
coords.insert(Coordinate(x0 + x, y0 - y), Shape::Circle);
y += 1;
err += 1 + 2 * y as i32;
if 2 * (err - x as i32) + 1 > 0 {
x -= 1;
err += 1 - 2 * x as i32;
}
}
coords
}
fn line_shape(rectangle: Rectangle) -> Shape {
let Rectangle(Point(x0, y0), Point(x1, y1)) = rectangle;
if y0 == y1 {
Shape::HorizontalLine
} else if x0 == x1 {
Shape::VerticalLine
} else if y0 > y1 {
Shape::DiagonalLineLeftToRight
} else {
Shape::DiagonalLineRightToLeft
}
}
fn line(rectangle: Rectangle) -> HashMap<Coordinate, Shape> {
let x0 = (rectangle.0).0 as i32;
let y0 = (rectangle.0).1 as i32;
let x1 = (rectangle.1).0 as i32;
let y1 = (rectangle.1).1 as i32;
let dx = ((x1 - x0)).abs();
let sx: i32 = if x0 < x1 { 1 } else { -1 };
let dy = ((y1 - y0)).abs();
let sy: i32 = if y0 < y1 { 1 } else { -1 };
let tmp = if dx > dy { dx } else { -dy };
let mut err = tmp / 2;
let mut e2;
let mut x0_m = x0;
let mut y0_m = y0;
let mut coords = HashMap::new();
let line_shape = line_shape(rectangle);
loop {
coords.insert(Coordinate(x0_m as u32, y0_m as u32), line_shape);
if x0_m == x1 as i32 && y0_m == y1 as i32 {
break;
}
e2 = err;
if e2 > -dx {
err -= dy;
x0_m += sx;
}
if e2 < dy {
err += dx;
y0_m += sy;
}
}
coords
}
fn draw(num: u32, coords: HashMap<Coordinate, Shape>) {
let mut vec: Vec<_> = coords.iter().collect();
vec.sort_by_key(|&(coord, _)| (!coord.1, coord.0));
for (coord, shape) in vec {
write(coord, shape.to_char(), num);
}
}
fn main() {
let num = 10;
let canvas_size = Dimension(num, num);
let point_1 = Point(2, 2);
let point_2 = Point(3, 4);
let point_3 = Point(7, 7);
let rectangle = Rectangle(Point(0, 0), Point(0, 9));
draw(num, combine(canvas(canvas_size), combine(circle(1, point_3), combine(circle(1, point_2), combine(circle(1, point_1), line(rectangle))))));
}
-
\$\begingroup\$ Thank you for the very detailed response with code samples, this is very helpful for me! Introducing type methods was indeed one of my next "evolution steps", but you gave me much more food for thought, thanks! – \$\endgroup\$BMBM– BMBM2016年10月29日 01:21:10 +00:00Commented Oct 29, 2016 at 1:21
-
\$\begingroup\$ Looks like your
{
got accidentally moved to the wrong line afterif 2 * (err - x as i32) + 1 > 0
. \$\endgroup\$Shepmaster– Shepmaster2016年10月30日 14:54:06 +00:00Commented Oct 30, 2016 at 14:54
I think Francis has given an excellent answer! I'm not fluent in Rust yet, which in part is why I have one simple suggestion: add comments to non-obvious code. There are several blocks that make no sense at a glance. Of course this has to do with me not having domain knowledge, but watch out! That will be you in a couple of months :)
Specifically, I'm thinking about the while loop in circle
, the meaning of err
etc.
-
\$\begingroup\$ Yes I totally agree with that, I admit, however, that I mostly copy/pasted the line and circle algorithms for the wikipedia page and just made it work in rust. Still no excuse for bad code, though. \$\endgroup\$BMBM– BMBM2016年10月29日 01:08:54 +00:00Commented Oct 29, 2016 at 1:08