The main benefit of using the current type alias is the simple syntax, however it creates a lot of confusion because it forces us to then explain things like arrays, indexing and the "orphan rule" (when talking about random values) just to use points. It also isn't particularly expressive. For example:
// We have to access components of the point like so:
let x = pt[0]; // requires explaining arrays and indexing to use
let y = pt[1];
// Instead of by name:
let x = pt.x;
let y = pt.y;
Implementing our own type gives us the freedom to implement any traits for it however we want. We can provide a better random implementation or even implement methods on it for different situations that come up. Overall, it's more forwards compatible to use our own type. We can implement different vector operations on it like multiplying/dividing by a constant (this is helpful in a number of situations), adding points, etc.
We can actually even keep the convenient syntax by changing the few functions that take Point to take Into<Point> instead. This is a small change that affects barely any code since points are used so infrequently.
Sample Implementation
The implementation below can be
accessed with either the pt.x and pt.y syntax or with the pt[0] or pt[1] syntax
converted from and to [f64; 2]
That means that if we change the functions that take Point to take Into<Point>, no syntax has to change (this is completely backwards compatible)
added/subtracted with other points
multiplied/divided by f64 values
use std::ops::{Add, Sub, Mul, Div, Index, IndexMut};
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
pub fn is_finite(&self) -> bool {
self.x.is_finite() && self.y.is_finite()
}
}
impl From<(f64, f64)> for Point {
fn from(pt: (f64, f64)) -> Self {
Point {x: pt.0, y: pt.1}
}
}
impl From<[f64; 2]> for Point {
fn from(pt: [f64; 2]) -> Self {
Point {x: pt[0], y: pt[1]}
}
}
impl From<Point> for [f64; 2] {
fn from(pt: Point) -> Self {
[pt.x, pt.y]
}
}
impl Add for Point {
type Output = Point;
fn add(self, other: Self) -> Self::Output {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
impl Sub for Point {
type Output = Point;
fn sub(self, other: Self) -> Self::Output {
Point {
x: self.x - other.x,
y: self.y - other.y,
}
}
}
impl Mul<f64> for Point {
type Output = Point;
fn mul(self, other: f64) -> Self::Output {
Point {
x: self.x * other,
y: self.y * other,
}
}
}
impl Div<f64> for Point {
type Output = Point;
fn div(self, other: f64) -> Self::Output {
Point {
x: self.x / other,
y: self.y / other,
}
}
}
impl Index<usize> for Point {
type Output = f64;
fn index(&self, index: usize) -> &Self::Output {
match index {
0 => &self.x,
1 => &self.y,
_ => panic!("Invalid coordinate for Point: {}", index),
}
}
}
impl IndexMut<usize> for Point {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
match index {
0 => &mut self.x,
1 => &mut self.y,
_ => panic!("Invalid coordinate for Point: {}", index),
}
}
}
[x] Implement the traits necessary for Point to be usable with random_range
generates a random value within the rectangle formed by the two given points (document this in the rand crate documentation and in the random_range function)
[x] Replace validation in Drawing::set_center with Point::is_finite
[x] Update any documentation (including rand documentation) that references Point
[x] Document all the different usages of Point (indexing, via fields, different construction methods, etc.) -- see color module docs
The main benefit of using the current type alias is the simple syntax, however it creates a lot of confusion because it forces us to then explain things like arrays, indexing and the "orphan rule" (when talking about random values) just to use points. It also isn't particularly expressive. For example:
Implementing our own type gives us the freedom to implement any traits for it however we want. We can provide a better random implementation or even implement methods on it for different situations that come up. Overall, it's more forwards compatible to use our own type. We can implement different vector operations on it like multiplying/dividing by a constant (this is helpful in a number of situations), adding points, etc.
We can actually even keep the convenient syntax by changing the few functions that take
Point
to takeInto<Point>
instead. This is a small change that affects barely any code since points are used so infrequently.Sample Implementation
The implementation below can be
pt.x
andpt.y
syntax or with thept[0]
orpt[1]
syntax[f64; 2]
Point
to takeInto<Point>
, no syntax has to change (this is completely backwards compatible)f64
valuesPoint
to be usable withrandom_range
Drawing::set_center
withPoint::is_finite
Point
Point
(indexing, via fields, different construction methods, etc.) -- seecolor
module docs