Can callable structs subtype Function? Should they?

The docstring of Function doesn’t specify any constraints on subtypes, so I’m wondering whether it makes sense to define callable structs (functors) as <: Function.

As far as I remember, the general rule was “no”, because there’s some additional requirements for Function that are not really documented. It’s not just being callable.

1 Like

I was thinking maybe every Function should have its own singleton subtype typeof(f), but that’s not written anywhere for instance.

Should callable structs (functors) subtype Function?

I think not. A callable structs might want to inherit from something else instead, right ?

I’d be curious to know what these are.

1 Like

Nope, closures are not singletons and are subtypes of Function:

julia> f = let x = 1
           () -> x + 1
       end
#1 (generic function with 1 method)

julia> supertype(typeof(f))
Function

julia> Base.issingletontype(typeof(f))
false

FWIW I think if your struct is mainly used “like” a function, and there’s not another abstract type that makes more sense for you to inherit from, it can make sense to make it a <:Function because sometimes people write type signatures like

function higher_order(f::Function)
    ...
end

This isn’t so common, but it does unfortunately happen so you can bypass it by subtyping function, but honestly it’s not a big deal either way IMO.

4 Likes

That’s true, I adjusted the title to better reflect my question.

1 Like

As a datapoing – Accessors.jl, a popular package which primary functionality is the creation of “fancy callables”, does not subtype Function for performance reasons. See a discussion there.

3 Likes

Interesting! For future reference, this is because Julia doesn’t specialize on Function arguments by default.

4 Likes

Pedant: Julia specializes on function args when they’re explicitly called in the function body. If folks are going to treat it like a normal function, then having it optimize like a normal function is a feature, I think. The only trouble is if it plays some sort of double-duty — for example, dispatching on : has thrown me for a loop so many times it’s embarrassing.

It is really common — there are hundreds of Function subtypes in the ecosystem. I’m not aware of any “interface” requirements beyond the callable method.

5 Likes

What do you mean?

Here’s an example:

: does double-duty: it’s a function to create ranges but it’s also — by itself — used as a flag for slicing. In the latter usage, you don’t call it like a function so it might not specialize and can lead to unexpected performance snags.

4 Likes