Shorthand syntax for defining functors

Currently, to define a function with a custom type in julia, we use functors:

julia> struct MyType <: Function end

julia> (::MyType)() = println("Hello world!")

julia> (::MyType)(name) = println("Hello $(name)!")

julia> MyType()()
Hello world!

julia> MyType()("Steve")
Hello Steve!

But it would be cool if i could do something like:

julia> abstract type MyType <: Function end

julia> function foo <: MyType end
foo (generic function with 0 methods)

julia> foo() = println("Hello world!")
foo (generic function with 1 methods)

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

julia> foo()
Hello world!

julia> foo("Steve")
Hello Steve!

julia> foo isa MyType
true

Which should be equivalent to:

abstract type MyType <: Function end

struct Foo <: MyType end

const foo = Foo()

(::Foo)() = println("Hello world!")

(::Foo)(name) = println("Hello $(name)!")

What do you think about this syntax?

The question to begin with is “why”?

1 Like

Considering that you’re making a singleton type with a const name for its only callable instance, it seems like you just want to declare a supertype of a function’s type. Such a declaration would be error-prone in multiple method definitions e.g. different methods that declare different supertypes, so we would need a dedicated singular block for it, a lot like a functor’s struct block.

I can’t speak for OP, but I might use it for dispatch in higher-order methods, like a method that only considers the functions that subtype a particular supertype. I’ve done it for some functors, but never a singleton type like a function’s. <:Function had always been good enough for functions for me, since dispatching on arguments is necessary to narrow down to methods.

1 Like

Right, wouldn’t then this be served equally well by:


struct MyType <: Function end

(::MyType)() = println("Hello world!")

(::MyType)(name) = println("Hello $(name)!")

foo = MyType()

foo()

foo("Steve")
1 Like

It’s what I’d do, but perhaps with an extra const foo = MyType().

For my specific use case, i have some callable objects that acts as events that the application will dispatch (call) in specific situations, something like configure, setup, dispose, etc. I need to perform different actions based on the types of the event being fired. For example, for setup, i do some initialization and dispose cleaning up things. I also wanna group these events with specific types. For example, configure, setup and dispose should be subtype of LifeCycleEvent, and mouse events such as mousepress and mousedrag should be subtype of MouseEvent, and the same for the keyboard events and etc.

Of course, the solution is to define singleton functors:

abstract type Event <: Function end

abstract type LifeCycleEvent <: Event end
abstract type MouseEvent <: Event end
abstract type KeyboardEvent <: Event end

struct SetupEvent <: LifeCycleEvent end
struct ConfigureEvent <: LifeCycleEvent end
struct DisposeEvent <: LifeCycleEvent end

struct MousePressEvent <: MouseEvent end
struct MouseDragEvent <: MouseDrag end
struct KeyPressEvent <: KeyboardEvent end
struct KeyInputEvent <: KeyboardEvent end

const setup = SetupEvent()
const configure = ConfigureEvent()
const dispose = DisposeEvent()

Then, implementing each event:

function (::SetupEvent)()
    @info "Starting..."
end

function (::DisposeEvent)()
    @info "Closing..."
end

# ... etc

With this, i can create a function called dispatch that acts different based on the event
that is being fired:

function dispatch(event::Event, args...)
    if applicable(event, args...)
        # fire the event
        event(args...)
    end
end

function dispatch(event::SetupEvent, args...)
    # do something before the app starts
end

function dispatch(event::DisposeEvent, args...)
    # do something before the app closes
end

I just think that the syntax (::MyType)() = ... or function (::MyType)() end is a little bit unintuitive, specially for people still learning julia. Instead, if i could just write:

function myfunction <: MyType end

And then defining the methods later could be more easier to understand.

But of course, that is specific to my use case. I don’t know how many people would actually use this.

As I said, function blocks are just terrible alternatives for struct blocks because there can be multiple method definitions each with their own declared supertype.

That’s true, and it’s one of my criticisms of the documentation. Functors are written like it’s some weird minor feature, but it’s really how functions work and should be explained from the get-go. Really, function blocks are syntactic sugar that omits the type definition for a function: unannotated names e.g. function foo end are made const and are assigned the callable instance instead of the type typeof(foo), which is always a singleton subtype of Function. A function call foo(a, b) dispatches on the types of foo, a, b just like functor calls.

2 Likes

Just to be clear, is not like want to change the syntax for defining functors, i just want an alternative way to define them.

That’s exactly what function-blocks with unannotated names are, and for good reason they are locked into an implicit struct ___ <: Function end. But if you really do want to risk declaring supertypes in function-blocks, maybe you can pull it off with a macro? That seems difficult though, and I’m not even sure if it’s possible to make one that works perfectly.

1 Like

The macro is actually fairly simple to implement:

julia> macro singleton(expr)
           @assert Meta.isexpr(expr, :(<:))
           name, supertype = expr.args
           typename = gensym(name)
           quote
               struct $typename <: $(esc(supertype)) end
               const $(esc(name)) = $typename()
           end
       end
@singleton (macro with 1 method)

julia> abstract type MyType <: Function end

julia> @singleton foo <: MyType
(::var"##foo#312") (generic function with 0 methods)

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

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

julia> foo()
Hello world!

julia> foo("Steve")
Hello Steve!

julia> foo isa MyType
true

I just don’t know how to properly name it yet.

1 Like

Moving the supertype declaration out of the function blocks does solve the issue. Only suggestion I could have is defining Base.show for your hidden type to print the name directly, I think you can interpolate a Quotenode(name) into that. But how did you turn foo into a generic function name? I see it printed as such, but I can’t replicate it with struct _Bar end; const bar = _Bar(); bar() = 0. EDIT: nvm, struct _Bar <: Function end does it, there must be a isa Function check somewhere.

Also it appears that const doesn’t always make a name accept new methods. For example, const x = 1.0im; x() = 0 throws a ERROR: cannot define function x; it already has a value, and I know complex numbers are struct types, not primitive types.

1 Like

I honestly can’t figure out why this works:

julia> struct Foo end

julia> const foo = Foo()
Foo()

julia> foo() = 0
Foo()

julia> foo(n) = n - 2
Foo()

julia> foo()
0

julia> foo(3)
1

julia> foo isa Foo
true

And this doesn’t:

julia> const bar = Complex(0)
0 + 0im

julia> bar() = 0
ERROR: cannot define function bar; it already has a value
Stacktrace:
 [1] top-level scope
   @ none:0
 [2] top-level scope
   @ REPL[12]:1
2 Likes

Using your advice to define custom Base.show method for the hidden type, the macro is now:

julia> macro singleton(expr)
           @assert Meta.isexpr(expr, :(<:))
           name, supertype = expr.args
           typename = gensym(name)
           quote
               struct $typename <: $(esc(supertype)) end

               function Base.show(io::IO, f::$(typename))
                   name = $(QuoteNode(name))
                   type = supertype(typeof(f))
                   print(io, "singleton function $name (subtype of $type)")
               end

               const $(esc(name)) = $typename()
           end
       end
@singleton (macro with 1 method)

The problem is that if the supertype is subtype of Function, the printing doesn’t work:

ulia> abstract type MyType <: Function end

julia> @singleton foo <: MyType
(::var"##foo#312") (generic function with 0 methods)

julia> foo
(::var"##foo#312") (generic function with 0 methods)

But it works ortherwise:

julia> abstract type Bar end

julia> @singleton bar <: Bar
singleton function bar (subtype of Bar)

julia> bar
singleton function bar (subtype of Bar)

I’m not sure why. Maybe functions are printed differently?

2 Likes
  1. It works if you do function Base.show(io::IO, ::MIME"text/plain", f::$(typename)) instead. I also still don’t understand the docs for Base.show saying what the difference is. I think it may have run into a preexisting method with such a signature for ::Function like this one for AbstractArray, but the linked docs isn’t helpful at all.

  2. Try to avoid lines like name = $(QuoteNode(name)), use a different name like name2 so the later interpolation $name2 is unambiguous.

  3. I intended for you to interpolate a symbol into an expression so that it remains a symbol rather than become a variable, like print(io, "singleton function ", $(QuoteNode(name)), " (subtype of $type)"). You instead did the equivalent of print(io, "singleton function $($(QuoteNode(name))) (subtype of $type)"). I’m actually not sure how the latter even works, I was under the impression that interpolation in strings in expressions is unfeasible in macros, e.g. :("This is $x") is considered to be an expression containing a String interpolation, so it is evaluated as "This is $x" outside the macro and fails.

2 Likes

I tracked the error printout to a part of methods.c, and I can’t read C so I don’t really know how this works. But I did figure out that struct _Bar x::Int end; const bar = _Bar(1); bar() = 0 throws the error, so that part probably does some check of whether the struct contains any data. After all, a multimethod-like instance can only be safely given the const name instead of the type if the type is singleton. So struct Foo end; struct _Bar x::Nothing; y::Foo end; const bar = _Bar(nothing, Foo()); bar() = 0 works.

I have a hard time wrapping my head around this:

julia> struct _Bar x::Nothing; end; const bar = _Bar(nothing); bar() = 0                                    
_Bar(nothing)                                                                                               
                                                                                                            
julia> struct _Bar2 x::Int; end; const bar2 = _Bar2(13); bar2() = 0                                         
ERROR: cannot define function bar2; it already has a value                                                  
Stacktrace:                                                                                                 
 [1] top-level scope                                                                                        
   @ none:0                                                                                                 
 [2] top-level scope                                                                                        
   @ REPL[2]:1                                                                                              
                                                                                                            
julia> 

Why does it work in the first case?

1 Like

I think it’s because you don’t need any data for singleton fields like Nothing. sizeof(_Bar) returns 0. I think it’s because it is a singleton type, specifically that Base.issingletontype(_Bar) is true; the current implementation also assigns the sole instance to _Bar.instance. It makes sense that a singleton type must be immutable (mutable type can have 2 different instances of same value) and have either no fields or only singleton fields like ::Nothing. I think the methods.c code checks for that somehow, not sure because I can’t read C. Incidentally, I love how we’re just breaking down how functions are special cases of functors.

3 Likes