How to analyze complicated struct in julia

some ocdes:

julia> using StatsModels, FixedEffectModels

julia> import StatsModels: capture_call

julia> macro Atest2(ex)
           StatsModels.is_call(ex, :~)
               length(ex.args) == 3
                   StatsModels.terms!(StatsModels.sort_terms!(StatsModels.parse!(ex)))
                   end
@Atest2 (macro with 1 method)

julia> f1 = @Atest2(Sales ~ NDI + fe(State) + fe(Year))
FormulaTerm
Response:
  Sales(unknown)
Predictors:
  NDI(unknown)
  (State)->fe(State)
  (Year)->fe(Year)

julia> typeof(f1)
FormulaTerm{Term, Tuple{Term, FunctionTerm{typeof(fe), var"#3#5", (:State,)}, FunctionTerm{typeof(fe), var"#4#6", (:Year,)}}}
  • Why the type of f1 is some complicated? So how developers of julia to tackle with complicated struct?
  • Or may be I need some references/books to teach me how to design and write a project with julia.

Those types look complicated, but sometimes come out naturally when programming, because of nested struct constructs. They end up in the type fields of the outer type because this is the easiest way to avoid abstract field. For example:

This has a very simple type signature, but all fields are abstract, and will be very bad for performance:

julia> struct A
           x::Function
           y
       end

julia> struct B
           a::A
           b
       end

julia> b = B(A(sin, 1), true)
B(A(sin, 1), true)

julia> typeof(b)
B

Now if you use parametric types to annotate the types of each field, the signatures get complicated quite quickly:

julia> struct C{F,T}
           x::F
           y::T
       end

julia> struct D{A,B}
           a::A
           b::B
       end

julia> d = D(C(sin,1), true)
D{C{typeof(sin), Int64}, Bool}(C{typeof(sin), Int64}(sin, 1), true)

But now all fields are concrete, and performance of operations on these types will be better.

You do not need to parameterize every field (you can use for instance b::Bool, c::Float64, etc, but when the fields contain more complicated objects it starts to be easier just to use a parametric type than to get the correct signature for the field.

thx, I finally understood why this complex struct comes out. So in practice how can we judge an instance with such complicated struct match which method? And developer how to organized structs in pkg?

The functions don’t need to be annotated with all parameters for dispatch. For example:

julia> using StaticArrays

julia> f(x) = 1 # generic
f (generic function with 1 method)

julia> f(x::SVector) = 2 
f (generic function with 2 methods)

julia> f(x::SVector{3}) = 3
f (generic function with 3 methods)

julia> f(x::SVector{3,T}) where T<:Float64 = 4
f (generic function with 4 methods)

julia> f([1,2,2])
1

julia> f(SVector{2,Float64}(1,2))
2

julia> f(SVector{3,Float32}(1,2,3))
3

julia> f(SVector{3,Float64}(1,2,3))
4


More questions:

  • the type of sin is a Function, why it reports typeof(sin)?
  • “The functions don’t need to be annotated with all parameters for dispatch.”. Maybe I understand what you mean, because the specific parameterized types are subtypes of the type itself, so if I want to pass d to a method, I don’t have to care about the type structure inside D, as long as that method can receive the type D
julia> typeof(sin) === Function
false

julia> typeof(sin) <: Function
true

julia> x = rand(3);

julia> typeof(x) === AbstractArray
false

julia> typeof(x) <: AbstractArray
true

Function is abstract.

Note that this means you can dispatch on particular functions, and that a function receiving another function as an input can specialize on that particular function (note that as a heuristic, specialization will generally only occur if the function is called, or specialized is forced via making the type a parameter, e.g foo(f::F) where {F}.

For example

julia> foo(::typeof(sin)) = "sin"
foo (generic function with 1 method)

julia> foo(::Function) = "some other function"
foo (generic function with 2 methods)

julia> foo(cos)
"some other function"

julia> foo(sin)
"sin"
1 Like