6
\$\begingroup\$

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.

\$\endgroup\$

2 Answers 2

4
\$\begingroup\$
  1. canvas_index_to_coords would sound better as a factory method on Coordinate, i.e. Coordinate::from_canvas_index.
  2. In combine, you can turn the HashMaps into iterators, chain the two iterators together and collect that into a new HashMap. collect defers to FromIter::from_iter; HashMap implements FromIter and uses the iterator's size_hint to reserve enough memory for the reported minimum number of items at once, whereas repeated calls to insert may need to reallocate a few times (which may mean copying the HashMap's items every time). (Note: we don't need to call into_iter on b, as chain will do it for us. However, you could still do it if you like the visual symmetry; it works because iterators implement IntoIterator.)
  3. In canvas, you can use map on the range iterator to turn it into an iterator of key-value pairs, then collect that into a HashMap.
  4. In circle and line_shape, you separately assign tuple struct fields to local variables. You can use a tuple struct pattern in a let statement to destructure the tuple struct and assign all fields to local variables at once. (This wouldn't work in line because of the casts.) The patterns could also be used in the parameter list, but I find that they're too long here.
  5. In line_shape, you repeat the x0 != x1 condition. I would reorder the conditions to avoid that.
  6. In draw, you can construct vec by getting an iterator from the HashMap and collecting it into a Vec. This works because HashMap's iterators iterate on key-value tuples, which is exactly what you're putting in your vector!
  7. In draw, you want to order by y in descending order. However, the way you do it will panic when overflow checks are enabled of the y coordinate is equal to std::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 to i32!
  8. In draw, you can combine the two calls to sort_by_key into one: make the closure return a tuple. This works because tuples implement Ord (for up to 12-tuples). So instead of sorting by x, then by !y, we can simply sort by (!y, x).
  9. When matching on a reference, it's typical to use the dereferencing operator in the match expression instead of repeating a reference pattern on all arms.
  10. In draw, each arm repeats the call to write with only the character value differing between each arm. I'd make a method on Shape 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))))));
}
\$\endgroup\$
2
  • \$\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\$
    – Max
    Oct 29, 2016 at 1:21
  • \$\begingroup\$ Looks like your { got accidentally moved to the wrong line after if 2 * (err - x as i32) + 1 > 0. \$\endgroup\$
    – Shepmaster
    Oct 30, 2016 at 14:54
0
\$\begingroup\$

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.

\$\endgroup\$
1
  • \$\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\$
    – Max
    Oct 29, 2016 at 1:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.