Function subtypes

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?

You might want to look at the related problem solved by LinearOperators.jl.

1 Like

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?

Yes - I think that’s right.

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.)

1 Like

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.

2 Likes

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.

Right. You would have to refer to specify the type.

julia> (baz42::typeof(baz42))(x) = exp(-x)*baz42.val

julia> baz42(0)
42.0

julia> baz42(-1)
114.1678367952799

julia> baz42(1)
15.450936529200579

julia> baz0 = Baz(0)
Baz(0)

julia> baz0(3)
0.0

You can implement this behavior for any type if you would like. This is based on @less show(stdout, MIME"text/plain"(), foo)

julia> function Base.show(io::IO, ::MIME"text/plain", f::Bar)
           get(io, :compact, false)::Bool && return show(io, f)
           ft = typeof(f)
           mt = ft.name.mt
           if isa(f, Core.IntrinsicFunction)
               print(io, f)
               id = Core.Intrinsics.bitcast(Int32, f)
               print(io, " (intrinsic function #$id)")
           elseif isa(f, Core.Builtin)
               print(io, mt.name, " (built-in function)")
           else
               name = mt.name
               isself = isdefined(ft.name.module, name) &&
                        ft == typeof(getfield(ft.name.module, name))
               n = length(methods(f))
               m = n==1 ? "method" : "methods"
               sname = string(name)
               ns = (isself || '#' in sname) ? sname : string("(::", ft, ")")
               what = startswith(ns, '@') ? "macro" : "generic function"
               print(io, ns, " (", what, " with $n $m)")
           end
       end

julia> bar
(::Bar) (generic function with 2 methods)

julia> bar(x,y,z) = z*cos(x+y)
(::Bar) (generic function with 3 methods)

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.)

3 Likes

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.

1 Like

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.

1 Like

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.

1 Like

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.

1 Like

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.

This appears to be free syntax for a typed function declaration:

function foo::MyFuncType end
2 Likes

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).

2 Likes

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.

This is already the case for functions that subtype Function; their type is var"#foo".

1 Like

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"()