Does anyone program like this?

In chemistry we have the ideal gas Law PV=NRT

So do anyone program like this?

function CalcVIR(v::Union{Number,Missing},
                 i::Union{Number,Missing},
                 r::Union{Number,Missing})
    ''' If voltage is missing then calc v = i * r
        If current is missing then calc i = v / r
        If resistance is missing then calc r = v / i
    '''
    if ismissing(v)
        return i * r
    elseif ismissing(i)
        return v / r
    elseif ismissing(r)
        return v / i
    end
end

And using it like

Starting Julia...
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _  |  |
  | | |_| | | | (_| |  |  Version 1.2.0 (2019-08-20)
 _/ |\__ _|_|_|\__ _|  |  Official https://julialang.org/ release
|__/                   |

julia> v = CalcVIR(missing,3,5)
15

julia> v = CalcVIR(missing,3.4,5.6)
19.04

julia> i = CalcVIR(7,missing,3)
2.3333333333333335

Well, is there any advantage compared to writing the fraction explicitly?

I mean do people write function(s) that only returns the value of the missing parameter?

It’s how chemistry student uses the ideal gas Law in chemistry.

This doesn’t feel idiomatic to me since you’ll need to keep track what will be returned. I would probably assign the missing value and then return v, i, r

1 Like

I think I would prefer something like

complete_vir(v::Missing, i::Real, r::Real) = (v = i * r, i = i, r = r)
complete_vir(v::Real, i::Missing, r::Real) = (v = v, i = v / r, r = r)
complete_vir(v::Real, i::Real, r::Missing) = (v = v, i = i, r = v / i)

This way, it would be easier to avoid bugs from using the wrong variable accidentally from the output.

17 Likes

I might write it like this:

function CalcVIR(;v=missing, i=missing, r=missing)
    !ismissing(i) && !ismissing(r) && return i * r # i, r present 
    !ismissing(v) && !ismissing(r) && return v / r # v, r present 
    !ismissing(v) && !ismissing(i) && return v / i # v, i present 
    throw("Missing inputs ..")                     # 1 or 2 inputs missing 
end

and test different inputs:

julia> v = CalcVIR(i=3, r=5)
15

julia> i = CalcVIR(v=15, r=5)
3.0

julia> r = CalcVIR(v=15, i=3)
5.0

julia> v = CalcVIR(i=3)
ERROR: "Missing inputs .."
Stacktrace:
 [1] #CalcVIR#13(::Missing, ::Int64, ::Missing, ::Function) at .\REPL[32]:5
 [2] (::getfield(Main, Symbol("#kw##CalcVIR")))(::NamedTuple{(:i,),Tuple{Int64}}, ::typeof(CalcVIR)) at .\none:0
 [3] top-level scope at none:0
2 Likes

But how do you know which variable is being returned?

You could use keyword arguments as input and return a named tuple of all three.

1 Like

The keyword inputs are named, so it is obvious that the missing one is being returned. I agree, however, that using a named tuple for outputs can be better.

Something like this:

function CalcVIR(;v=missing, i=missing, r=missing)
    !ismissing(i) && !ismissing(r) && return (v = i * r, i = i, r = r) # i, r present 
    !ismissing(v) && !ismissing(r) && return (v = v, i = v / r, r = r) # v, r present 
    !ismissing(v) && !ismissing(i) && return (v = v, i = i, r = v / i) # v, i present 
    throw("Missing inputs ..")            # missing 1 or 2 inputs
end

Voting for @Tamas_Papp’s solution.
Maybe even more idiomatic is to put such things into a struct that would always contain consistent values:

struct VoltCurRes{T<:Number}
    voltage::T
    current::T
    resistance::T
    function VoltCurRes(volt::V, cur::I, res::R) where {V<:Number, I<:Number, R<:Number}
        struct_type = reduce(promote_type, (V, I, R))
        new{struct_type}(volt, cur, res)
    end
end

VoltCurRes(volt::Missing, cur::Number, res::Number) = VoltCurRes(cur * res, cur, res)

VoltCurRes(volt::Number, cur::Missing, res::Number) = VoltCurRes(volt, volt / res, res)

VoltCurRes(volt::Number, cur::Number, res::Missing) = VoltCurRes(volt, cur, volt / cur)

Update: While the logic here is almost the same as with ismissing() checks, there is at least one logical and one performance-relevant difference.
The logical difference is, the functions as written by Tamas and myself explicitly require exactly one missing argument and will give an error if there are more.
The performance difference is that the type matching can be done at compile time while the ismissing() checks have to be done at runtime, which makes the version employing multiple dispatch faster in some cases.

9 Likes

I like your answer Vasily_Pisarev

because in Electrical Engineering, the Voltages can be Real or Complex.

In fact having Imaginary Voltages is PERFECTLY normal in Electrical Engineering.

So the type Number is highly suitable for Electrical Engineering. This is why I use the type Number instead of the type Real or AbstractFloat in my julia code for the function CalcVIR.

i saw this on a matlab program to calculate psycrometric properties for humid air. in that case they take the parameters as keywords and returned all variables (calculated and specified). its used a lot in chemical engineering simulation. here is my two cents on how to do it:

#main function: the user should only see calculate_vir, 
#and not use the implementation calc_vir
calculate_vir(;v=missing;i=missing;r=missing) = calc_vir(v,i,r)
calc_vir(v::Missing, i::Number, r::Number) = (i*r,i,r)
calc_vir(v::Number, i::Missing, r::Number) = (v,v/r,r)
calc_vir(v::Number, i::Number, r::Missing) = (v,i,v/i)

Here is an interesting case, you can throw an error (overspecified), or try to calculate using some values (the cheapest calculation), it depends if your calculation comes from a system or it is user specified

#this comes from user overspecification
calc_vir(v::Number, i::Number, r::Number) = error("system overspecified")
#in this case, a tuple comes from a system calculation, so its reasonable to return the same tuple.
#you can add some checks if you want to be sure, in this case v≈i*r
calc_vir(vir::Tuple{Number,Number,Number}) = vir
1 Like