Why does Core.Compiler.return_type expect function instance instead of function type?

While Core.Compiler.return_type can give imprecise results, it is quite handy from time to time. Today my question is not about its usage, however about why the first argument - the function - is different compared to the rest.

All other arguments are given by type, but the function is given by instance.

Is there an underlying reason for this, or is it just an happenstance and underlying there is its counterpart, which takes everything by type without any drawback.


EDIT:
using typeof and apply(f, args...; kwargs...) = f(args...; kwargs...) I can get a homogenous version of return_type.

return_type(FullTuple) = Core.Compiler.return_type(apply, FullTuple)

The core of my question is more whether this has any known disadvantages, or whether you really should use the function instance instead of the function type.

I am almost sure that someone understands it better and could give you a better answer, but I would guess that is because: (1) the type of a function and the function itself while different, are a 1-to-1 match, each function has its own single type, so they are kinda interchangeable; (2) it is possible that Core.Compiler.return_type either do some kind of partial evaluation of the function, or call some other function that was also defined for the function itself (not its type) so it was just more convenient to receive the function too; (3) the reason Core.Compiler.return_type gives imprecise types is probably because it only looks if it can state which is the return type of the method of the function that takes arguments of such types, the type of return can vary with the values passed (but to know this you have to basically evaluate the code), but you can already have an idea if you know the method that will be called only return values of some type, so Core.Compiler.return_type is already making clear in its signature that it only look at parameter types not how the values can affect the return type.

2 Likes

thanks a lot, I made an EDIT to my question to clarify my focus.

I am not sure why you expect that it would be similar to the other arguments — the function plays a special role there.

Also, using a type would be somewhat cumbersome, with typeof(f) etc. In any case, either one identifies the function.

2 Likes

thanks.

sometimes you may only have the function type available and need to fallback to using something like apply. Then it would be great to reuse the same machinery as for functioninstances.

is the use of apply the most basic you can get, or is there another function in Core which can be used for this?

apply(f, args...; kwargs...) = f(args...; kwargs...)
return_type(FullTuple) = Core.Compiler.return_type(apply, FullTuple)
ulia> f() = 1
f (generic function with 1 method)

julia> F = typeof(f)
typeof(f)

julia> Core.Compiler.singleton_type(F) === f
true
3 Likes

really nice.

Caveat: This does not work for singleton Types

Core.Compiler.singleton_type(Type{Dict})

and probably also not for other callables…

Note that

  1. Dict is not a singleton type (it has two parameters),

  2. you call the function above with the type directly, eg

    julia> Core.Compiler.singleton_type(Base.HasLength)
    Base.HasLength()
    
  3. this is an internal method for special cases, and needing it too often may be a code smell. it is hard to be more specific without code, but generally it is recommended to operate on instances, not types.

thanks for the elaboration. I was referring to Type{Dict} which should have the single “instance” Dict (and not Dict, which as you say has many instances).

I will try a bit forward with using the apply way, and will keep your warnings in mind.
Thanks a lot

1 Like

I see — sorry I misunderstood. Thanks for clarifying.

Generally, I prefer to think of Type as a special construct to use in method signatures, not as part of the type system (though technically it is). IMO using it for anything unrelated dispatch may be a code smell. The manual could use some advice/clarification about this.

1 Like

Belatedly, I just realized there are two conceptually related but not overlapping definitions for “singleton type” in Julia. The manual talks about Type{T} as “the” singleton type, while it is a singleton type. Maybe that is what caused confusion here. I commented in

1 Like

I experimented a bit more with the apply approach and indeed it turns out to be more difficult than expected.

Concretely, nested apply constructs do not seem to work. I opened an issue for this and also found a workaround which you can find there: https://github.com/JuliaLang/julia/issues/36626.

To repeat the workaround in this context, it solves some inference problems if you use

good_inference_apply(f, args) = f(args...)

instead of bad_inference_apply(f, args...) = f(args...)