Promotion for arithmetic operations on @enum types

if I have an @enum defined like this: @enum CircPolType RightPol=1 LeftPol=-1,
And would like to then uses instances RightPol and LeftPol directly with their integer values in mathematical operations without having to wrap them always in Int(), what would be the preferred approach?
Should I redefine the mathematical operations for type CircPolType or is there something like defining promotion_rules to make it work more elegantly? (I tried looking at promotion_rule but couldn’t figure out how to make it work for my use case)

As an example, if I have:

pol = LeftPol
test = 3 * pol

I would like the result to be test == -3, but I just get an error as *(::Int,::CircPolType) is not defined

The easiest thing to do, if you want the enums to work with arithmetic, is actually to just define them as const integers, rather than proper enums. If you need the other Enum functionality though, then this isn’t an option. In that case, you can still do the following:

@enum MyEnum En1 En2 En3

Base.promote_rule(T::Type, ::Type{MyEnum}) = T
Base.convert(T::Type, v::MyEnum) = T(Integer(v))

for op in (:*, :/, :+, :-)
    @eval begin 
        Base.$op(a::Number, b::MyEnum) = $op(promote(a, b)...)
        Base.$op(a::MyEnum, b::Number) = $op(promote(a, b)...)
        # not sure if you want this last one. It allows 
        # e.g. En1 + En2, but that might just lead to bugs. YMMV
        Base.$op(a::MyEnum, b::MyEnum) = MyEnum($op(Integer(a), Integer(b)))
    end
end
julia> 2En3 + 6
10

Thanks for the answer,
My use case is actually dispatching on the type of polarization.
I have an abstract type Polarization and a concrete one CircPol <: Polarization that only has a field that can be either RightPol or LeftPol.

For the moment I defined this struct as

@enum CircPolType RightPol=1 LeftPol=-1
struct CircPol <: Polarization
    type::CircPolType 
end

I need the integer values to be exactly 1 and -1 for the computations involving them and the enum gave the nice display CircPol(RightPol) instead of CircPol(1) which is a bit more clear. It also forces the valid inputs when constructing a circular polarization to only be those 2 rather than all integers.

I could put RightPol and LeftPol as const for 1 and -1 and change Base.display for the display part, but is there a way to enforce the input to the constructor to only accept 1 and -1?

You can constrain the input by defining the inner constructor method as discussed in the Inner Constructor section of the Julia Docs. This could be achieved like this

struct CircPol <: Polarization
	type::Int
	CircPol(x::Int) = x == 1 || x == -1 ? new(x) : error("not valid polarization")
end

Alternatively however, if CircPol is a type that just stores another type why not make CircPol an abstract type that RightPol and LeftPol are subtypes of? They can both be a concrete type of their own with no fields and functions that need either one can accept the parent CircPol. Should then be possible to design functions and promotions as needed. (But I don’t know what I’m talking about usually so someone else should comment on if this is a good idea).

1 Like

I agree with @Jordan_Cluts in light of information you’ve added.

In addition, if you go with his first suggestion and still want a nice printout, just overwrite Base.show

Base.show(io::IO, ::MIME"text/plain", c::CircPol) = println(io, "CircPol(", c.type==1 ? "RightPol" : "LeftPol", ")")
2 Likes

Many thanks to both for the nice suggestions.
In the end I think for my use case the cleaner option is to go with the first suggestion from @Jordan_Cluts and adding the method for pretty printing from @tomerarnon.
At the moment I don’t foresee the need of dispatching the left and right separately and using this approach I don’t have to redefine custom arithmetic operations for the new types :smiley:

I noticed that the custom Base.show implementation only works if printing the struct CircPol directly in the REPL.
If I print another struct that has a CircPol as one of the fields, the display of the pol field for that struct goes back to the standard one.

So for example I have a feed struct that has the polarization as one of its fields, and printing the feed on the REPL gives me this:
HuygensSourceFeed{Float64}(1.77e10, 0.016937427005649718, 370.96456888544765, 12.566370614359172, 1.634804083471573e-33, 0.2412473, -22.0, 0.23441221753550226, CircPol{Int64}(1))

Do I have to redefine the Base.show method for all custom types that contain pol as a field or is there a way to propagate the show of CircPol?

Do I have to redefine the Base.show method for all custom types that contain pol as a field or is there a way to propagate the show of CircPol?

No, that would be crazy! Imagine if each new new container type had to reimplement show for all possible objects it could contain (numeric types, other containers, itself)! It would be madness.

There are only two methods you need. The one from before, which is for “display” style printing. I.e. when the object is front-and-center like if you directly println it. And the other that does not use ::MIME"text/plain" for when it’s in a context that would otherwise be considered more “compact”. Like inside another object.

Base.show(io::IO, c::CircPol) = println(io, "CircPol(", c.type==1 ? "RightPol" : "LeftPol", ")")
1 Like

That makes much more sense indeed :smiley:.

Thanks for keeping helping :slight_smile: