Getting the name of a Struct / Scoping Issues with Custom Structs

Hi there,

I have a scoping issues, and did not manage to find a solution on my own. I was hoping that someone more knowledgeable might have a quick solution for this:

  1. I define an abstract type AbstractModel in a module, and a function func1 that works with a sub-type of it.
  2. After loading this module, I define a subtype Model <: AbstractModel. I cannot define this in the module, as I dont know all fields beforehand.
  3. The task of func1 is to output the correct name of Model - here is my problem. Model is not known when I load the module, and I get an error. MWE:
module TestModule
## Define Abstract type
	abstract type AbstractModel end
## What I would Like - Define function that works with Model<:AbstractModel
 	function func1(mod::M) where {M<:AbstractModel}
		eval( nameof( typeof(mod) ) )
	end
## Export everything
    export AbstractModel
    export func1
end
using Main.TestModule

struct Model{A,B} <: AbstractModel
	a :: A
	b :: B
end
mod = Model(1., 2)
func1(mod) #UndefVarError: Model not defined - not defined in Module yet
eval( nameof( typeof(mod) ) ) #But easy to do in global scope...

Unfortunately, I cannot just use eval( typeof(mod) ) (which would work), and I need the struct name, as I may need to initiate a new Model with different field types (Dual numbers instead of Floats). If I define

function func2(mod::M) where {M<:AbstractModel}
	eval( typeof(mod) )
end

in the module, I actually get a valid result func2(mod) #Model{Float64, Int64}. However, I need the name of the struct, not including all the types. Is there any way that I could func1 get to work?

Best regards,

Edit: Alternatively one could easily just get the Symbol :Model, but then it costs much more to initiate such a struct because I have to work with eval(:Model) every time.

eval works within a global scope (each module has its own global scope). Now Model is not defined in the module TestModule, thus the UndefVarError. This works:

module TestModule
## Define Abstract type
	abstract type AbstractModel end
## What I would Like - Define function that works with Model<:AbstractModel
 	function func1(m, mod::M) where {M<:AbstractModel}
	    Base.eval(m, nameof( typeof(mod) ))
	end


## Export everything
    export AbstractModel
    export func1
end
using Main.TestModule

struct Model{A,B} <: AbstractModel
	a :: A
	b :: B
end
mod = Model(1., 2)
func1(@__MODULE__, mod) 

Anyhow, this doesn’t look right to me. eval is bad, eval in another module is worse. As I don’t quite understand your problem, I cannot help though…

1 Like

I certainly could be mis-understanding your MWE, but I don’t think you need eval at all. func1 already knows what M is through the type parameter - you can stringify, construct new values, etc with it.

# inside `TestModule`
function func1(mod::M) where {M<:AbstractModel}
    println("type name is $(string(M))")
    return M(1.0, 3)
end

# outside
julia> func1(mod)
type name is Model{Float64, Int64}
Model{Float64, Int64}(1.0, 3)

Thank you so much for your reply! Your function works perfectly in the example, but I was not explaining it very well, apologies for that. And yes I agree that this approach is not ideal, but for now this would be a quick fix for a problem I am facing.

What I would like to do is the following: starting from a Vector{Float64} θ, create a new TestModel via SuperModel.name(θ...) (MWE below). I have a working version where I only create a mutable TestModel once and inplace replace the fields instead of creating a new Model - here I also do not need to use eval or check for struct names. However, I do have to use Automatic Diff libraries on such structs, so the types for A and B in TestModel{A,B} would be Reals, and thus no fully typed if I wanted an inplace version for this.

Instead of that, I just create new parametric struct where A and B are fully typed (until Zygote is faster or I find an AD library that does not change types of my input) with the corresponding input. This allocates memory but the calculations that I am performing with the fully typed TestModel afterwards are much faster than if I would use a version with not abstract typed fields.

Question 1: I wrote down a more realistic code of my problem below. @__MODULE__ works when I call it within the script, is there something similar if I would call it from within the module as below?

module TestModule
## Define Abstract type
	abstract type AbstractModel end
## Define function that works with Model<:AbstractModel
	function func1(m, mod::M) where {M<:AbstractModel}
		Base.eval(m, nameof( typeof(mod) ) )
	end
## Define Super Model
	struct SuperModel{M<:AbstractModel, B}
		model :: M
		name  :: B
		function SuperModel(model::M) where {M<:AbstractModel}
			name = func1(@__MODULE__, model)
			new{M, typeof(name)}(model, name)
		end
	end
## Export everything
	export func1, AbstractModel, SuperModel
end
using Main.TestModule

struct TestModel{A,B} <: AbstractModel
	a :: A
	b :: B
end
mod = TestModel(1., 2)

#Problem here contintues for me as I would call the function from the module.
SuperModel(mod) #UndefVarError: TestModel not defined

Question 2: Performance wise I was under the impression, that making a new instance of a TestModel was as fast via Supermodel.name as via directly calling TestModel, but somehow the latter approach seems faster here? Maybe I used more fields for my previous benchmarks, or the compiler is too clever here:

using BenchmarkTools
mod 	  = TestModel(randn(5), randn(5))
mod_name  = eval( nameof( typeof( mod ) ) )
a = randn(5)
b = randn(5)
@btime TestModel($a, $b) #1 - 1.100 ns (0 allocations: 0 bytes) Seems wrong
@btime mod_name($a, $b) #2 - 85.595 ns (1 allocation: 32 bytes)
@btime eval( nameof( typeof( mod ) ) )( $a, $b ) #3 - 156.784 ns (1 allocation: 32 bytes)

Thank you for your time! I hope this makes a bit more sense now.

Edit: I can adjust the inner constructor to:

## Define Super Model
	struct SuperModel{M<:AbstractModel, B}
		model :: M
		name  :: B
		function SuperModel(model::M) where {M<:AbstractModel}
			name = func1(Main, model)
			new{M, typeof(name)}(model, name)
		end
	end

to make it work. But func1(Main, model) sounds dangerous even for a temporary fix. Is there a safer variant to remember the Model name in order be able to call TestModel($a, $b) just as fast as I would directly call it?