Type promotion not working as expected

So I am learning Julia from Erik Engheim’s book “Julia for Beginners”. I am working through the code as I read the book (I put all my work in Github if anyone else is using the book and want to compare their own code)

He has a chapter on defining angle types as a way to teach type conversion rules and promotion (which is quite a nice feature of Julia). Unfortunately, my code isn’t working as expected and I’m not sure why. Here is the code:

abstract type Angle end

struct Radian <: Angle
    radians::Float64
end

struct DMS <: Angle
    seconds::Int
end

begin
    Degree(degrees::Integer) = Minute(degrees * 60)
    Degree(deg::Integer, min::Integer) = Degree(deg) + Minute(min)
    Degree(deg::Integer, min::Integer, secs::Integer) = Degree(deg, min) + Second(secs)

    function Minute(minutes::Integer)
        DMS(minutes * 60)
    end

    function Second(seconds::Integer)
        DMS(seconds)
    end 

    import Base: -, +, show, convert, *, /, promote_rule
    +(Θ::DMS, α::DMS) = DMS(Θ.seconds + α.seconds)      
    -(Θ::DMS, α::DMS) = DMS(Θ.seconds - α.seconds)

    +(Θ::Radian, α::Radian) = Radian(Θ.radians + α.radians)
    -(Θ::Radian, α::Radian) = Radian(Θ.radians - α.radians)

    function degrees(dms::DMS)
        minutes = dms.seconds ÷ 60
        minutes ÷ 60
    end

    function minutes(dms::DMS)
        minutes = dms.seconds ÷ 60
        minutes % 60
    end

    seconds(dms::DMS) = dms.seconds % 60

    function show(io::IO, dms::DMS)
    print(io, degrees(dms), "° ", minutes(dms), "' ", seconds(dms), "''")
    end

    function show(io::IO, rad::Radian)
    print(io, rad.radians, "rad")
    end

    # convert constructors
    Radian(dms::DMS) = Radian(deg2rad(dms.seconds/3600))
    DMS(rad::Radian) = DMS(floor(Int, rad2deg(rad.radians) * 3600))

    convert(::Type{Radian},  dms::DMS)    = Radian(dms)
    convert(::Type{DMS},     rad::Radian) = DMS(rad)

    sin(rad::Radian) = Base.sin(rad.radians)
    cos(rad::Radian) = Base.cos(rad.radians)

    sin(dms::DMS) = sin(Radian(dms))
    cos(dms::DMS) = cos(Radian(dms))

    *(coeff::Number, dms::DMS) = DMS(coeff * dms.seconds)
    *(dms::DMS, coeff::Number) = coeff * dms
    /(dms::DMS, denom::Number) = DMS(dms.seconds/denom)

    *(coeff::Number, rad::Radian) = Radian(coeff * rad.radians)
    *(rad::Radian, coeff::Number) = coeff * rad
    /(rad::Radian, denom::Number) = Radian(rad.radians/denom)

    const ° = Degree(1)
    const rad = Radian(1)

    Base.promote_rule(::Type{Radian}, ::Type{DMS}) = Radian
end

As can be seen I have a promote rule and this works for me:

+(promote(90°,3.14rad/2)...)
# result: 
3.140796326794897rad

But it I just do a + without the explicit promote it complains. I don’t understand why it isn’t automatically promoting the DMS to Radian:

90°+ 3.14rad/2
# result:
ERROR: MethodError: no method matching +(::DMS, ::Radian)
Closest candidates are:
  +(::Any, ::Any, ::Any, ::Any...) at operators.jl:560
  +(::DMS, ::DMS) at /Users/aronet/Code/FourM/Study/Julia/JuliaforBeginners/angleunits.jl:25
  +(::Radian, ::Radian) at /Users/aronet/Code/FourM/Study/Julia/JuliaforBeginners/angleunits.jl:28
Stacktrace:
 [1] top-level scope
   @ REPL[5]:1

Thanks for any help.

abstract type Angle <: Number end

reason:

Alternatively, define methods

+(x::Angle, y::Angle) = +(promote(x,y)...)
# etc.

Number already has these defined, so Angle inherits them.

if the point is to get promotion rules to do the work for you, this would be going in the other direction…

Yes, either inherit them, or do it yourself.

@jling Thanks so much! That solved the problem!

For those reading the book, I am not sure why Engheim left this out in his definition but it seems he might have preferred @DNF’s solution since he is trying to get you to “do the work yourself”. Of course he left that bit out, which was confusing. Thanks @DNF for your insights as well.

That makes sense. One thing to note is that although Julia is flexible enough to let you write your own promote rules, doing so correctly is surprisingly difficult, and result in infinite recursion. As such for most real use cases, you probably want to just use the ones in Base since they probably do what you want and are relatively reliable.

2 Likes

my advise is: if they are really number-like thing, user promotion to make life easy, other-wise, don’t. For example, you should never use promotion rule for adding vector to a scalar.

3 Likes