That’s a good start, I would simplify a few things and to make it more idiomatic and efficient, add some type parameters. First, the type definitions:
struct Point{R<:Real}
x :: R
y :: R
end
abstract type ConvexShape{R} end
struct Circle{R<:Real} <: ConvexShape{R}
center :: Point{R}
radius :: R
end
struct Rectangle{R<:Real} <: ConvexShape{R}
left :: R
right :: R
bottom :: R
top :: R
end
Rectangle(p::Point, q::Point) =
Rectangle(minmax(p.x, q.x)..., minmax(p.y, q.y)...)
# a matrix of the corners of a rectangle
corners(r::Rectangle) = [Point(x, y) for x in (r.left, r.right), y in (r.bottom, r.top)]
Now some geometric computational methods:
dist(p::Point, q::Point) = sqrt((p.x - q.x)^2 + (p.y - q.y)^2)
inside(p::Point, q::Point) = p == q
inside(p::Point, c::Circle) = dist(p, c.center) ≤ c.radius
inside(p::Point, r::Rectangle) =
r.left ≤ p.x ≤ r.right && r.bottom ≤ p.y ≤ r.top
inside(c::Circle, c′::Circle) =
c.radius + dist(c.center, c′.center) ≤ c′.radius
inside(c::Circle, r::Rectangle) =
r.left ≤ c.center.x - c.radius && c.center.x + c.radius ≤ r.right &&
r.bottom ≤ c.center.y - c.radius && c.center.y + c.radius ≤ r.top
inside(r::Rectangle, c::ConvexShape) =
all(inside(p, c) for p in corners(r))
const ⊆ = inside
A few points about why I wrote things this way:
- Type parameters allow structs to be much more efficient and always have matching types for fields
- These allow you to use whatever real number type you want for the dimensions, e.g.
Float64
, Int
or something slightly exotic like Rational{BigInt}
- No need to define constructors most of the time—the defaults just work
- Also no need to provide show methods often, the default is pretty good
- Since you can put types on arguments, it’s often clearer to use short argument names for math formulas—you can tell that
p
is a Point
and c
is a Circle
, etc. from the method signature
- It is conventional in Julia to express “subject verb object” relationships as
verb(subject, object)
; accordingly one should read inside(c::Circle, r::Rectangle)
as “c
is inside r
”; at the end I create ⊆
as an alias for inside
which allows write this using infix operator notation as c ⊆ r
which matches mathematical notation.
About the representation and construction of Rectangles:
- There are many ways to represent and construct this type
- I like
Rectangle(::Point, ::Point)
for construction because you don’t have to remember which corner is special or which which order the arguments are in—if you give any two points it constructs the rectangle between them
- Internal representation doesn’t really matter, but a list of left, right, bottom and top coordinates seems simple and allows easy construction from two points.
The only bit of type system cleverness here:
- I create an abstract type
ConvexShape
and make Circle
and Rectangle
implementations of it
- Checking if a rectangle is contained in any convex shape can be done by checking if all its corners are inside the convex shape, so we can define a single method
Note that the way these types are defined they work great if you use the same Real
type everywhere (e.g. all Int
or all Float64
), but they start breaking as soon as you try mixing integers and floats for example. If you want to allow mixing types, then you can define these additional mixed type constructors for convenience:
Point(x::Real, y::Real) =
Point(promote(x, y)...)
Rectangle(left::Real, right::Real, bottom::Real, top::Real) =
Rectangle(promote(left, right, bottom, top)...)
function Circle(center::Point{R}, radius::S) where {R<:Real, S<:Real}
T = promote_type(R, S)
Circle(Point{T}(center.x, center.y), convert(T, radius))
end
Here’a a fun example that ends up using a lot of the above functionality:
julia> o = Point(0, 0) # origin
Point{Int64}(0, 0)
julia> c₁ = Circle(o, 1)
Circle{Int64}(Point{Int64}(0, 0), 1)
julia> c₂ = Circle(o, 2)
Circle{Int64}(Point{Int64}(0, 0), 2)
julia> c₁ ⊆ c₂
true
julia> c₁ ⊆ c₁
true
julia> c₂ ⊆ c₁
false
julia> r = Rectangle(Point(-1, -1), Point(1, 1))
Rectangle{Int64}(-1, 1, -1, 1)
julia> c₁ ⊆ r ⊆ c₂
true