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.
I was thinking maybe every Function
should have its own singleton subtype typeof(f)
, but that’s not written anywhere for instance.
I think not. A callable structs might want to inherit from something else instead, right ?
I’d be curious to know what these are.
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.
That’s true, I adjusted the title to better reflect my question.
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.
Interesting! For future reference, this is because Julia doesn’t specialize on Function
arguments by default.
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.
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.