Promotion, Multiple Dispatch, in example using temperature

Hello all,

I’m trying to understand conversion, multiple dispatch, and promotion using a modified version of the example described in this blogpost. The post discusses multiple dispatch, promotion, and conversion using the example of temperature, and how to combine measurements in different temperature units (Kelvin, Celsius, Fahrenheit).

Here is a modified collection of the script used in this post (Julia 1.5):

abstract type Temperature end
import Base:+,-, promote,promote_rule
types = [:Celsius, :Kelvin, :Fahrenheit]
for T in types
    @eval begin
        struct $T <: Temperature
            value::Float64
        end

        +(x::$T, y::$T) = $T(x.value + y.value)
        -(x::$T, y::$T) = $T(x.value - y.value)
    end
end
convert(::Type{Kelvin},  t::Celsius)     = Kelvin(t.value + 273.15)
convert(::Type{Kelvin},  t::Fahrenheit)  = Kelvin(Celsius(t))
convert(::Type{Celsius}, t::Kelvin)      = Celsius(t.value - 273.15)
convert(::Type{Celsius}, t::Fahrenheit)  = Celsius((t.value - 32)*5/9)
convert(::Type{Fahrenheit}, t::Celsius)  = Fahrenheit(t.value*9/5 + 32)
convert(::Type{Fahrenheit}, t::Kelvin)   = Fahrenheit(Ceslius(t))
for T in types, S in types
    if S != T
       @eval $T(temp::$S) = convert($T, temp) 
    end
end

promote_rule(::Type{Kelvin}, ::Type{Celsius})     = Kelvin
promote_rule(::Type{Fahrenheit}, ::Type{Kelvin})  = Kelvin
promote_rule(::Type{Fahrenheit}, ::Type{Celsius}) = Celsius



+(x::Temperature, y::Temperature) = +(promote(x,y)...);
-(x::Temperature, y::Temperature) = -(promote(x,y)...);

From what I understand of this based on the docs, calling promote_type(Kelvin,Celsius) Should return Kelvin, but instead it’s returning Temperature in the REPL. Moreover, attempting to add, say Kelvin(4)+Celsius(5) returns an error that there is no method for adding these two types. All this leads me to conclude that the promotion text here is not working as intended. If so,could anyone help me see why that is not the case?

Edit: using

import Base: promote,promote_rule

Fixes the behavior before: now promote_type(Kelvin,Celsius) returns Kelvin, as expected. However, adding between types still seems to be failing, so not sure what’s going on.
In the REPL, I get

 Kelvin(4)+Celsius(5)
ERROR: MethodError: Cannot `convert` an object of type Celsius to an object of type Kelvin
Closest candidates are:
  convert(::Type{T}, ::T) where T at essentials.jl:171
  Kelvin(::Celsius) at types_temp.jl:23
  Kelvin(::Any) at types_temp.jl:8
Stacktrace:
 [1] _promote at .\promotion.jl:259 [inlined]
 [2] promote at .\promotion.jl:282 [inlined]
 [3] +(::Kelvin, ::Celsius) at types_temp.jl:33
 [4] top-level scope at REPL[1]:1

Any help?

Edit 2: Okay, the obvious solution is that I also needed import Base: convert. Which fixed everything and makes the code run in the expected way.

I guess I solved all of my problems, but I guess my question is now:

is this best practice for writing Julia code (as artificial as this example might be)? Is there a more elegant, more Julian way to do the same thing? Why the need to import all of these Base functions? What negative behavior occurs if that import feature is implicit?

1 Like

I can speculate on a couple reasons to only import explicitly. First, just like using, there may be some significant compilation involved when using an external package. I just tried import DifferentialEquations.solve and it triggered a lengthy precompilation. Second, it is just good programming practice to warn about imports. Otherwise you might forget that somewhere deep in your module, you accidentally did some type piracy or damaging redefinition of another package’s code.

2 Likes

I think this is the main reason. You should explicitly import or qualify the function (from other package) you want to extend as a prove that you know what your doing.

Just FWIW, for whoever wanting to do temperature conversions and not thinking about the multiple-dispatch and promotion mechanisms, you can use Unitful.jl:

julia> using Unitful: K, °F, °C

julia> 3.2K |> °F
-453.91 °F

julia> 102.3°F |> °C
39.0555555555556 °C

julia> 102K + 10.0°C
385.15 K
9 Likes

Moreover, Unitful prevents erroneous operations between affine quantities such as 0°C + 0°C. On the other hand the code above has

Celsius(0) + Celsius(0) == Celsius(0)

but

Celsius(Kelvin(Celsius(0)) + Kelvin(Celsius(0))) == Celsius(273.15)

which can cause subtle bugs, because the meaning of + changes depending on which units one is currently working in.
20°C is not the “double” of 10°C, it is 293.15°C: 2K(10.0°C) |> °C.

4 Likes
Celsius(Kelvin(Celsius(0)) + Kelvin(Celsius(0))) == Celsius(273.15)

Oh that is a problem, oops!

Thanks for the answers everyone, very helpful. This example was more for me to better understand how the features of julia, obviously it’s not perfect but I do think I understand these concepts better.

2 Likes

If you muck around with Temperature, you need to know there is two types of Temperature Types.

  1. Temperature
  2. TemperatureDifference

These are NOT the same.

for example:  
                      0 C == 273 K  in Temperature
BUT
                      0 C == 0 K   in TemperatureDifference

If you get these two types mixed up, you will have ENDLESS problems in your coding.

2 Likes