Can I make type alias show up in a stack trace?

I’ve seen a lot of style guides that suggest being sparing with closures. Which is concerning, because I use a ton of closures. Part of the motivation for that advice seems to be because stack traces are hard to learn from when they have elements on [5] (::var"#1#2")(::Float64) or whatever in them.

I unfortunately need to make a lot of closures that are not at top level scope, so it isn’t super easy to make a normal struct and then dynamically add methods to it. But even inside my function bodies I can of course make anonymous structs and give them methods, like this:

test = (a=1, b="yes", c=1.0, f=log)
const MyStruct = typeof(test)
eval_f(s::MyStruct, x) = s.f(x)

I’m hoping that this may at least be used to get more information in stack traces. But when I call eval_f(test, -1.0), my stacktrace gives me:

ERROR: DomainError with -1.0:
log will only return a [...].
Stacktrace:
[...]
 [4] eval_f(s::NamedTuple{(:a, :b, :c, :f), Tuple{Int64, String, Float64, typeof(log)}}, x::Float64)
   @ Main ./REPL[4]:1
[...]

Is there any way for me to make this line in the stacktrace instead say eval_f(s::MyStruct, x::Float64) for methods and type aliases that are not created at top level? It seems sort of unlikely, but I thought I’d ask.

1 Like

No, because it’s not really an alias, as it would be in Haskell. MyStruct is a constant value, like a normal variable, that evaluates at function definition to NamedTuple{...etc}, so julia isn’t aware of MyStruct any more when it’s defining the method eval_f, or when it’s printing the stack trace.

I think the closest you can get is:

Base.@kwdef struct MyStruct{A,B,C,F}; a::A; b::B; c::C; f::F; end
eval_f(s::MyStruct, x) = ...

# or, to be able to call it using the syntax s(x)
# which you can't do with a NamedTuple

function (s::MyStruct)(x)
    s.f(x)
end

I haven’t run this example, it might be a little bit wrong if it happens that julia doesn’t figure out how to call s.f(x) efficiently, but I think it should. If not sure, you’d need to check with @profile and @descend.

1 Like

Type aliases are correctly printed in stack traces:

julia> map(1, [1, 2])
...
 [4] map(f::Int64, A::Vector{Int64})
...

However, Julia only displays aliases defined in the same module as the original type. That’s why your NamedTuple alias is not shown.

1 Like

Did that change at some point? I might be wrong about this, but on 1.7.3:

module M

struct A
    a
end

const B = A

function f(b::B)
    Base.show_backtrace(stderr, backtrace())
    b.a
end

end

M.f(M.A(0))
Stacktrace:
 [1] f(b::Main.M.A)
...
1 Like

Thanks to you both for the observations and clarifications. I think what I was hoping to do is just a bit more doomed than I thought. I managed to put together a working example of what I was thinking about, but I needed to use GeneralizedGenerated.jl and my stack traces are not better than just using a closure, in my opinion. Here is the example:

using GeneralizedGenerated

struct AnonStr{Name,T}
  nt::T
end

# This is just an example, but in my actual application I was hoping to add
# methods to MOI's functions.
function objective end

# If you just make this @generated, you get an error that there's a closure
# inside. But I don't get it, because at the time of compilation isn't the type
# ProbT fully known? I see in the GG docs that functions become closures,
# but I guess I just don't have an intuition for why that limitation exists.
@gg function example(a1::T1, a2::T2, a3::T3) where{T1, T2, T3}
  NT = NamedTuple{(:a1, :a2, :a3), Tuple{T1, T2, T3}}
  ProbT = AnonStr{:MyProblem, NT}
  quote
    myproblem = $ProbT((a1=a1, a2=a2, a3=a3))
    function objective(m::$ProbT, x) 
      log(x/m.nt.a1)*m.nt.a2 + m.nt.a3
    end
    objective(myproblem, 1.1) # change to -1.1 to see stacktrace.
  end
end

# Not only does this seem sort of weird because now every new call to example
# has the potential to re-define a method, but it doesn't even create those
# methods persistently and the stack traces are not helpful:
example(1,2,3)
length(methods(objective)) # zero

My actual application is that I want to use MathOptInterface to interact with optimizers like Ipopt and KNITRO, but for some reasons I don’t really want to get into I don’t want to use JuMP or other existing tools. But I think I’ve basically just sort of discovered-by-doing why they all use macros to define various things like the objective and constraints and stuff as a way to avoid having a million closures.

Anyways, thanks again for giving my question a read and sharing thoughts!

Not a direct answer to your question, but functors (callables that hold data) might be a solution;
there is an example in this issue (fixed):
https://github.com/JuliaNLSolvers/Optim.jl/issues/853

1 Like

Thanks for the link! I actually didn’t think of that option (good to know there is a real name, by the way—I’ve just been referring to them with “structs with methods” for a while). In playing around it seems like I still need to use @eval and some extra generated functions package. But if I can find some option that isn’t crazy and gives me nicer stacktraces than closures I’ll update this thread.

julia> test = (a=1, b="yes", c=1.0, f=log)
(a = 1, b = "yes", c = 1.0, f = log)

julia> const MyStruct = typeof(test)
MyStruct

julia> Base.show(io::IO, ::Type{MyStruct}) = print(io, "MyStruct")

julia> [test]
1-element Vector{MyStruct}:
 (a = 1, b = "yes", c = 1.0, f = log)

julia> test + 1
ERROR: MethodError: no method matching +(::MyStruct, ::Int64)
…
2 Likes

in a fresh REPL

julia> test = (a=1, b="yes", c=1.0, f=log)
(a = 1, b = "yes", c = 1.0, f = log)

julia> const MyStruct = typeof(test)
NamedTuple{(:a, :b, :c, :f), Tuple{Int64, String, Float64, typeof(log)}}

so MyStruct is still a NamedTuple.

Overloading Base.show(io::IO, ::Type{MyStruct}) is thus type piracy
(there nothing specific to the writer,
since MyStruct is just a binding to a type that belongs to Base)
All NamedTuple{(:a, :b, :d, :f), Tuple{Int64, String, Float64, typeof(log)}},
even those from another package would now display the same MyStruct.
(probability is low here, but you see the danger of this approach in general)

 # same type, but constructed through Base, not MyStruct
julia> [(a=2, b="yes", c=2.0, f=log)]
1-element Vector{MyStruct}:
 (a = 2, b = "yes", c = 2.0, f = log)

# changed first key to 'd'
julia> [(d=2, b="yes", c=2.0, f=log)] 
1-element Vector{NamedTuple{(:d, :b, :c, :f), Tuple{Int64, String, Float64, typeof(log)}}}:
 (d = 2, b = "yes", c = 2.0, f = log)

Thanks to you both! I see your point about type piracy. Considering that I actually need to do all of this inside of functions this is just starting to seem like a bad idea. But in case anybody else stumbles on this thread, here is the “solution” to what I was hoping to do:

# @noinline just to make the stack trace show the method.
@noinline eval_f(str, x) = str.f(x)

function fun(x)
  myobject = (a=1, b="yes", c=1.0, f=log)
  myobjectT = typeof(myobject)
  @eval Base.show(io::IO, ::Type{$myobjectT}) = print(io, "MyCoolStruct")
  eval_f(myobject, x)
end

I haven’t looked at the performance implications of doing something like this and it is almost certainly not really advisable just to get a little more information in a stacktrace. Also, I see no reason why I couldn’t just do this to a closure type directly and then get a named closure in the stacktrace.

Anyways. Thanks to all who chimed in to help!

1 Like