I mentioned it in my original post, but let me know if more detail would help.
Note that you could reasonably define addition and scalar multiplication on functions that output values which are members of a vector space. Think lazy + and * operations. For example, if you had f(x::Number) = x^2 and g(x::Number) = sin(x) - 1. Then writing h = 2f+g is equivalent to defining h(x) = 2*f(x) + g(x) or h(x) = 2x^2 + sin(x) - 1. The function itself does not need to be linear like y(x) = 3x or something.
Yes, but I think you would also want the result of addition or scalar multiplication to be tagged as a linear functional if the arguments were linear functionals, right?
The goal of tagging âfunctionsâ makes plenty of sense, but what is unclear from your post is why <: Function itself is necessary to do that. Why not just create your own traits and ignore the Function supertype entirely? There are many cases of callable types in julia which would fail the <: Function but still be used for anything that requires a âfunctionâ. If there is a particular outside interface that you need to use which is expecting <: Function for dispatching, perhaps the interface can be changed itself?
Poor choice of words on my part. This assumes that h has been initialized to be a LinearFunctional type, or whatever we should call the type representing functions that are members of our function space.
I agree. I wanted to be able to use the function block syntax to define methods on my own callable type. Since this apparently works for any singleton type, there is likely no need to subtype Function. (I had erroneously assumed function blocks only worked on Functions.)
Not only does this work for any singleton type, it works for ANY type:
julia> mutable struct FooBar
baz::Int
end
julia> function (fb::FooBar)(x)
fb.baz + x
end
julia> a = FooBar(3)
FooBar(3)
julia> a(5)
8
julia> a.baz = 10
10
julia> a(5)
15
On the contrary - <: Function does mean a bit more than just âcallableâ and (generally) entails a bit more of an opt-in than you may want. Itâs not to be confused with the mathematical concept of a function.
Let me clarify what I mean. Consider the following types and their instances.
struct Foo <: Function end
const foo = Foo() # singleton
struct Bar end
const bar = Bar() # singleton
struct Baz val::Int end
baz42 = Baz(42) # not singleton
You can define methods using the names foo and bar, but not baz42.
julia> foo(x) = x^2/2
(::Foo) (generic function with 1 method)
julia> foo(x,y) = sin(x*y)
(::Foo) (generic function with 2 methods)
julia> bar(x) = x^3
Bar()
julia> bar(x,y) = cos(x+y)
Bar()
julia> baz42(x) = exp(-x)
ERROR: cannot define function baz42; it already has a value
Stacktrace:
[1] top-level scope
@ none:0
[2] top-level scope
@ REPL[6]:1
Since foo isa Function, it also prints the number of methods that have been defined.
Ok - this seems to bring out an additional (albeit minor) detail if I am understanding things correctly. Functions seem to have their âownâ method table.
julia> function dummyfunc end
dummyfunc (generic function with 0 methods)
julia> typeof(dummyfunc).name.mt
# 0 methods for generic function "dummyfunc"
On the other hand, singleton objects do not.
julia> struct _MyFuncType <: Function end
julia> const dummymyfunc = _MyFuncType()
(::_MyFuncType) (generic function with 0 methods)
julia> typeof(dummymyfunc).name.mt
# 71 methods for callable object:
[1] (::Core.Compiler.Colon)(a::Real, b::Real) in Core.Compiler at range.jl:3
[2] (::Core.Compiler.Colon)(start::T, stop::T) where T<:Real in Core.Compiler at range.jl:5
...
[71] (I::LinearAlgebra.UniformScaling)(n::Integer) in LinearAlgebra at /home/ngchis/.local/opt/julia/1.8.3/share/julia/stdlib/v1.8/LinearAlgebra/src/uniformscaling.jl:84
Thus, there does seem to be an internal distinction between functions and callable objects (functors) that the user cannot access.
This distinction comes out in the code quoted above. When you define a function and show it, a string with the function name and number of methods is printed, which can be helpful in debugging.
julia> dummyfunc
dummyfunc (generic function with 0 methods)
If you define a singleton type that subtypes Function, the name of the singleton type is printed instead.
julia> dummymyfunc
(::_MyFuncType) (generic function with 0 methods)
This makes sense, since we could have named our instance of _MyFuncType anything and the name would not be known to the compiler ahead of time. Unfortunately, this is less helpfulâor at least uglierâfor debugging, especially if you gensym a symbol for the singleton type like in the macro @dylanxyz wrote. (@dylanxyzâs solution is to embed a new show method right in the macro.)
Itâs really weird to me that typeof(x).name.mt for an instance x gives a massive list of seemingly unrelated methods, I would love some explanation for that. But methods(x) gives the right answer (hence its use in the Base.show method) with a different method list type.
Julia is implemented based on multiple dispatch, so mt simply accesses the entire method table. The methods function instead filters to return the set applicable specifically to the given argument.
Thus, there does seem to be an internal distinction between functions and callable objects (functors) that the user cannot access.
Note that rather there is a number of features of struct (named type, supertypes, parameters, explicit fields, mutation) that cannot be replicated with function. These give it a slightly alternate representation for show when those features are eliminated from function, since they are not relevant to the function syntax.
Entire method table for what exactly? The printed methods arenât related to the instance or its type, and the printed number of methods is too low to be all methods ever.
Thanks for the clarification. Do you think there is merit in adding the ability for functions to have custom supertypes (perhaps restricted to those <: Function)? I understand that might not be possible, but I certainly have had spots where Iâve wanted to do this, usually where I want to informally indicate that a function has certain properties and perhaps define methods and control dispatch according to this information.
@jeff.bezanson suggested extending the function syntax to allow this in issue #17162, which might be really nice for these kinds of things.
I like the idea to extend the empty generic function syntax, since it makes clear that the type applies to all methods and avoids the potential accident of trying to assign different types to methods of the same function. My only concern with using :: is that it might look like a return type annotation. The syntax
function foo <: MyFuncType end
is tempting but awkward because foo is not a type. My personal preference might be
function foo isa MyFuncType end
since it mirrors the use of <: in type declarations. As a weird edge case,
function foo isa Any end
if that were allowed, would be basically equivalent to creating a singleton type
struct TypeOfFoo end
const foo = TypeOfFoo()
where the type of foo is âanonymousâ: typeof(foo) would have to return typeof(foo).
Iâm not sure Iâm a fan of that, because thereâs a sort of expectation that something declared with function would subtype Function.
I do like using isa here;
function foo isa MyFuncType end
has a clean feel to it.
What would it be called? Canât call it âtype-assertion,â âtype-annotation,â or âtype-declarationâ because theyâre already used by ::; maybe âtype-proclamation?â
I guess thatâs kind of a hurdle, because we must be able to communicate what it is that weâre doing in natural language, and thatâs starting to get ambiguous. Another hurdle is that conceptually, isa returns a Boolean; :: doesnât, and is therefore already overloaded with a bunch of different meaningsâso adding another meaning here doesnât seem as big a deal.
If we used ::, itâd burn the bridge of being able to declare that function to always return a specific type. I suspect Juliaâs core philosophy of multiple dispatch burned that bridge already, but Iâm not sure if weâd ever want to walk back on it in specific cases.
Maybe a singleton keyword would be more appropriate, working for all cases when a singleton object is needed and it doesnât have to be a subtype of Function.
So essentially, the following expression:
singleton foo isa MyType end
Is the same as:
struct var"##foo#292" <: MyType end
const foo = var"##foo#292"()