Thread overview
enum conversions
May 13, 2011
bearophile
May 13, 2011
Timon Gehr
May 13, 2011
Andrej Mitrovic
May 13, 2011
Andrej Mitrovic
May 13, 2011
Andrej Mitrovic
May 13, 2011
Some musings, feel free to ignore this post.

Sometimes I have to convert enums to integers or integers to enums. I'd like to do it efficiently (this means with minimal or no runtime overhead), and safely (this means I'd like the type system to prove I am not introducing bugs, like assigning enums that don't exist).


This function classifies every natural number in one of the three classes (deficient numbers, perfect numbers, and abundant nubers, according to the sum of its factors), so I use a 3-enum:

enum NumberClass : int { deficient=-1, perfect=0, abundant=1 }

NumberClass classifyNumber(int n)
    auto factors = filter!((i){ return n % i == 0; })(iota(1, n));
    int difference = reduce!q{a + b}(0, factors) - n;
    return cast(NumberClass)sgn(difference);
}


std.math.sgn() returns a value in {-1, 0, 1}, so this first version of the function uses just a cast, after carefully defining the same values for the NumberClass enums. But casts stop the type system, so it can't guaranteed the code is working correctly or safely, so if I change the values of the enums the type system doesn't catch the bug.

This version is safer, works with any value associated to the enum items, but it performs even two tests at run-time:

NumberClass classifyNumber(int n)
    auto factors = filter!((i){ return n % i == 0; })(iota(1, n));
    int diff = sgn(reduce!q{a + b}(0, factors) - n);
    if (diff == -1)
        return NumberClass.deficient;
    else if (diff == 0)
        return NumberClass.perfect;
    else
        return NumberClass.abundant;
}


This version is about as safe, and uses one array access on immutable array (I have not used an emum array to avoid wasting even more run time):

NumberClass classifyNumber(int n)
    static immutable res = [NumberClass.deficient, NumberClass.perfect, NumberClass.abundant];
    auto factors = filter!((i){ return n % i == 0; })(iota(1, n));
    int sign = sgn(reduce!q{a + b}(0, factors) - n);
    return res[sign + 1];
}


Using a switch is another safe option, I can't use a final switch. This too has some run-time overhead:

NumberClass classifyNumber(int n)
    auto factors = filter!((i){ return n % i == 0; })(iota(1, n));
    int sign = sgn(reduce!q{a + b}(0, factors) - n);
    switch (sign) {
        case -1: return NumberClass.deficient;
        case 0:  return NumberClass.perfect;
        default: return NumberClass.abundant;
    }
}


In theory a bit better type system (with ranged integers too as first-class types) knows that sgn() returns the same values as the enum NumberClass, this allows the first version without cast and compile-time proof of correctness:


NumberClass classifyNumber(int n)
    auto factors = filter!((i){ return n % i == 0; })(iota(1, n));
    int difference = reduce!q{a + b}(0, factors) - n;
    return sgn(difference);
}

I don't know what to think.

Bye,
bearophile
May 13, 2011
> Some musings, feel free to ignore this post.
>
> Sometimes I have to convert enums to integers or integers to enums. I'd like to
do it efficiently (this means with minimal or no runtime overhead), and safely
(this means I'd > like the type system to prove I am not introducing bugs, like
assigning enums that don't exist).
>
>
> This function classifies every natural number in one of the three classes
(deficient numbers, perfect numbers, and abundant nubers, according to the sum of its factors), so I use a 3-enum:
>
> enum NumberClass : int { deficient=-1, perfect=0, abundant=1 }
>
> NumberClass classifyNumber(int n)
>     auto factors = filter!((i){ return n % i == 0; })(iota(1, n));
>     int difference = reduce!q{a + b}(0, factors) - n;
>     return cast(NumberClass)sgn(difference);
> }
>
>
> std.math.sgn() returns a value in {-1, 0, 1}, so this first version of the
function uses just a cast, after carefully defining the same values for the NumberClass enums. But
> casts stop the type system, so it can't guaranteed the code is working correctly
or safely, so if I change the values of the enums the type system doesn't catch the bug.
>
> This version is safer, works with any value associated to the enum items, but it
performs even two tests at run-time:
>
> NumberClass classifyNumber(int n)
>     auto factors = filter!((i){ return n % i == 0; })(iota(1, n));
>     int diff = sgn(reduce!q{a + b}(0, factors) - n);
>     if (diff == -1)
>         return NumberClass.deficient;
>     else if (diff == 0)
>         return NumberClass.perfect;
>     else
>         return NumberClass.abundant;
> }
>
>
> This version is about as safe, and uses one array access on immutable array (I
have not used an emum array to avoid wasting even more run time):
>
> NumberClass classifyNumber(int n)
>     static immutable res = [NumberClass.deficient, NumberClass.perfect,
NumberClass.abundant];
>     auto factors = filter!((i){ return n % i == 0; })(iota(1, n));
>     int sign = sgn(reduce!q{a + b}(0, factors) - n);
>     return res[sign + 1];
> }
>
>
> Using a switch is another safe option, I can't use a final switch. This too has
some run-time overhead:
>
> NumberClass classifyNumber(int n)
>     auto factors = filter!((i){ return n % i == 0; })(iota(1, n));
>     int sign = sgn(reduce!q{a + b}(0, factors) - n);
>     switch (sign) {
>         case -1: return NumberClass.deficient;
>         case 0:  return NumberClass.perfect;
>         default: return NumberClass.abundant;
>     }
> }
>
>
> In theory a bit better type system (with ranged integers too as first-class
types) knows that sgn() returns the same values as the enum NumberClass, this
allows the first
> version without cast and compile-time proof of correctness:
>
>
> NumberClass classifyNumber(int n)
>     auto factors = filter!((i){ return n % i == 0; })(iota(1, n));
>     int difference = reduce!q{a + b}(0, factors) - n;
>     return sgn(difference);
> }
>
> I don't know what to think.
>
> Bye,
> bearophile

That overhead you are referring to gets negligible even for moderately large n. An entirely safe version of the code without any overhead would be:

enum NumberClass : int { deficient=-1, perfect=0, abundant=1 }

NumberClass classifyNumber(int n)
    auto factors = filter!((i){ return n % i == 0; })(iota(1, n));
    int difference = reduce!q{a + b}(0, factors) - n;
    //guard the cast:
    static assert(NumberClass.min==-1 && NumberClass.max==1);
    static assert(cast(int)NumberClass.deficient==-1 &&
cast(int)NumberClass.perfect==0 && cast(int)NumberClass.abundant==1);
    return cast(NumberClass)sgn(difference);
}

This is not the way of least resistance though.
May 13, 2011
Well I don't know about safety, but this will throw an exception at runtime on invalid enum values, and it also seems to perform a little better than your original classifyNumber function (The classifyNumberNewImpl function is the new one):

http://codepad.org/qyUaD1Hj

I'm not sure what is making the code faster, but on my machine I get:
classifyNumberNew: 5543
classifyNumberOld: 5550
May 13, 2011
Wow sorry please neglect that, I completely wiped out an entire line of code. Ignore that code please.
May 13, 2011
Or wait, I didn't? I think I'm confusing two implementations now. LOL.