Leveraging the Type System

The Type System is really powerful and can be used to make code much more clean and maintainable.

In this example, I've created an abstract type TempUnit to represent temperature units, specifically Fahrenheit and Celcius. I've then created two structs for Fahrenheit and Celcius which just have one field, deg to store the degree values for each.

abstract type TempUnit end

struct Fahrenheit <: TempUnit
    deg::Float64
end

struct Celcius <: TempUnit
    deg::Float64
end

For a conversion function, one may consider having a function which would take the type to convert to and then the value we want to convert, but the following approach is much cleaner.

We start off with a function which says if the target type is the same as the value we're trying to convert, then we just return the value provided as an argument. This is also why we needed the abstract type which both Fahrenheit and Celcius are subtypes of. This ensures we don't impact any other types, but this code is uniquely working with the temperature unit types we've built today.

function convert(::Type{T}, deg::T) where {T <: TempUnit}
    return deg
end

Next, I've written one function for converting fahrenheit and a second function for converting celcius. Because we've set the type constraint in the function definition itself, we know we are only working within a specific context.

function convert(::Type{Fahrenheit}, deg::Celcius)
    Fahrenheit(9/5 * deg.deg + 32.0)
end

function convert(::Type{Celcius}, deg::Fahrenheit)
    Celcius(5/9 * (deg.deg - 32.0))
end

And we test them out...

boiling = Fahrenheit(212.0)
freezing = Celcius(0.0)

@show convert(Celcius, freezing)
> convert(Celcius, freezing) = Celcius(0.0)

@show convert(Fahrenheit, boiling)
> convert(Fahrenheit, boiling) = Fahrenheit(212.0)

@show convert(Fahrenheit, freezing)
> convert(Fahrenheit, freezing) = Fahrenheit(32.0)

@show convert(Celcius, boiling)
> convert(Celcius, boiling) = Celcius(100.0)

Adding Kelvin is simple enough too.

struct Kelvin <: TempUnit
    n::Float64
end

function convert(::Type{Celcius}, n::Kelvin)
    Celcius(n.n - 273.15)
end

function convert(::Type{Kelvin}, deg::Celcius)
    Kelvin(deg.deg + 273.15)
end

absolutezero = Kelvin(0.0)

@show convert(Celcius, absolutezero)
> convert(Celcius, absolutezero) = Celcius(-273.15)

A little bit of clean up and adding of additional computational functionality.

abstract type TempUnit end

struct Fahrenheit <: TempUnit
    v::Float64
end

struct Celcius <: TempUnit
    v::Float64
end

struct Kelvin <: TempUnit
    v::Float64
end

# temperature conversion functions
import Base.convert
convert(::Type{T}, deg::T) where {T <: TempUnit} = deg
convert(::Type{Fahrenheit}, deg::Celcius) = Fahrenheit(9/5 * deg.v + 32.0)
convert(::Type{Celcius}, deg::Fahrenheit) = Celcius(5/9 * (deg.v - 32.0))
convert(::Type{Celcius}, n::Kelvin) = Celcius(n.v - 273.15)
convert(::Type{Kelvin}, deg::Celcius) = Kelvin(deg.v + 273.15)

import Base.:+, Base.:-, Base.:*, Base.:/

Base.:+(a::T, b::S) where {T <: Number, S <: TempUnit} = S(a + b.v)
Base.:+(a::S, b::T) where {T <: Number, S <: TempUnit} = S(a.v + b)
Base.:-(a::T, b::S) where {T <: Number, S <: TempUnit} = S(a - b.v)
Base.:-(a::S, b::T) where {T <: Number, S <: TempUnit} = S(a.v - b)
Base.:*(a::T, b::S) where {T <: Number, S <: TempUnit} = S(a * b.v)
Base.:*(a::S, b::T) where {T <: Number, S <: TempUnit} = S(a.v * b)
Base.:/(a::T, b::S) where {T <: Number, S <: TempUnit} = S(a / b.v)
Base.:/(a::S, b::T) where {T <: Number, S <: TempUnit} = S(a.v / b)

@testset "temperature conversions" begin
    boiling = Fahrenheit(212.0)
    freezing = Celcius(0.0)
    absolutezero = Kelvin(0.0)
    
    @test convert(Celcius, freezing) == Celcius(0.0)
    @test convert(Celcius, boiling) == Celcius(100.0)
    @test convert(Fahrenheit, boiling) == Fahrenheit(212.0)
    @test convert(Fahrenheit, freezing) == Fahrenheit(32.0)
    @test convert(Celcius, absolutezero) == Celcius(-273.15)
end

@testset "temperature struct computations" begin
    @test Celcius(0.0) + 4.0 == Celcius(4.0)
    @test Celcius(2.0) * 2 == Celcius(4.0)
end