Constructors
Constructors are functions that create new objects — specifically, instances of Composite Types. In Julia, type objects also serve as constructor functions: they create new instances of themselves when applied to an argument tuple as a function. This much was already mentioned briefly when composite types were introduced. For example:type Foo
bar
baz
end
julia> foo = Foo(1,2)
Foo(1,2)
julia> foo.bar
1
julia> foo.baz
2
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. Accordingly, you can add functionality to a constructor by simply defining new methods. For example, let’s say you want to add a constructor method for Foo objects that takes only one argument and uses the given value for both the bar and baz fields. This is simple:Foo(x) = Foo(x,x)
julia> Foo(1)
Foo(1,1)
Foo() = Foo(0)
julia> Foo()
Foo(0,0)
Inner Constructor Methods
While outer constructor methods succeed in addressing the problem of providing additional convenience methods for constructing objects, they fail to address the other two use cases mentioned in the introduction of this chapter: enforcing invariants, and allowing construction of self-referential objects. For these problems, one needs inner constructor methods. An inner constructor method is much like an outer constructor method, with two differences:- It is declared inside the block of a type declaration, rather than outside of it like normal methods.
- It has access to a special locally existent function called new that creates objects of the block’s type.
type OrderedPair
x::Real
y::Real
OrderedPair(x,y) = x > y ? error("out of order") : new(x,y)
end
julia> OrderedPair(1,2)
OrderedPair(1,2)
julia> OrderedPair(2,1)
out of order
in OrderedPair at prompt:5
If any inner constructor method is defined, no default constructor method is provided: it is presumed that you have supplied yourself with all the inner constructors you need. The default constructor is equivalent to writing your own inner constructor method that takes all of the object’s fields as parameters (constrained to be of the correct type, if the corresponding field has a type), and passes them to new, returning the resulting object:
type Foo
bar
baz
Foo(bar,baz) = new(bar,baz)
end
type T1
x::Int64
end
type T2
x::Int64
T2(x::Int64) = new(x)
end
julia> T1(1)
T1(1)
julia> T2(1)
T2(1)
julia> T1(1.0)
no method T1(Float64,)
in method_missing at /Users/stefan/projects/julia/base/base.jl:58
julia> T2(1.0)
no method T2(Float64,)
in method_missing at /Users/stefan/projects/julia/base/base.jl:58
Incomplete Initialization
The final problem which has still not been addressed is construction of self-referential objects, or more generally, recursive data structures. Since the fundamental difficulty may not be immediately obvious, let us briefly explain it. Consider the following recursive type declaration:type SelfReferential
obj::SelfReferential
end
b = SelfReferential(a)
To allow for the creation of incompletely initialized objects, Julia allows the new function to be called with fewer than the number of fields that the type has, returning an object with the unspecified fields uninitialized. The inner constructor method can then use the incomplete object, finishing its initialization before returning it. Here, for example, we take another crack at defining the SelfReferential type, with a zero-argument inner constructor returning instances having obj fields pointing to themselves:
type SelfReferential
obj::SelfReferential
SelfReferential() = (x = new(); x.obj = x)
end
x = SelfReferential();
julia> is(x, x)
true
julia> is(x, x.obj)
true
julia> is(x, x.obj.obj)
true
type Incomplete
xx
Incomplete() = new()
end
julia> z = Incomplete();
julia> z.xx
access to undefined reference
type Lazy
xx
Lazy(v) = complete_me(new(), v)
end
Parametric Constructors
Parametric types add a few wrinkles to the constructor story. Recall from Parametric Types that, by default, instances of parametric composite types can be constructed either with explicitly given type parameters or with type parameters implied by the types of the arguments given to the constructor. Here are some examples:type Point{T<:Real}
x::T
y::T
end
## implicit T ##
julia> Point(1,2)
Point(1,2)
julia> Point(1.0,2.5)
Point(1.0,2.5)
julia> Point(1,2.5)
no method Point(Int64,Float64)
in method_missing at /Users/stefan/projects/julia/base/base.jl:58
## explicit T ##
julia> Point{Int64}(1,2)
Point(1,2)
julia> Point{Int64}(1.0,2.5)
no method Point(Float64,Float64)
in method_missing at /Users/stefan/projects/julia/base/base.jl:58
julia> Point{Float64}(1.0,2.5)
Point(1.0,2.5)
julia> Point{Float64}(1,2)
no method Point(Int64,Int64)
in method_missing at /Users/stefan/projects/julia/base/base.jl:58
What’s really going on here is that Point, Point{Float64} and Point{Int64} are all different constructor functions. In fact, Point{T} is a distinct constructor function for each type T. 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. This automatic provision of constructors is equivalent to the following explicit declaration:
type Point{T<:Real}
x::T
y::T
Point(x::T, y::T) = new(x,y)
end
Point{T<:Real}(x::T, y::T) = Point{T}(x,y)
Suppose we wanted to make the constructor call Point(1,2.5) work by “promoting” the integer value 1 to the floating-point value 1.0. The simplest way to achieve this is to define the following additional outer constructor method:
Point(x::Int64, y::Float64) = Point(convert(Float64,x),y)
julia> Point(1,2.5)
Point(1.0,2.5)
julia> typeof(ans)
Point{Float64}
julia> Point(1.5,2)
no method Point(Float64,Int64)
Point(x::Real, y::Real) = Point(promote(x,y)...)
julia> Point(1.5,2)
Point(1.5,2.0)
julia> Point(1,1//2)
Point(1//1,1//2)
julia> Point(1.0,1//2)
Point(1.0,0.5)
Case Study: Rational
Perhaps the best way to tie all these pieces together is to present a real world example of a parametric composite type and its constructor methods. To that end, here is beginning of `rational.jl <https://github.com/JuliaLang/julia/blob/master/base/rational.jl>`_, which implements Julia’s Rational Numbers:type Rational{T<:Integer} <: Real
num::T
den::T
function Rational(num::T, den::T)
if num == 0 && den == 0
error("invalid rational: 0//0")
end
g = gcd(den, num)
num = div(num, g)
den = div(den, g)
new(num, den)
end
end
Rational{T<:Integer}(n::T, d::T) = Rational{T}(n,d)
Rational(n::Integer, d::Integer) = Rational(promote(n,d)...)
Rational(n::Integer) = Rational(n,one(n))
//(n::Integer, d::Integer) = Rational(n,d)
//(x::Rational, y::Integer) = x.num // (x.den*y)
//(x::Integer, y::Rational) = (x*y.den) // y.num
//(x::Complex, y::Real) = complex(real(x)//y, imag(x)//y)
//(x::Real, y::Complex) = x*y'//real(y*y')
function //(x::Complex, y::Complex)
xy = x*y'
yy = real(y*y')
complex(real(xy)//yy, imag(xy)//yy)
end
Now things get interesting. Rational has a single inner constructor method which checks that both of num and den aren’t zero and ensures that every rational is constructed in “lowest terms” with a non-negative denominator. This is accomplished by dividing the given numerator and denominator values by their greatest common divisor, computed using the gcd function. Since gcd returns the greatest common divisor of its arguments with sign matching the first argument (den here), after this division the new value of den is guaranteed to be non-negative. Because this is the only inner constructor for Rational, we can be certain that Rational objects are always constructed in this normalized form.
Rational also provides several outer constructor methods for convenience. The first is the “standard” general constructor that infers the type parameter T from the type of the numerator and denominator when they have the same type. The second applies when the given numerator and denominator values have different types: it promotes them to a common type and then delegates construction to the outer constructor for arguments of matching type. The third outer constructor turns integer values into rationals by supplying a value of 1 as the denominator.
Following the outer constructor definitions, we have a number of methods for the // operator, which provides a syntax for writing rationals. Before these definitions, // is a completely undefined operator with only syntax and no meaning. Afterwards, it behaves just as described in Rational Numbers — its entire behavior is defined in these few lines. The first and most basic definition just makes a//b construct a Rational by applying the Rational constructor to a and b when they are integers. When one of the operands of // is already a rational number, we construct a new rational for the resulting ratio slightly differently; this behavior is actually identical to division of a rational with an integer. Finally, applying // to complex integral values creates an instance of Complex{Rational} — a complex number whose real and imaginary parts are rationals:
julia> (1 + 2im)//(1 - 2im)
-3//5 + 4//5im
julia> typeof(ans)
ComplexPair{Rational{Int64}}
julia> ans <: Complex{Rational}
true
No comments:
Post a Comment
Thank you