Reasons for specifying the concrete types of function arguments in Julia

In many QuantEcon lectures of Julia, arguments of methods is specified as a concrete type such as Float64 or Int64.

Why don’t they accept abstract type?

As at the end of this section writes, there is no loss in performance by doing so.

It seems to me that writing functions with abstract type makes the program robust to the case where some values are extremely large.

Thanks for the suggestions @Shunsuke-Hori. It’s a good idea to debate this. Can you give us a particular example from one of the lectures to discuss?

Yes @Shunsuke-Hori, you are exactly right about it not impacting performance at all.

If you could provide an example like @john.stachurski recommended we could have a more pointed discussion.

Thank you @john.stachurski and @spencer.lyon .

I realize that most type specifications for function arguments are used with type definitions.

As far as I understand, specifying the types of the element improves performance and given that the types of the elements in the definition are specified, other function arguments which affect these elements should be specified in the same way.

However, how about this case from “An Introduction to Asset Pricing”?

Does zeta or p_s needs to be Float64? I don’t think it’s necessary (although the benefit from using abstract type must be negligible)

I don’t have a strong opinion in either direction. I like simple code, and the Float64 type on zeta could be omitted, in, say, this function

function consol_price(ap::AssetPriceModel, zeta::Float64)
    # == Simplify names, set up matrices  == #
    beta, gamma, P, y = ap.beta, ap.gamma, ap.mc.p, ap.mc.state_values
    y = reshape(y, 1, ap.n)
    M = P .* ap.g(y).^(- gamma)

    # == Make sure that a unique solution exists == #
    test_stability(ap, M)

    # == Compute price == #
    I = eye(ap.n)
    Ones = ones(ap.n)
    p = (I - beta * M) \ ( beta * zeta * M * Ones)

    return p
end

On the other hand, having the type specified tells me immediately that zeta is a scalar, which is useful.

I don’t think there is a “right” answer here. @spencer.lyon Do you have a stronger opinion?

I think more what @Shunsuke-Hori had in mind was the following

function consol_price(ap::AssetPriceModel, zeta::AbstractFloat)
    # ^^^ Note zeta is an AbstractFloat now rather than Float64
    ...
    return p
end

I don’t think switches like this should have any performance impact – The dispatch will just compile versions of the function for each concrete type of AbstractFloat the first time that version is called.

@cc7768 Yes, that’s what I have in my mind.

Regarding the point @john.stachurski notes, AbstractFloat still tells us that it is a scalar

Everything said here is correct. Abstract type parameters to not impact performance in any way, they just make restrictions a bit looser for dispatch.

I’m totally happy accepting changes like this to any of QuantEcon.jl, QuantEcon.applications, or the lectures.

The main reasons we ended up where we did is that (1) at the time that I wrote the code, I didn’t fully understand how abstract types work with the compiler and (2) on my machine Float64 is the default type for something like 1.0, so I typed that because it was easy and just works most of the time.

1 Like

Is there a reason why you don’t go all of the way and just duck-type? Or at least explain duck typing? The amount of strict typing on the functions that I see in the examples is usually what I would think of as not proper Julia code, or at least not proper generic coding.

It probably wouldn’t hurt to explain what duck-typing is, but many of the people in our target audience are probably already in the habit of doing so because of previous experience in Python or Matlab. @john.stachurski might have opinions about why (or why not) it would be useful to discuss in the lectures.

A couple of the reasons I think it can be beneficial to be specific about the argument types are:

  1. Specifying the type of the inputs serves a similar purpose to documentation. If a function is poorly documented then I can at least get an idea of how it is supposed to be used by looking at what its inputs are – Though I think the QE team has done a reasonable job keeping up with our documentation. Relatedly, it prevents people from passing things that we don’t want them to pass… If a function accepts a scalar (like in the consol_price example above) then we ensure that they run into an error as early as possible. I have found that having the type information helps me understand code that I wrote months earlier.
  2. The other benefit is multiple dispatch.

In my eyes, the benefit of typing the arguments often outweighs the cost. Though it is entirely possible that we occasionally take this too far. @spencer.lyon may have additional opinions about this.

@ChrisRackauckas Thanks a lot for your very helpful comment.

I agree with @cc7768 that, in some instances, it’s nice to have type information. At the same time, I like simplicity and readability, so duck typing is also attractive.

Here are some rules I propose:

Point 1: Methods with at least one argument typed should have all arguments typed.

There’s no hard and fast reason for this but typing just some arguments feels odd. For example,

function consol_price(ap::AssetPriceModel, zeta)
    # ^^^ Note zeta is not typed
    ...
    return p
end

seems less “symmetric” than

function consol_price(ap::AssetPriceModel, zeta::AbstractFloat)
    # ^^^ Note zeta is an AbstractFloat now rather than Float64
    ...
    return p
end

Point 2: It appears that we’ve been too heavy with concrete types inside method definitions.

Example: Sometimes arguments are required to be Float64s in the code when they could very well be integers in the model. Let’s shift to Real in this setting.

Backing off from some of these strict types would seem to address @ChrisRackauckas’s criticism.

1 Like