Handling enum whose variants are often known a priori

#1

I’m working on a music notation program. A basic unit of organization in a document is a staff, which can have a sequence of objects of various kinds on it. My first instinct was to do this:

enum Object
{
    Clef,
    Duration,
    GraceNote,
    KeySignature,
    TimeSignature
}

struct Staff
{
    objects: Vec<Object>
}

However, it’s often necessary to iterate through the Durations only, while keeping track of where they are relative to the other Objects, so I switched to

struct Duration
{}

enum Object
{
    Clef,
    GraceNote,
    KeySignature,
    TimeSignature
}

struct Staff
{
    object_ranges: Vec<Vec<Object>>,
    durations: Vec<Duration>
}

The first duration can have other objects before it, and the last duration can have other objects after it, so the nth duration is between the nth and n+1st object_range and there is always one more object_range than there are durations. Before, I could specify any object on the staff (for the purposes of specifying the entry cursor position, or keeping a list of the objects that are currently selected, for example) with an index into the objects Vec, but now I have to introduce the concept of an address:

enum Address
{
    Duration
    {
        duration_index: usize
    },
    Object
    {
        range_index: usize,
        object_index: usize
    }
}

Moreover, while clefs can appear anywhere on a staff, a clef at the beginning of a staff is treated specially (for example, drawn at a different size and subjected to a different spacing algorithm than other clefs) so I wanted to add a slot specifically for one:

struct Staff
{
    header_clef: Option<Clef>,
    object_ranges: Vec<Vec<Object>>,
    durations: Vec<Duration>
}

Of course, this means Clef needs to be made into its own type:

struct Clef
{}

enum Object
{
    Clef(Clef),
    GraceNote,
    KeySignature,
    TimeSignature
}

It also means that, unless I want to make it some kind of special case of one of the two existing Address variants, like making the index stored by AddressDuration an isize so it can store a -1, there also needs to be an Address::Clef variant.

Now for the real difficulties. While durations and header clefs are treated specially for some things, such that it’s convenient to have them singled out, there are plenty of situations where they are treated the same as other objects. In particular, it’s common to iterate through a Vec<Address>, resolving each address to the object it identifies in order to perform some operation, like setting the is_selected fields of any currently selected objects to false when the user cancels a selection. This means that in many places I duplicate the procedure of matching against an address and resolving it to an object, only to do the same operation to the object in each branch.

It would be handy to have a function that takes an address and returns “a reference to the object,” but what that should mean isn’t clear. It couldn’t return a bare Clef or Duration, since those don’t qualify as the same type as other objects. If I added Object::Duration(Duration), then Object would once again have a variant for every addressable thing, but what happens to Durations and Clefs that weren’t already wrapped in their respective Object variants? I don’t see how the function could turn a reference to a Clef into a reference to an Object that contains a Clef by value.

Is there a way to make an address-resolution function work after all? Should I give up on having Clef and Duration be their own types and use Object everywhere instead so that every access of a duration or header clef involves an irrefutable if-let? Or some bigger redesign?

#2

Having individual types that are then wrapped in an enumeration for storage or polymorphic use is a good pattern. Take a look at https://doc.rust-lang.org/std/net/enum.IpAddr.html, for example, which also does this.

1 Like
#3

It’s good to know that people have success with that approach. Does the problem I’m having with it make sense? For any function foo() implemented for all Objects, I currently do something like

fn foo(staff: &mut Staff, address: &Address)
{
    match address
    {
        Address::Duration{duration_index} => staff.durations[*duration_index].foo(),
        Address::HeaderClef => staff.header_clef.foo(),
        Address::Object{range_index, object_index} => staff.object_ranges[*object_range_index][*object_index].foo()
    }
}

The actual match is clunkier than that even, because DurationAddress is also its own type and each address also has a staff_index because there are multiple staves. What I would like is to have something like

fn resolve_address(staff: &mut Staff, address: &Address) -> &mut Object;

so I could just do

resolve_address(staff, address).foo();

rather than having to paste in the whole match every time.

#4

Ooh, I think I know what to do. I could define a trait that Object and all the types it wraps implement, and have resolve_address return a trait object.

1 Like