What is a good way to write methods that help the user not mix up input order?

I’ve often encountered issues similar to Keyword argument types - what's going on? - #11 by sijo . I like to write methods with keyword arguments because I find them harder to silently screw up calling than positional arguments. Making up an example, let’s consider a function which gives utility from mean and variance of returns U = E[r] - \frac{1}{2} A \sigma^2:

function utility(expected_return, sd_of_returns, risk_aversion)
    expected_return - 1/2 * risk_aversion * sd_of_returns^2
end

Suppose a stock has E[r_s] = 10\% and \sigma_s = 20\% and an individual’s risk aversion A = 2, then this could be called:

julia> utility(0.1, 0.2, 2.0)
0.06

Since these are all Float64, nothing stops someone from making a mistake such as the following:

julia> utility(0.2, 0.1, 2.0)
0.19

Of course, the potential mistakes increase in the number of arguments, the complexity of their meaning, etc.

Now. If the function is rewritten to use keyword arguments, there is visually no ambiguity:

julia> function utility(;expected_return, sd_of_returns, risk_aversion)
                         expected_return - 1/2 * risk_aversion * sd_of_returns^2
                         end
utility (generic function with 1 method)

julia> utility(0.1, 0.2, 2.0)
ERROR: MethodError: no method matching utility(::Float64, ::Float64, ::Float64)
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1

julia> utility(risk_aversion=2.0, sd_of_returns = 0.2, expected_return = 0.1)
0.06

Lovely. But, keyword-argumenting everything seems to be discouraged by the design of the method dispatch system. Furthermore, unlike in this MWE, often I want to write multiple methods that do dispatch differently on types of arguments. But I’d still like to reduce argument order ambiguity.

There are clearly some possible solutions, including:

  1. GitHub - simonbyrne/KeywordDispatch.jl: Dispatch on keyword arguments as mentioned in the previously linked post here—but this is currently failing CI and again does not seem to be an encouraged approach
  2. Create a bunch of custom types e.g. struct ExpectedReturn and then use typing function utility(expected_return::ExpectedReturn,...); utility(ExpectedReturn(0.1)...) —but this is overbearing!
  3. Similar approach with typing the input itself in a new type (perhaps à la Parameters.jl, @with_kw struct UtilityInput ... end; utility(UtilityInput(expected_return = 0.1, ...))…but once again this is quite overbearing.

Is strikes me as strange that simply essentially offering the user a “tip” of being able to tell the function/method “here’s the name of the argument I’m trying to pass this value to” needs to break dispatch. What’s so special about argument order? Couldn’t an equivalent ordered-argument method always be present “under-the-hood” for any given named-argument function? (I assume, without being able to understand its source code, that this is what KeywordDispatch.jl tries to achieve.)

TL;DR, is there not a way that combines the elegance, simplicity, and error-preventiveness of keyword arguments with the elegance, power, and stability of positional argument method dispatch?

1 Like

One thing I do sometimes is to define a function as

f(a1, a2, a3) = ....
f(; a1, a2, a3) = f(a1, a2, a3)

This is generally quite simple to do (just one more line of code) and still allows multiple dispatch by adding methods to f based on the type of the arguments.

julia> utility(expected_return, sd_of_returns, risk_aversion) = expected_return - 1/2 * risk_aversion * sd_of_returns^2
julia> utility(; expected_return, sd_of_returns, risk_aversion) = utility(expected_return, sd_of_returns, risk_aversion)
julia> utility(0.1, 0.2, 2.0)
0.06
julia> utility(risk_aversion = 2.0, sd_of_returns = 0.2, expected_return = 0.1)
0.06
julia> utility(expected_return::Int, sd_of_returns, risk_aversion) = println("No integers, please!")
julia> utility(expected_return = 1, sd_of_returns = 0.2, risk_aversion = 2.0)
No integers, please!
3 Likes

Sometimes defining say f((a,b)::Pair, c) = (; a,b,c) and thus accepting f(0.1 => 0.2, 0.3) helps.

In this case, maybe utility(0.1 ± 0.2, 0.3) makes more sense than =>. There’s no pre-defined ±, but you could borrow it from Measurements.jl, or you could define your own struct PlusMinus.

1 Like

One option is to require a named tuple as the input of the function at the interface. Then you can do whatever you want with the types internally:

julia> _utility(a::Float64,b::Int) = 1
_utility (generic function with 1 method)

julia> _utility(a::Float64,b::Float64) = 2
_utility (generic function with 2 methods)

julia> utility(input) = _utility(input.a,input.b) # interface function
utility (generic function with 1 method)

julia> input = (a=1.0, b=2.0)
(a = 1.0, b = 2.0)

julia> utility(input)
2

julia> input = (a=1.0, b=2)
(a = 1.0, b = 2)

julia> utility(input)
1

The function at the interface can then also check for the types, provide sensible error messages, etc. I usually do this, but instead of a named tuple I use a custom type, InputData, or whatever.

Not unreasonable… add some docstrings and you’re off to the races.

"`Stonk(expected_return, standard_deviation)`\n\nA fractional digital asset, kinda like an NFT"
struct Stonk{R, S}
    r :: R
    σ :: S
end

"`Investoor(risk_aversion)`\n\nPerson who full-port YOLOs stimmy checks into 0DTE 0.05Δ call options"
struct Investoor{A} 
    a :: A
end

"`utility(stonk, investoor)`\n\nCalculate expected utility for some contrived notion of rationality"
utility(stonk::Stonk, investoor::Investoor) = let (; r, σ) = stonk, (; a) = investoor
    r - (1/2)a*σ^2 
end

This allows you to express the utility of this asset to that person, which is a bit more meaningful than a bunch of numbers in arbitrary order.

julia> let TSLA=Stonk(0.1, 0.2), uncle_joe = Investoor(2.0)
           @show u=utility(TSLA, uncle_joe)
           if u > 0; println("TELL UNCLE JOE TO BUY ON 100x MARGIN!! 💎🙌") end
       end
u = utility(TSLA, uncle_joe) = 0.06
TELL UNCLE JOE TO BUY ON 100x MARGIN!! 💎🙌

Very interesting; thanks! I think this is the cleanest and most flexible of the proposals. I will tinker a bit and come back to mark this as the solution after I stress-test the proposal in some harder cases.

Also interesting. I think the InputData idea is essentially what I wrote as option #3 (InputData = UtilityInput) but the alternate of using a NamedTuple so one doesn’t have to create a struct is worth considering. My hunch, though, is that it would end up being an unhappy middle–not as rigorous as spelling out what the type InputData should be nor as lightweight as just taking unstructured arguments. Thanks for the suggestion!

(Lol.)

But do you think this is an anti-pattern if applied too often? In the case you mentioned, it seems to make sense. But for broader purposes, it seems like it would create “type clutter” to essentially have to have a type that wraps every or vector of primitives before a module’s methods can operate on it. I wasn’t specific enough when I wrote the question, but I think the use case you indicate is better served by having types, but in a sense when those types are already germane to the problem / module. When a method is “naturally” about primitives (numbers), I think @mbaz’s solution feels more natural. But all of this is great food for thought; thanks all.

“All things are poison, and nothing is without poison; the dosage alone makes it so a thing is not a poison.” – Paracelsus

Another way to check arguments are lined up is to use types or dimensional analysis. I’m not sure what the dimensions of your example are but you might have a better idea.

1 Like