Function subtypes

According to the somewhat dated discussion in this bug report, the only way to create a function whose supertype is a custom subtype of Function is to create a new singleton type, and then define methods on its sole instance.

# Boiler plate to make a "function" that is <: MyFunction
abstract type MyFunction <: Function end
struct _typeof_f <: MyFunction end
const f = _typeof_f()

# Now define some methods for f
function f(args...)
   ...
end
...

The boilerplate is annoying and the Julia LSP (using Emacs + LSP mode) complains at every method definition, ā€œCannot define function; it already has a valueā€. (Though the code does work as expected, so maybe this is a bug/limitation of the language server.)

In any case, I am wondering if there is a better way to do things. For normal functions, I assume that something like the above boilerplate somehow happens automatically given the description in Juliaā€™s documentation. However, Iā€™m not sure if the machinery that does this is available to the user or if it could be emulated with a macro.

My use case here is that I want to ā€œtagā€ functions (by making them a special subtype of Function) as being members of a (linear) function space and define methods for addition and scalar multiplication directly on the functions themselves as a matter of convenience. Thus, one would have (2f + g)(x) == 2f(x) + g(x) where f and g are both instances of the custom subtype.

2 Likes

In your example it seems that itā€™s necessary to define f as an alias name for the _typeof_f() function to be able to define a method for the function.

I wonder whether thereā€™s a way to define a method without giving the function a name, that would reduce the boilerplate.

1 Like

Have you considered using something like Union{typeof(f), typeof(g)} instead of MyFunction?

You do not have to subtype Function. You do not even have to instantiate the type. You can turn the type directly into a functor, a function-like object.

julia> struct MyFunction end

julia> (::Type{MyFunction})(x) = x + 3

julia> (::Type{MyFunction})(x, y) = x + y

julia> MyFunction(2)
5

julia> MyFunction(2, 8)
10

julia> struct MyParameterizedFunction{A} end

julia> (::Type{MyParameterizedFunction{N}})(x, y) where N = x + y * N

julia> MyParameterizedFunction{3}(2,3)
11

Taking this further, you can do the following.

julia> abstract type MyAbstractFunction end

julia> struct MyConcreteFunction <: MyAbstractFunction end

julia> MyConcreteFunction(x) = x*2
MyConcreteFunction

julia> MyConcreteFunction <: MyAbstractFunction
true

julia> MyConcreteFunction(4)
8
7 Likes

The ā€œtaking this furtherā€ part is probably bad advice, because, stylistically, a constructor for some type should return an instance of that type.

Also relevant:

1 Like

This is unfortunate:

julia> abstract type MyAbstractFunction <: Function end

julia> struct MyConcreteFunction <: MyAbstractFunction end

julia> MyConcreteFunction(x) = x*2
MyConcreteFunction

julia> MyConcreteFunction(2)
4

julia> MyConcreteFunction isa Function
false

julia> MyConcreteFunction isa MyAbstractFunction
false
1 Like

This not how you make a callable object / functor ā€” youā€™ve just defined a constructor, not a call method. A functor is a callable object which means you need to define how to call an instance, not the type itself.

For example:

julia> abstract type MyAbstractFunction <: Function end

julia> struct MyConcreteFunction <: MyAbstractFunction end

julia> (::MyConcreteFunction)(x) = x*2

julia> f = MyConcreteFunction() # create an instance
(::MyConcreteFunction) (generic function with 1 method)

julia> f(3)
6

julia> f isa Function
true

julia> f isa MyAbstractFunction
true

As a more practical example, consider a polynomial functor type, where instances represent particular coefficients:

julia> struct Poly{T} <: Function
           coeffs::Vector{T}
       end

julia> (p::Poly)(x) = evalpoly(x, p.coeffs)

julia> p = Poly([3,4,5]) # call the constructor
Poly{Int64}([3, 4, 5])

julia> p(7) # call the instance
276
14 Likes

The first and second part are actually the same in this regard. In both cases, Iā€™m overriding the constructor and not returning an instance of the type. If the second part is bad advice, then both parts are bad advice. My motive here is to eliminate ā€œannoyingā€ boiler plate, and technically the syntax actually allows for this. The primary issue is likely a language server bug.

julia> struct MyFunction end

julia> (::Type{MyFunction})(x) = x + 3

julia> MyFunction(2)
5

julia> MyFunction(x) = 2x
MyFunction

julia> MyFunction(2)
4

julia> MyFunction isa DataType
true

Regarding Issue 42372, the other take away from this is that the behavior Iā€™m exploiting is actually supported in Julia 1.x. The developers have also emphasized strongly that there are no plans for Julia 2.x at the moment.

Technically, I am calling an instance. Itā€™s just an instance of DataType. The OP does not actually need any fields or captured variables.

However, I do think exploiting the constructor is distracting from a few points. Let me emphasize that subtyping Function is not necessary. You can make the instance callable without subtyping Function.

abstract type MyAbstractFunction end
struct MyFunction <: MyAbstractFunction end
(::MyFunction)(x) = x - 1

julia> MyFunction()(2)
1

The main advantage that subtyping Function provides is that you can pass your type to functions that accept a subtype of Function.

Hereā€™s one approach:

abstract type LinearFunction <: Function end
struct ConcreteLinearFunction{F <: Function} <: LinearFunction
    func::F
end
const CLF = ConcreteLinearFunction
(clf::CLF)(x...) = clf.func(x...)

import Base: *, +
n::Number * clf::CLF = CLF(x->n*clf(x))
a::CLF + b::CLF = CLF(x->a(x)+b(x))

You can then do the following:

julia> const f = CLF(x->2x)
(::ConcreteLinearFunction{var"#5#6"}) (generic function with 1 method)

julia> const g = CLF(x->3x)
(::ConcreteLinearFunction{var"#7#8"}) (generic function with 1 method)

julia> (2f + g)(3) == 2f(3) + g(3)
true

julia> f(3)
6

julia> g(3)
9

julia> (2f)(3)
12

julia> f(6)
12

julia> (f + g)(3)
15

julia> (2f + g)(3)
21

1 Like

Youā€™re just confusing isa for <:. isa on objects, <: on types.

2 Likes

Thanks for the additional suggestion - I hadnā€™t thought of that. For what I am envisioning, though, I think that this approach may be hard to extend because you (or users of a package) wouldnā€™t be able to add new types to the original Union{ā€¦}.

With regard to (ab)using constructors, I did think of that possibility, but I decided against it due to reasons mentioned by others in this thread.

The suggestion to create a wrapper type is a good one. In fact, it is the first approach I took. However, I ran into two small issues:

  1. Defining the functions is slightly awkward ā€” you can wrap anonymous functions, use do syntax on the constructor, or wrap an existing regular function
  2. Defining new methods is also awkward ā€” AFAIK, you end up having to keep around a (named) unwrapped function to add new methods

Despite the boilerplate, I am now thinking that my original approach is probably best. Thatā€™s because

  1. It follows Juliaā€™s model for regular functions most closely
  2. You can define new methods for subtyped functions as usual, e.g., using function ... end.

The LSPā€™s complaints are annoying but hopefully could be fixed.

Iā€™ll try writing a macro to simplify things and post whatever I come up with here if I am successfulā€¦ Anything more than simple macros seem to turn into day-long projects for me :weary:. Iā€™ll just need to figure out away to automate the naming, creation, and instantiation of a singleton type - hopefully not too challenging.

Still, it would be nice to see dedicated syntax for this sort of thing, e.g.,

function myfunc(args...) <: MyFuncType
    ...
end

as suggested in Issue 17162, which could be done when a function is first defined. It appeared there was some support for this, but the issue was eventually closed because of the possibility of defining singleton types manually, as done here.

See these discussion: Shorthand syntax for defining functors - Internals & Design - Julia Programming Language (julialang.org)

Iā€™ve actually have written a macro to do this before:

julia> macro singleton(expr)
           expr = expr::Expr
           @assert Meta.isexpr(expr, :call) && first(expr.args) == :isa "Not an `isa` expression"

           _, name, supertype = expr.args
           typename = gensym(name)

           return quote
               struct $typename <: $(esc(supertype)) end
               const $(esc(name)) = $typename()
               Base.@__doc__($(esc(name)))
           end
       end
@singleton (macro with 1 method)

julia> abstract type MyType <: Function end

julia> begin
           """
           Singleton object `foo` acts as a function with special properties...
           """
           @singleton foo isa MyType
       end
foo

julia> foo() = println("Hello world!")
(::var"##foo#292") (generic function with 1 method)

julia> foo(name) = println("Hello $(name)!")
(::var"##foo#292") (generic function with 2 methods)

julia> foo()
Hello world!

julia> foo("Steve")
Hello Steve!

julia> foo isa MyType
true

julia> foo isa Function
true

help?> foo
search: foo floor pointer_from_objref OverflowError RoundFromZero unsafe_copyto! functionloc StackOverflowError

  Singleton object foo acts as a function with special properties...

Edit:

  • Use isa instead of <:
  • Removed Base.show definition
  • Add support for docstrings with Base.@__doc__
2 Likes

Yes, thatā€™s the point. Because !isa(MyConcreteFunction, Function), itā€™s impossible to pass MyConcreteFunction to methods specialized on ::Function arguments, which is the whole point of subtyping Function to begin with.

Unless such functions can also be expected to accept ::Type{<:Function} arguments but I donā€™t think thatā€™s the case.

You pass MyConcreteFunction(), i.e. an instance, which is a subtype of Function.

You wouldnā€™t pass Function to a method expecting ::Function, or Int to a function expecting ::Integer, after all.

3 Likes

YeahYesGIF

Here is another approach with a bit of type piracy.

julia> islinear(::Function) = false
islinear (generic function with 1 method)

julia> function Base.:*(n::Number, f::Function)
           if islinear(f)
               h = (x...) -> 2 * f(x...)
               @eval islinear(::typeof($h)) = true
               return h
           else
               throw(MethodError(*, (n,f)))
           end
       end

julia> function Base.:+(a::Function, b::Function)
           if islinear(a) && islinear(b)
               h = (x...) -> a(x...) + b(x...)
               @eval islinear(::typeof($h)) = true
               return h
           else
               throw(MethodError(+, (a,b)))
           end
       end

julia> f(x) = 2x
f (generic function with 1 method)

julia> g(x) = 3x
g (generic function with 1 method)

julia> f + g
ERROR: MethodError: no method matching +(::typeof(f), ::typeof(g))
Closest candidates are:
  +(::Function, ::Function) at REPL[3]:1
  +(::Any, ::Any, ::Any, ::Any...) at operators.jl:591
Stacktrace:
 [1] +(a::Function, b::Function)
   @ Main ./REPL[3]:7
 [2] top-level scope
   @ REPL[6]:1

julia> islinear(::typeof(f)) = true
islinear (generic function with 2 methods)

julia> islinear(::typeof(g)) = true
islinear (generic function with 3 methods)

julia> f + g
#3 (generic function with 1 method)

julia> h = f + g
#3 (generic function with 1 method)

julia> h(1)
5

julia> h(2)
10

julia> f + h
#3 (generic function with 1 method)

julia> islinear(ans)
true

The question then becomes how to avoid the type piracy. You could make your own + and * in a module:

julia> module LinearFunctions
           islinear(::Function) = false
           function +(a::Function, b::Function)
                  if islinear(a) && islinear(b)
                      h = (x...) -> Base.:+(a(x...),b(x...))
                      @eval islinear(::typeof($h)) = true
                      return h
                  else
                      throw(MethodError(+, (a,b)))
                  end
              end
           function *(n::Number, f::Function)
                  if islinear(f)
                      h = (x...) -> Base.:*(2,f(x...))
                      @eval islinear(::typeof($h)) = true
                      return h
                  else
                      throw(MethodError(*, (n,f)))
                  end
              end
       end
Main.LinearFunctions

julia> f(x) = 2x
f (generic function with 1 method)

julia> g(x) = 3x
g (generic function with 1 method)

julia> f(1)
2

julia> g(2)
6

julia> import .LinearFunctions: islinear, +, *

julia> h = f + g
ERROR: MethodError: no method matching +(::typeof(f), ::typeof(g))
Closest candidates are:
  +(::Function, ::Function) at REPL[1]:3
Stacktrace:
 [1] +(a::Function, b::Function)
   @ Main.LinearFunctions ./REPL[1]:9
 [2] top-level scope
   @ REPL[5]:1

julia> islinear(::typeof(f)) = true
islinear (generic function with 2 methods)

julia> islinear(::typeof(g)) = true
islinear (generic function with 3 methods)

julia> h = f + g
#1 (generic function with 1 method)

julia> islinear(h)
true

julia> 1 + 2
ERROR: MethodError: no method matching +(::Int64, ::Int64)
You may have intended to import Base.:+
Stacktrace:
 [1] top-level scope
   @ REPL[10]:1

julia> h(2)
10

One way around this would be to use a macro. Hereā€™s a quick version.

julia> module LinearFunctions
                  islinear(::Function) = false
                  function +(a::Function, b::Function)
                         if islinear(a) && islinear(b)
                             h = (x...) -> Base.:+(a(x...),b(x...))
                             @eval islinear(::typeof($h)) = true
                             return h
                         else
                             throw(MethodError(+, (a,b)))
                         end
                     end
                  function *(n::Number, f::Function)
                         if islinear(f)
                             h = (x...) -> Base.:*(n,f(x...))
                             @eval islinear(::typeof($h)) = true
                             return h
                         else
                             throw(MethodError(*, (n,f)))
                         end
                     end
              macro linear(e)
                  if e.head == :call && e.args[1] āˆˆ (:+, :*)
                      e.args[1] = :(LinearFunctions.$(e.args[1]))
                      return esc(e)
                  elseif e.head == :(=) && e.args[1].head == :call
                       func = esc(e.args[1].args[1])
                       e = esc(e)
                       quote
                           $e
                           LinearFunctions.islinear(::typeof($func)) = true
                           $func
                       end
                  else
                      return e
                  end
              end
           end
Main.LinearFunctions

julia> import .LinearFunctions: islinear, @linear

julia> @linear f(x) = 2x
f (generic function with 1 method)

julia> @linear g(x) = 3x
g (generic function with 1 method)

julia> islinear(f)
true

julia> islinear(g)
true

julia> foo = @linear f + g
#1 (generic function with 1 method)

julia> islinear(foo)
true

julia> foo(1)
5

julia> foo(2)
10

julia> h = @linear (@linear 2f) + g
#1 (generic function with 1 method)

julia> h(1)
7

julia> 1 + 2
3
1 Like

Can you give a hint of the use case for this? I donā€™t see Function used often, and for good reason. Normally people duck type callables in interfaces to my knowledge?

I think the use case here would help. Is there a particular package or interface you are planning to use which requires that for dispatching? If so, maybe fixing that signature (eg, usually just removing unnecessary constraints) solves the problem.

This also seems like a classic case for Traits in one form or another since being accessible as a function is often orthogonal to other things you might want to dispatch with for your types. Without built-in traits support in Julia, duck typing is often the way to go.

Ha fair, I never questioned the legitimacy of the OPā€™s desire to subtype Function.

Supposing that the OPā€™s reasons are legitimate, theyā€™re certainly for dispatch.

Hereā€™s an example of methods specializing on ::Function:

julia> methods(open)
# 11 methods for generic function "open":
 ā‹®
[5] open(f::Function, cmds::Base.AbstractCmd, args...; kwargs...) in Base at process.jl:414
[6] open(f::Function, args...; kwargs...) in Base at io.jl:381
 ā‹®

Why go to such lengths to circumvent an amazing type system?

julia> abstract type AbstractLinearFunction <: Function end

julia> struct Foo <: AbstractLinearFunction end

julia> const foo = Foo()
(::Foo) (generic function with 0 methods)

julia> foo(x) = 2x
foo (generic function with 1 method)

julia> foo(x, y) = 2x + 3y
foo (generic function with 2 methods)

julia> foo(2)
4

julia> foo(2, 3)
13

julia> foo isa Function && foo isa AbstractLinearFunction
true

Depending how crazy you want to getā€¦

julia> struct LinearFunction{A} <: AbstractLinearFunction a::A end

julia> (lf::LinearFunction)(x) = lf.a * x

julia> const id2d = LinearFunction([1 0; 0 1])
(::LinearFunction{Matrix{Int64}}) (generic function with 1 method)

julia> id2d([1, 2])
2-element Vector{Int64}:
 1
 2

julia> using Polynomials

julia> const integrator = LinearFunction(1 // Polynomial((0, 1), :s))
(::LinearFunction{RationalFunction{Int64, :s, Polynomial{Int64, :s}}}) (generic function with 1 method)

julia> (lf::LinearFunction{<:Function})(x) = lf.a(x)

julia> (lf::LinearFunction{<:Polynomials.AbstractRationalFunction})(x) = lf.a(x) 

julia> # because RationalFunction doesn't subtype Function lolwtf

julia> let Ļ‰=2Ļ€*10; integrator(im*Ļ‰) end
0.0 - 0.015915494309189534im

julia> integrator isa Function
true

julia> scale(Ī½) = Base.Fix2(*, Ī½)
scale (generic function with 1 method)

julia> Base.:*(f::AbstractLinearFunction, k::Number) = LinearFunction(scale(k) āˆ˜ f)

julia> Base.:*(k::Number, f::AbstractLinearFunction) = LinearFunction(scale(k) āˆ˜ f)

julia> let Ļ‰=2Ļ€*10; (10integrator)(im*Ļ‰) end
0.0 - 0.15915494309189535im

julia> 10integrator isa LinearFunction &&
           10integrator isa AbstractLinearFunction &&
           10integrator isa Function
true

I essentially implemented an islinear trait above.

That amazing type system lacks multiple inheritance for better or for worse. The thought of multiple inheritance and multiple dispatch at the same time gives me a headache, so itā€™s probably for the better.

However, we clearly have functions such as a identity, which are linear functionals, and already belong to a type hierarchy that we cannot change. While we could wrap the function up as you suggest, perhaps we could just add information to the type via islinear(::typeof(identity)) = true or alternatively islinear(::Type{typeof(identity)}) = true

Note that we could dispatch on this as well:

foobar(f::Function) = foobar(f, Val(islinear(f)))
foobar(f::Function, linear::Val{true}) = do_with_linear_function(f)
foobar(f::Function, linear::Val{false}) = do_with_nonlinear_function(f)

The trait pattern is just another way of using of the type system. We could replace Val with something more formal as Tom Kwong details here about the Holy Trait Pattern:
https://ahsmart.com/pub/holy-traits-design-patterns-and-best-practice-book/#identifying_traits

Thanks, @dylanxyz. The thread you linked to answers some questions for me. Itā€™s interesting that you can define methods for any instance of a singleton type using the function block syntax.

I also appreciate the macro exampleā€”I think thatā€™s pretty much as convenient as one can get without adding some built-in syntactic sugar to the language.

1 Like