Are these type functors the same as Outer Constructors?

I found this approach in Rotations.jl. Why code it this way? Note that AngleAxis is a concrete type, so AA can only be one type.

@inline function (::Type{AA})(q::QuatRotation) where AA <: AngleAxis
    w = real(q.q)
    x, y, z = imag_part(q.q)
    s2 = x * x + y * y + z * z
    sin_t2 = sqrt(s2)
    theta = 2 * atan(sin_t2, w)
    num_pert = eps(typeof(theta))^4
    inv_sin_t2 = 1 / (sin_t2 + num_pert)
    return principal_value(AA(theta, inv_sin_t2 * (x + num_pert), inv_sin_t2 * y, inv_sin_t2 * z, false))
end

# Trivial type conversions for RotX, RotY and RotZ
@inline function (::Type{AA})(r::RotX) where AA <: AngleAxis
    return AA(r.theta, 1, 0, 0)
end
@inline function (::Type{AA})(r::RotY) where AA <: AngleAxis
    return AA(r.theta, 0, 1, 0)
end
@inline function (::Type{AA})(r::RotZ) where AA <: AngleAxis
    return AA(r.theta, 0, 0, 1)
end

The full code can be found here:

No. These are callable structs (see Function factories or callable structs?)

I understand they are callable structs, but how are these three functions

different than an outer constructor for AngleAxis? Would an outer constructor have a different type signature?

No the signatures look the same AFAICT. These aren’t really callable structs.

It is a strange way to write a constructor, but I can’t seem to find a difference. This can be used to do certain “Abstract” constructors but AngleAxis is a struct so I’m not sure the purpose.

So I did some basic tests with these two idioms:

outer constructor and
type functor

and indeed I can see no difference in how they are working in the context cited above. So I will mark it as correct, but maybe someone will see this and explain the use case more. Thanks!

I’ve seen this around and never found out why people write ::Type{X} in the callable position, it doesn’t seem to make a difference:

julia> abstract type Y end

julia> struct X<:Y end

julia> (::Type{X})(::Int) = 1

julia> (::Type{blah})(::Float64) where blah<:X = 1.2

julia> (::Type{blah})(::Float64) where blah<:Y = 2.1 # potentially useful

julia> methods(X)
# 4 methods for type constructor:
 [1] X(::Int64)
     @ REPL[3]:1
 [2] X()
     @ REPL[2]:1
 [3] (::Type{blah})(::Float64) where blah<:X
     @ REPL[4]:1
 [4] (::Type{blah})(::Float64) where blah<:Y
     @ REPL[5]:1

Julia sees all of these as type constructor methods, and only the one subtyping the supertype seems to do something beyond the 1 type, and that is indeed a pattern that shows up frequently, check methods(Int).

PS also found out that Julia doesn’t disallow ::DataType and ::Type in the callable position like it disallows ::Function and ::Any, but those are just as bad.

1 Like

One possible reason is just ergonomically defining constructors with all concrete parameters? It gives you access to the constructor you need within the function obviously. If parameters are added you don’t need to worry about changing constructor signatures.

The in-development version of the Manual mentions this: Constructors are just callable objects. This was the PR: doc: manual: constructors: note the correspondence with callable objs by nsajko ¡ Pull Request #53051 ¡ JuliaLang/julia ¡ GitHub