Dispatching on a new composite type that extends an old composite type

I have a situation where I want to check if two 2D vectors are neighbours or not according to some criteria. The initial criteria I considered was to check whether both vectors are within some fixed distance radius of one another. I did this as follows:

using LinearAlgebra
abstract type Mask end

struct RadialMask<:Mask

neighbours(mask::RadialMask, x, y) = norm(x-y) < mask.radius

I now want to consider a new criteria that still performs this distance check, but also checks that the vectors do not face in “opposite” directions. More specifically I want to check whether the vector y forms an angle between π - θ and π + θ with the vector x, where theta is allowed to vary. To do this, I implemented a new composite type to dispatch neighbours on.

struct AngleMask<:Mask

function neighbours(mask::AngleMask, x, y)
    radius_check = norm(x-y) < mask.radius
    angle = acos(dot(x, y) / (abs(x)*abs(y)))
    angle_check = angle >= π + mask.theta ||  theta <= π - mask.theta
    return radius_check && angle_check

The problem I have with this approach is code duplication. More specifically, radius_check is a reimplementation of neighbours for RadialMask. In an OOP language one gets around this by making AngleMask a subclass of RadialMask. This would also remove the need to declare a radius variable in the definition of AngleMask. Is there a neat design pattern to avoid this kind of code duplication in Julia? More generally, one can consider the following scenario.

struct Foo 
    # foo variables

struct Bar
    # foo variables
    # bar variables

function f(element::Foo) = # do some stuff based on foo variables

function f(element::Bar)
    # do some stuff based on foo variables
    # do some stuff based on bar variables

I don’t know if this fully addresses your problem, but one way to reduce code duplication is to break your functions into smaller functions and compose them.

check_radius(mask::Mask, x, y) = norm(x-y) < mask.radius

function check_angle(mask::Mask, x, y)
    angle = acos(dot(x, y) / (abs(x)*abs(y)))
    return angle >= π + mask.theta ||  theta <= π - mask.theta

neighbours(mask::RadialMask, x, y) = check_radius(mask, x, y)

neighbours(mask::AngleMask, x, y) = check_radius(mask, x, y) && check_angle(mask, x, y)

Decoupling is good, but that’ll hit a ERROR: MethodError: no method matching check_radius(::AngleMask, ...

Julia has supertypes, you just can’t instantiate them. You can implement methods for the supertype Mask, or if there isn’t a neat supertype, a Holy trait that both AngleMask and RadialMask have. Taking Christopher_Fisher’s example assuming the original type definitions, all you would need for reuse is this edit: check_radius(mask::Mask, x, y) = norm(x-y) < mask.radius. Personally, I would decouple check_radius even further from Mask because it really only needs to check a floating point value:

check_radius(radius, x, y) = norm(x-y) < radius

neighbours(mask::RadialMask, x, y) = check_radius(mask.radius, x, y)

The problem with inheriting fields along with methods is that you don’t necessarily want the fields or treat them the same way, so it’s easy to end up with memory bloat or silent bugs when some method isn’t overridden properly. Instead of doing that by default, you opt into fields reuse with composition. Here you only reuse a radius field, so it’s probably overkill to do that. If you ever choose to represent radius differently (can’t imagine how) from a single field in a type, you could make a method radius to make the access call the same across all types. This would also be useful with composition, like:

radius(m::Mask) = m.radius # general fallback

struct NestedMask<:Mask # no reason to do this, just illustrative

radius(nm::NestedMask) = radius(nm.radialnask)
1 Like

This was also the first thought I had regarding a solution. The main benefit here is that a single change to check_radius ensures that both masks are updated. There seems to be no way to tell Julia that the operations on each of the structs as defined should be related without having them rely jointly on some auxiliary function like this (to the best of my knowledge at least).

Thanks for the thorough response. Decoupling was also the first thing that came to mind for me when thinking about this. I also thought about the nesting, but it seems a messy way of composing a sequence of masks in cases when you may want to combine more than two structs. In that case I guess it would be better to define a binary operation that composes masks.