Extract function from function type

Consider the following:

foo(x) = x
foo_type = foo |> typeof

# Don't know how to implement but I know what I want it to do
# Example: default(foo_type) -> foo
default(::Type{T}) where T <: Function = ???

# The following code works but is not generic
default(::Type{foo_type}) = foo

How can I implement the first version of default that is generic?

Basically my function default takes a type and returns a default value of said type which I use in combination with a Bool to mimic uninitialized values. I want to be able to do the same with functions.

I am aware of missing and nothing. And I am also aware that union splitting removes the performance impact from the type instability they cause. However, in my specific use case I am not interested in that as a solution.

I am also aware that I could use a macro to add the hard coded version of the function automatically. But I use macros only as a last resort.

Here you go:

The second hardcoded version works even if there are multiple instances of the function.

Namely I am not looking to get back a specific method of the function but rather the function itself which does have a 1 to 1 relationship with the type

I wasn’t talking about methods, I was talking about functions themselves. One function type can have multiple instances in the case of things like closures.

julia> f = let x = 1
           y -> x + y
#5 (generic function with 1 method)

julia> dump(f)
#5 (function of type var"#5#6"{Int64})
  x: Int64 1

julia> f2 = eval(Expr(:new, typeof(f), 2))
#5 (generic function with 1 method)

julia> dump(f2)
#5 (function of type var"#5#6"{Int64})
  x: Int64 2
julia> f2(1)

julia> f(1)

Most importantly here,

julia> typeof(f2) == typeof(f)

julia> f2 == f

This is because the way we implement closures is that they’re really structs carrying data representing the closed over variables, so they’re not uniquely defined by their type alone.

Notice here that both f and f2 have only one method in my example, so this is completely orthogonal to the question of how many methods a function has.

As I said in the linked discussion though, if you want to ignore those corner cases, you can write something like

function default(::Type{ft}) where {ft <: Function}
    if isdefined(ft, :instance)
        error("This type does not have a single instance")

which will work for most functions, but can definitely fail for the reasons I’ve described.

Ah I see. Sorry I thought methods and instances were the same, but thank you for correcting me on that.

I think I should be fine not being able to handle that corner case. Thanks

Actually a follow up. In my case I just need a value of the type. In other words it does not have to be any particular instance, just an instance.

So even if there is not a 1-1 matching that would be fine for me. Is there a way to get an instance regardless of whether it is a single instance or not?

@HashBrown Do you mind if I ask what are you actually trying to do here? It may be that there’s a smarter way to get what you want here…

Regardless though, yes, it is in fact possible for you to get a instance of any type (including functions) which uninitialized values in any slots. Doing this requires mucking around with some internals, and this can get dangerous. Here’s how I’d do it:

@generated function new(::Type{T}, args...) where {T}
    Expr(:new, T, (:(args[$i]) for i ∈ 1:length(args))...)

any_instance(::Type{T}) where {T} = new(T);

For single instance types, this just gives you the type:

julia> any_instance(Missing)

julia> any_instance(typeof(sin))
sin (generic function with 14 methods)

For structs and whatever that only store inline bits, it’ll give you garbled random bits:

julia> any_instance(Int)

julia> f = let x = 1
           y -> x + y
#8 (generic function with 1 method)

julia> any_instance(typeof(f)) |> dump
#8 (function of type var"#8#9"{Int64})
  x: Int64 139734222465888

julia> any_instance(typeof(f))(1)

And for structs which are storing pointer-backed types, this gives undefined references (which at least are slightly safer than null pointers)

julia> struct Foo

julia> let foo = any_instance(Foo)
  x: #undef

julia> let foo = any_instance(Foo)
ERROR: UndefRefError: access to undefined reference
 [1] getproperty(x::Foo, f::Symbol)
   @ Base ./Base.jl:38
 [2] top-level scope
   @ REPL[5]:2

Again though, I want to reiterate that I suspect that this isn’t actually the right path, and if you tell us what you’re actually trying to achieve we can probably give a better suggestion.

I noticed that any_instance(Tuple{Int64, String}) is crashing Julia. Is that expected?

As for what I am working on, it is rather hard to explain. But basically I am creating a bunch of const Ref types in global scope and functions that control access to them through metaprogramming (another thing I am sure you will advise me against :wink:). I need to initialize them with something and for the sake of type stability and ease in other ways I don’t want to use missing.

Also on the implementation of new I am not really following what args... is doing.

When I write:

struct Foo

Expr(:new, Foo) |> eval # ERROR: invalid struct allocation

Which to me seems like it should work based on your function definitions.

Obviously the following works:

Expr(:new, Foo, Expr(:new, Int64)) |> eval

but I don’t see how that is analogous to the code you posted.

It’s not crashing julia. Trying to diplay it in the REPL is crashing julia.

julia> let x = any_instance(Tuple{Int, String})
Tuple{Int64, String}
  1: Int64 0
  2: #undef

That’s expected yes, because as I explained above, this will end up with an undefined reference, and accessing undefined references isn’t allowed.

1 Like

I edited the above to include a follow up question. Sorry, I should have posted it as a reply, but I think I edited it while you were responding.

Yeah, for some reason Expr(:new) works differently in a generated function than in macros or eval. I believe this is because the lowering process adds a hook to try and protect users from making an instance of a struct with undefined fields, and maybe there’s a missing branch in the generated function lowering that lets us evade this restriction. Not sure.

But either way, compare:

julia> @generated foo() = Expr(:new, Foo);

julia> foo()

julia> macro foo()
           Expr(:new, Foo)
@foo (macro with 1 method)

julia> @foo()
ERROR: invalid struct allocation
 [1] top-level scope
   @ REPL[6]:1

Very annoying, since generated functions cause a lot more compilation overhead than macros.

1 Like

Huh, interesting. Thanks!

Also, I should note that using new in an inner constructor (i.e. the only place it’s really meant to be used) works like how generated functions work, not like how eval or macros work:

julia> struct Foo
           Foo() = new()

julia> Foo()

1 Like