Constructors for `<:Function` types when possible, like in C++

I was thinking about how C++20 added support for default constructors for stateless (with no captures) lambdas, meaning that nowadays all function object families may be default-constructible in C++.
Then I realized that in Julia I can’t even do typeof(sqrt)(), or something like this:

func(::F) where {F <: Function} = F()(5.0)
func(sqrt)

# Or this
func(::Type{F}) where {F <: Function} = F()(5.0)
func(typeof(sqrt))

Seems like passing “pure” functions as types, instead of as objects would make sense in some cases (possibly enabling new Julia design patterns), and maybe might even enable additional optimizations when used?

The above would basically enable expressing things like this, but in a prettier and safer way:

func(::Val{f}) where {f} = f(5.0)
func(Val{sqrt}())

Informally, each “stateless” Function type should have a no-argument constructor, on a best-effort basis from the compiler. In @assume_effects terminology, I guess that “stateless” might mean the combination of :notaskstate, :inaccessiblememonly and perhaps :consistent.

Though the distinction between methods and functions in Julia complicates things perhaps, because only methods may be “pure” or “stateless”, it doesn’t make sense to say that a function is “stateless”?

Thoughts?

1 Like

Every ‘singleton’ struct can be constructed from it’s type using T.instance

julia> struct Foo end

julia> Foo.instance
Foo()

julia> typeof(sqrt).instance
sqrt (generic function with 19 methods)

julia> typeof(+).instance
+ (generic function with 206 methods)

I doubt this provides any great opportunities for optimizations though, and it’s generally not a good idea.

This sort

I think you should read that section again. The boxing problem only comes into play if you rebind the captured variable.

3 Likes

I don’t see why, since you can already specialize on the function type, and for singleton types there is not much practical distinction between passing a type and passing the instance.

What, concretely, do you hope to achieve that isn’t possible now?

2 Likes

I only realized that functions are singleton types after writing the post above :sweat_smile:.

Still, even after realizing that functions are singleton types I thought that there might be a certain reason to prefer passing functions as types: the Performance of captured variables issue. I thought that passing functions as types could enable writing more concise code with closures, because let isn’t necessary for type parameters. Long story short, turns out that let is also unnecessary for variables that happen to be of singleton type.

This is how I found this out:

fun_slow(f::F, g::G) where {F <: Function, G <: Function} =
  x ->
    let x::Float64 = x
      function()
        x = f(x)
        g(x)
      end
    end

fun_ugly(f::F, g::G) where {F <: Function, G <: Function} =
  let f = f, g = g
    x ->
      let f = f, g = g, x::Float64 = x
        function()
          x = f(x)
          g(x)
        end
      end
  end

(::Type{F})() where {F <: Function} = F.instance

fun_nice(::F, ::G) where {F <: Function, G <: Function} =
  x ->
    let x::Float64 = x
      function()
        x = F()(x)
        G()(x)
      end
    end

My initial idea was that fun_slow would be very slow and the other two would be tied in speed, but fun_nice is obviously a lot nicer than fun_ugly. After benchmarking with BenchmarkTools.@benchmark it turned out that all three returned functions ran at the same speed.

Good to know that there’s one less case where let is necessary for closures :smile:. Perhaps the Performance tips should be updated, if this behavior is something that can be counted on.

What is the advantage of what you wrote compared to:

fun_nice(f::F, g::G) where {F <: Function, G <: Function} =
  x ->
    let x::Float64 = x
      function()
        x = f(x)
        g(x)
      end
    end

Your fun_nice seems to be exactly my fun_slow. And there’s no advantage, as I said in the previous message. I expected (because of Performance tips) that fun_slow would be slower, but it is not, at least with nightly Julia.

Still, you seem to see some upside since you call it “nice”?

g ∘ f seems even nicer…

5 Likes