PatrickT
PatrickT

Reputation: 10510

Julia: Parametric types with inner constructor: new and typeof

Trying to understand parametric types and the new function available for inner methods. The manual states "special function available to inner constructors which created a new object of the type". See the section of the manual on new here and the section of the manual on inner constructor methods here.

Consider an inner method designed to calculate the sum of x, where x could be, say, a vector or a tuple, and is given the parametric type T. A natural thing to want is for the type of the elements of x to be inherited by their sum s. I don't seem to need new for that, correct?

struct M{T}
    x::T
    s
    function M(x)
        s = sum(x)
        x,s
    end
end


julia> M([1,2,3])
([1, 2, 3], 6)

julia> M([1.,2.,3.])
([1.0, 2.0, 3.0], 6.0)

julia> typeof(M([1.,2.,3.]))
Tuple{Vector{Float64}, Float64}

Edit: Correction! I intended to have the last line of the inner constructor be M(x,s)... It's still an interesting question, so I won't correct it. How does M(x,s) differ from new{typeof(x)}(x,s)?

One usage of new I have seen is in combination with typeof(), something like:

struct M{T}
    x::T
    s
    function M(x)
        s = sum(x)
        new{typeof(x)}(x,s)
    end
end

julia> M([1,2,3])
M{Vector{Int64}}([1, 2, 3], 6)

julia> M([1.,2.,3.])
M{Vector{Float64}}([1.0, 2.0, 3.0], 6.0)

What if wanted to constrain s to the same type as x? That is, for instance, if x is a vector, then s should be a vector (in this case, a vector of one element). How would I do that? If I replace the last line of the inner constructor with x, new{typeof(x)}(s), I get the understandable error:

MethodError: Cannot `convert` an object of type Int64 to an object of type Vector{Int64}

Upvotes: 6

Views: 1538

Answers (2)

Cameron Bieganek
Cameron Bieganek

Reputation: 7664

Here are the rules:

  1. If you are writing an outer constructor for a type M, the constructor should return an instance of M by eventually calling the inner constructor, like this: M(<args>).

  2. If you are writing an inner constructor, this will override the default inner constructor. So you must return an instance of M by calling new(<args>).

The new "special function" exists to allow the construction of a type that doesn't have a constructor yet. Observe the following example:

julia> struct A
           x::Int
           function A(x)
               A(x)
           end
       end

julia> A(4)
ERROR: StackOverflowError:
Stacktrace:
 [1] A(::Int64) at ./REPL[3]:4 (repeats 79984 times)

This is a circular definition of the constructor for A, which results in a stack overflow. You cannot pull yourself up by your bootstraps, so Julia provides the new function as a way to circumvent this problem.

You should provide the new function with a number of arguments equal to the number of fields in your struct. Note that the new function will attempt to convert the types of its inputs to match the declared types of the fields of your struct:

julia> struct B
           x::Float64
           B(x) = new(x)
       end

julia> B(5)
B(5.0)

julia> B('a')
B(97.0)

julia> B("a")
ERROR: MethodError: Cannot `convert` an object of type String to an object
of type Float64

(The inner constructor for B above is exactly the same as the default inner constructor.)

When you're defining parametric types, the new function must be provided with a number of parameters equal to the number of parameters for your type (and in the same order), analogously to the default inner constructor for parametric types. First observe how the default inner constructor for parametric types is used:

julia> struct Foo{T}
           x::T
       end

julia> Foo{String}("a")
Foo{String}("a")

Now if you were writing an inner constructor for Foo, instead of writing Foo{T}(x) inside the constructor, you would replace the Foo with new, like this: new{T}(x).

You might need typeof to help define the constructor, but often you don't. Here's one way you could define your M type:

struct M{I, T}
    x::I
    s::T

    function M(x::I) where I
        s = sum(x)
        new{I, typeof(s)}(x, s)
    end
end

I'm using typeof here so that I could be any iterable type that returns numbers:

julia> typeof(M(1:3))
M{UnitRange{Int64},Int64}

julia> g = (rand() for _ in 1:10)
Base.Generator{UnitRange{Int64},var"#5#6"}(var"#5#6"(), 1:10)

julia> typeof(M(g))
M{Base.Generator{UnitRange{Int64},var"#5#6"},Float64}

Note that providing the parameters for your type is required when you are using new inside an inner constructor for a parametric type:

julia> struct C{T}
           x::Int
           C(x) = new(x)
       end
ERROR: syntax: too few type parameters specified in "new{...}" around REPL[6]:1

Upvotes: 2

Silvio Mayolo
Silvio Mayolo

Reputation: 70267

Remember, a constructor is designed to construct something. Specifically, the constructor M is designed to construct a value of type M. Your example constructor

struct M{T}
    x::T
    s
    function M(x)
        s = sum(x)
        x,s
    end
end

means that the result of evaluating the expression M([1 2 3]) is a tuple, not an instance of M. If I encountered such a constructor in the wild, I'd assume it was a bug and report it. new is the internal magic that allows you to actually construct a value of type M.

It's a matter of abstraction. If you just want a tuple in the first place, then forget about the structure called M and just define a function m at module scope that returns a tuple. But if you intend to treat this as a special data type, potentially for use with dynamic dispatch but even just for self-documentation purposes, then your constructor should return a value of type M.

Upvotes: 1

Related Questions