Outer constructor not getting called despite being in stack trace?

Somehow Julia’s constructors manage to consistently confuse me. Here is a pared-down example of what I’m trying and failing to do:

const Optional{T} = Union{Some{T}, Nothing}

struct State{InpIterState,Input}
  full_input::Input
  in_iter_state::Optional{InpIterState}
end

function State(input, in_iter_state=nothing)
  let II = Base.notnothing(in_iter_state) ? typeof(something(in_iter_state)) : iter_state_type_for(input)
    State{II,typeof(input)}(input, in_iter_state)
  end
end

iter_state_type_for(::Array) = Int
iter_state_type_for(x) = error("🤷")

But calling State([1,2]) dies a fiery death because it actually ends up calling a type parameterless inner constructor and InpIterState can’t be inferred from a Nothing (I guess the default method argument handling causes State([1,2]) to end up calling the inner constructor?). I was a bit surprised that the outer constructor shows up on the stack trace even though it isn’t actually getting called (I tried replacing the body with just an error("err") and it had no effect):

UndefVarError: `InpIterState` not defined

Stacktrace:
 [1] State(full_input::Vector{Int64}, in_iter_state::Nothing)
   @ Main scratch.ipynb:4
 [2] State(input::Vector{Int64})
   @ Main scratch.ipynb:9
 [3] top-level scope
   @ scratch.ipynb:17

Now, two questions:

  1. why does the stack trace say the outer constructor was called even though it seemingly wasn’t?
  2. how should I be doing this? I want to provide an outer constructor that doesn’t take any type parameters and figures out the correct InpIterState type parameter, and with a default in_iter_state=nothing.

Edit: I think I figured out why the inner constructor’s getting called instead of the outer one:

# 3 methods for type constructor:
State(input) in Main at scratch.ipynb:8
State(full_input::Input, in_iter_state::Union{Nothing, Some{InpIterState}}) where {InpIterState, Input} in Main at scratch.ipynb:4
State(input, in_iter_state) in Main at scratch.ipynb:8

If my understanding is correct the inner constructor is getting called because out of those options it has the most specific matching type? So how do I make it not do that? :grin:

1 Like

Yes, the most specific method is called. Why not just make your outer constructor into an inner constructor instead?

If you don’t want to overwrite it, these should also work:

  • make the outer constructor use keyword arguments State(; input, state=nothing)
  • define a different function make_state(input, state=nothing) that looks exactly like your outer constructor now (just don’t name it State)
  • specify the argument type in the outer constructor: State(input, in_iter_state::Optional{T}=nothing) where T. Then it should have the same signature and be called instead of the inner constructor.
2 Likes

Ohhh I didn’t even think to just provide the parameter type in the outer constructor as well. What I ended up doing was just defining an inner constructor with all the type parameters present, but it feels a bit redundant since it doesn’t really “do” anything like check argument validity etc.

I didn’t want to make the parameterless constructor an inner constructor since I’ll also want to provide at least a State{InpIterState}(inp,is::Optional{InpIterState}) as well, and I’ve understood that generally you’d only want one inner constructor. Then again, I seem to follow best practices pretty selectively since apparently using constructors to provide default type parameters like I’ve been doing and was planning to do with State{InpIterState}(inp,is) is somewhat inadvisable as well due to some potential ambiguities (see this comment)

1 Like

Sure! There isn’t always a single “right” way to do it I think. Personally, I tend to define inner constructors only when checking the argument values for something, like you mentioned. Using outer constructors and make_... functions leaves a bit more flexibility if I realize I have to change something later (I can just add new ways of constructing the object vs. I have to change the inner constructor and perhaps break/update a lot of code).

If you want to provide default type parameters, would a type alias help? Analogously to (roughly) Vector{Int} = Array{Int, 1}.