39

TL;DR:

  1. what's the accurate definition of inner constructors? In Julia-v0.6+, is it right to say "any constructor that can be called with the signature typename{...}(...)(note the {} part) is an inner constructor"?
  2. As discussed in the comment below, is the outer-only constructor actually an explicit inner constructor?
  3. Is it right to use methods for checking whether a method is an inner/outer constructor?
  4. What's the difference between the default constructors that automatically defined by Julia and the corresponding ones explicitly defined by users?

BTW, I know how to use and when to use an inner constructor. I knew what an inner constructor is until the outer-only constructors come in and muddied the waters. :(

Let's recall some statements from the doc:

1. Outer Constructor Methods

A constructor is just like any other function in Julia in that its overall behavior is defined by the combined behavior of its methods.

2. Inner Constructor Methods

An inner constructor method is much like an outer constructor method, with two differences: 1. It is declared inside the block of a type declaration, rather than outside of it like normal methods. 2. It has access to a special locally existent function called new that creates objects of the block's type.

3. Parametric Constructors

Without any explicitly provided inner constructors, the declaration of the composite type Point{T<:Real} automatically provides an inner constructor, Point{T}, for each possible type T<:Real, that behaves just like non-parametric default inner constructors do. It also provides a single general outer Point constructor that takes pairs of real arguments, which must be of the same type.

I found inner constructor methods can't be directly observed by methods, even methods(Foo{Int}) works, it's actually not "just like any other function", common generic functions cannot be methodsed in this way.

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

julia> methods(Foo)
# 2 methods for generic function "(::Type)":
(::Type{Foo})(x::T) where T in Main at REPL[1]:2  # outer ctor  「1」
(::Type{T})(arg) where T in Base at sysimg.jl:24  # default convertion method「2」

julia> @which Foo{Int}(1) # or methods(Foo{Int})
(::Type{Foo{T}})(x) where T in Main at REPL[1]:2 # inner ctor 「3」

However, the outer-only constructors adds another wrinkle to the constructor story:

julia> struct SummedArray{T<:Number,S<:Number}
           data::Vector{T}
           sum::S
           function SummedArray(a::Vector{T}) where T
               S = widen(T)
               new{T,S}(a, sum(S, a))
           end
       end
julia> methods(SummedArray)
# 2 methods for generic function "(::Type)":
(::Type{SummedArray})(a::Array{T,1}) where T in Main at REPL[1]:5 # outer ctor「4」
(::Type{T})(arg) where T in Base at sysimg.jl:24

Hmmm, an outer constructor IN a type declaration block, and it calls new as well. I guess the purpose here is just to prevent Julia from defining the default inner-outer constructor pair for us, but is the second statement from the documentation still true in this case? It's confusing to new users.

Here, I read another form of inner constructors:

julia> struct Foo{T}
     x::T
     (::Type{Foo{T}})(x::T) = new{T}(x) 
   end

julia> methods(Foo)
# 1 method for generic function "(::Type)":
(::Type{T})(arg) where T in Base at sysimg.jl:24

julia> methods(Foo{Int})
# 2 methods for generic function "(::Type)":
(::Type{Foo{T}})(x::T) where T in Main at REPL[2]:3  「5」
(::Type{T})(arg) where T in Base at sysimg.jl:24

It's far from the canonical form Foo{T}(x::T) where {T} = new(x) but it seems the results are quite the same.

So my question is what's the accurate definition of inner constructors? In Julia-v0.6+, is it right to say "any constructor that can be called with the signature typename{...}(...)(note the {} part) is an inner constructor"?

Gnimuc
  • 8,098
  • 2
  • 36
  • 50
  • 2
    My 2¢ is that an inner constructor is used when you want to bypass the default outer constructor (whether an implicit or an explicit one), in order to perform initialisations / tests etc before you return the instance. When an inner constructor exists, the default outer one no longer applies unless you define one explicitly. I disagree with what you call inner / outer constructor on some of the examples above (but that may just be a typo). Also see [this question](https://stackoverflow.com/q/39133424/4183191) (disclaimer: mine) as an example of where an inner constructor is appropriate. – Tasos Papastylianou Jul 26 '17 at 14:44
  • @TasosPapastylianou I've added a reference after each constructor, could you please point out the examples you disagree? I'm not sure whether what I thought is right or not, so any information is appreciated. I know the use-case of inner constructors, what I would like to know is the accurate definition, since it's hard to merely explain a concept by how to use it ;) – Gnimuc Jul 26 '17 at 15:03
  • 2
    I think the distinction between inner and outer here confuses the issue. The issue is more one of default vs explicit vs implicit vs specialised vs generalised. What the doc says is that when explicit inner constructors are not provided, there exists default constructors that are equivalent to certain explicit inner constructor formulations. So I would call [1] the generalised default constructor, [3] the specialised default constructor, [4] is an explicit inner constructor (which also happens to be parameterised), and so is [5] (though, written in a slightly convoluted manner). – Tasos Papastylianou Jul 26 '17 at 15:32
  • @TasosPapastylianou [4] is an explicit **inner** constructor? If I undertood correctly, the doc means `SummedArray` doesn't have an inner constructor in this example, which is why they call it `outer-only` constructor. (I'm not a native speaker, maybe I just misunderstood the doc?) – Gnimuc Jul 27 '17 at 00:49
  • Your confusion over that passage is totally understandable, I think the way that passage is written could be improved. But I think the point of that passage is this: In the _absence_ of an explicitly defined _inner_ constructor, there is a number of default constructors that materialize, which are equivalent (and may well be implemented in this way under the hood) to a bunch of particular inner and outer constructors that are generated automatically. However, if you _explicitly_ define an inner constructor, then these defaults do not get generated. – Tasos Papastylianou Jul 27 '17 at 09:11
  • So the point of that passage is that, if you want to _prevent_ users from using the default constructors (because such use would be against what you wanted them to be able to do), you need to create an _explicit_ inner constructor, which does what you want, and also prevents any default constructors from being generated. Note that your explicit inner constructor does not necessarily need to have the same type and number of arguments as the default constructors would have, but in that example, it just happens to be the case because of the point they're trying to make. – Tasos Papastylianou Jul 27 '17 at 09:14
  • Compare this to C++, where if you _don't_ explicitly provide a constructor for a class, then there are certain default constructors that get generated for you automatically (i.e. the 'empty' constructor, the 'copy' constructor, and the 'copy assignment' constructor). – Tasos Papastylianou Jul 27 '17 at 09:16
  • @TasosPapastylianou The problem here is the terminology "inner/outer" is defined very vaguely(this is the reason why I asked the question), and it's only a Julia thing, why not just constructor? The doc should be improved, but in what way? – Gnimuc Jul 29 '17 at 01:27
  • Isn't an *inner constructor* simply a constructor which has access to `new` ? (this definition is simple, and if it isn't correct, it is what I assumed naturally, which should make it a good definition) – Dan Getz Jul 29 '17 at 08:49
  • Name-wise, the definition is pretty intuitive to me. An _inner_ constructor is one that is defined _inside_ the type definition, an _outer_ constructor is one that is defined _outside_. From an implementation point of view, yes, the inner one has access to new, and you could think of outer ones as glorified wrappers to inner constructors. What I think isn't clear to you is to what extent default implementations involve implicit inner or outer constructors, and the answer is _both_. As in the SummedArray example.The main point is all implicitly defined constructors (inner or outer) [...] – Tasos Papastylianou Jul 29 '17 at 08:55
  • [...] disappear in the presence of even a single explicitly defined inner constructor. When an inner constructor exists, all other constructors (whether secondary inner constructors, or outer constructors acting as glorified wrappers to the inner constructors) all need to be defined explicitly. – Tasos Papastylianou Jul 29 '17 at 08:55
  • @TasosPapastylianou makes sense to me. the section title -- [outer-only constructor](https://docs.julialang.org/en/latest/manual/constructors/#Outer-only-constructors-1) should be renamed in order to be in compatible with the definition. – Gnimuc Jul 29 '17 at 09:04
  • 2
    @TasosPapastylianou Yeah, the mechanics of automatic inner and outer constructors is important (and thanks for clearing it up), but in fact, I can see it change in the future. The concept of inner constructors allocating and generating consistent structs and outer constructors wrapping those to give a variety of creation methods is the core of the definitions. In this sense, I can even see the ability to define `new` calling inner constructors outside struct definition. Perhaps even overwriting an inner constructor for additional constraints on some specific type parameters might be convenient – Dan Getz Jul 29 '17 at 09:17
  • 2
    @Gnimuc I agree, it could be clearer. The title focus should probably have been about the range of implied default inner (and outer) parametric constructors that are available, and how these no longer apply if an explicit inner constructor is defined. The fact that if this is the case, you can then only rely on creating appropriate outer constructors that are effectively wrappers around the explicit inner constructor, and that you can no longer rely on implicit constructors that have not been activated, follows naturally. – Tasos Papastylianou Jul 29 '17 at 09:21
  • 2
    thanks for your replies! I just filed an issue here https://github.com/JuliaLang/julia/issues/23022, let's move the discussion there. – Gnimuc Jul 29 '17 at 09:21
  • "an outer constructor IN a type declaration block..": No, wrong, according to doc, paragraph 2, "IN declaration block" implies inner constructor – stefan bachert Aug 13 '17 at 08:44
  • @stefanbachert Hi, thanks for the reply. I was doubting the doc about this part. According to what Jeff told in this [post](https://github.com/JuliaLang/julia/issues/8135#issuecomment-53462575): "An outer constructor will just be a method of Type{A}, and an inner constructor will be a method of Type{A{N}}.", that constructor is indeed an "outer"-constructor IN declaration block. If we insist to use the current definition, it's obvious we will have two different kinds of inner constructors, which I think breaks the consistency of the concept. – Gnimuc Aug 13 '17 at 08:59
  • 1
    The doc on "outer-only" ctors, as of today is still rather confusing, and I continue to have some uncertainty after several hours of head scratching. Having to trawl through github issues spanning various changing julia versions to get an answer is not a good thing. I'm an experienced programmer, not a science/mathy type, so that should be even more of a worry to the doc writers. Hate to be a whiner, but somebody in the know should really look at this one. – polypus74 Nov 12 '17 at 14:55
  • I’m voting to close this question because it's obsoleted. – Gnimuc Sep 21 '22 at 15:02

1 Answers1

24

By the way of example, suppose you want to define a type to represent even numbers:

julia> struct Even
          e::Int
       end

julia> Even(2)
Even(2)

So far so good, but you also want the constructor to reject odd numbers and so far Even(x) does not:

julia> Even(3)
Even(3)

So you attempt to write your own constructor as

julia> Even(x) = iseven(x) ? Even(x) : throw(ArgumentError("x=$x is odd"))
Even

and ... drum roll, please ... It does not work:

julia> Even(3)
Even(3)

Why? Let's ask Julia what she has just called:

julia> @which Even(3)
Even(e::Int64) in Main at REPL[1]:2

This is not the method that you defined (look at the argument name and the type), this is the implicitly provided constructor. Maybe we should redefine that? Well, don't try this at home:

julia> Even(e::Int) = iseven(e) ? Even(e) : throw(ArgumentError("e=$e is odd"))
Even
julia> Even(2)
ERROR: StackOverflowError:
Stacktrace:
 [1] Even(::Int64) at ./REPL[11]:0
 [2] Even(::Int64) at ./REPL[11]:1 (repeats 65497 times) 

We've just created an infinite loop: we redefined Even(e) to recursively call itself. We are now facing a chicken and egg problem: we want to redefine the implicit constructor, but we need some other constructor to call in the defined function. As we've seen calling Even(e) is not a viable option.

The solution is to define an inner constructor:

julia> struct Even
          e::Int
          Even(e::Int) = iseven(e) ? new(e) : throw(ArgumentError("e=$e is odd"))
       end


julia> Even(2)
Even(2)

julia> Even(3)
ERROR: ArgumentError: e=3 is odd
..

Inside an inner constructor, you can call the original implicit constructor using the new() syntax. This syntax is not available to the outer constructors. If you try to use it, you'll get an error:

julia> Even() = new(2)
Even

julia> Even()
ERROR: UndefVarError: new not defined 
..
PatrickT
  • 10,037
  • 9
  • 76
  • 111
Alexander Belopolsky
  • 2,228
  • 10
  • 26