User Tools

Site Tools


structs
snippet.juliarepl
julia> pkgchk( [ "julia" => v"1.0.2" ] )

Structs

  • The julia documentation refers to 'struct's as composite types. (There are also composite type types, but we will skip these for now.)
  • Once declared, Structs never have mutable keys (field names). The mutable keyword before the struct states that values held in the keys can be changed.
  • Structs often have a mutable in front of them, because this makes it possible to change their values (not keys) after their initialization.
  • A Dictionary is logically similar—it is like a run-time struct. However dictionary access is syntactically very different: it uses d["key"] instead of d.key.
    • Dictionaries are accessed via [member], while structs are accessed via .member.

Defining a (Nested) Struct

snippet.juliarepl
julia> struct SI; si::Int8; end#struct

julia> struct SE
             v1::Int32; v2::Float64;
             v3::SI;
       end#struct##
  • The type syntax for structs is unusual. Unlike other types which can be defined as, say, v= Vector{Int8}, it is not possible to define a struct with v=struct; si::Int64; end. Instead, the correct syntax is struct v; si::Int64; end.
  • Internally, Julia will maintain a pointer to the SI object in an objects SE.v3; but whenever SE.v3 is used, it is always dereferenced. As always, copy() and deepcopy() can make complete copies. The programmer needs to remember whether copies or near copies refer to the very same identical data (i.e., memory), or to a copy.
  • After assignment, SI and SE objects will be read-only. Place a mutable in front of the struct if you want to change the contents of the struct later on.

Showing the Struct Definition

snippet.juliarepl
julia> struct SI; si::Int8; end;   struct SE; v1::Int32; v2::Float64; v3::SI; end#struct

julia> dump(SE)                    ## the universal inquisitor
SE <: Any
  v1::Int32
  v2::Float64
  v3::SI

julia> fieldnames(SE)              ## introspection
(:v1, :v2, :v3)

Initializing and Initializers

Most custom initializations in Julia are called outer initializations, because they are written as functions outside the actual structure definition. This means that the outer program must know the struct definition and just assigns inputs according to content types. For example,

snippet.juliarepl
julia> struct SI; si::Int8; end;				## inner structure

julia> struct SE; v1::Int32; v2::Float64; v3::SI; end#struct	## nested structure

julia> se= SE(12, 12.2, SI(2))     				## uses the automatic default constructors
SE(12, 12.2, SI(2))

julia> SE( x::Float64 )= SE( trunc(x), x, SI( trunc(x)*2 ) )	## defines a custom outer initializer function
SE

julia> x= SE( 5.2 )                                             ## and uses it
SE(5, 5.2, SI(10))

Here is an example of multiple types of initializers:

snippet.juliarepl
julia> mutable struct ST; f1::Float64; i1::Int64; end#struct

julia> function ST( in1::Float64 )
	   @assert (in1 >= 1.0) && (in1 <= 10.0)      ## Example of basic creation-time range-checking
           ST( Float64(in1), Int64(1) )          ## now calls the default constructor
       end#function##
ST

julia> function ST( in1::Int64 )
	    @assert( (in1 >= 0) && (in1 <= 5) )
            ST( Float64(NaN), Int64(in1) )        ## now calls the default constructor
       end#function##
ST

julia> ST(1.2)					## calls the first constructor
ST(1.2,1)

julia> ST(1.2, 22)				## calls the automatic constructor
ST(1.2, 22)

julia> ST(22)					## calls the second constructor, which does not like 22
ERROR: AssertionError: in1 >= 0 && in1 <= 5
Stacktrace:

WARNING Unlike other languages, Julia discourages inner initializers. You need them only when you want to override the default constructor. There is an example in the appendix.

Accessing Value of a Key/Field in a Struct

snippet.juliarepl
julia> struct SE; v1::Int16; end#struct		## define the sample structure type

julia> se= SE(12);                              ## create the object of this type, and assign value

julia> se.v1                                   ## retrieve the value in the object
12

Various Operations on Structs

Collecting all Values of a Particular Fields of Vector of Structs

snippet.juliarepl
julia> struct S; v::Int; s::String; end#struct

julia> svec= Vector{S}( [ S(1,"one"), S(2,"two"), S(3,"three") ] )
3-element Array{S,1}:
 S(1, "one")
 S(2, "two")
 S(3, "three")

julia> [ svec[i].v for i=1:length(svec) ]	## these "comprehensions" construct vectors
3-element Array{Int64,1}:
 1
 2
 3
 
julia> [ svec[i].s for i=1:length(svec) ]
3-element Array{String,1}:
 "one"
 "two"
 "three"

Eliminating Duplicates From Vector of Structs

snippet.juliarepl
julia> struct S; x::Int; s::String; end#struct

julia> svec= [ S(1, "one"), S(2, "twoa"), S(2, "twob"), S(2, "twoc"), S(3, "three") ]
5-element Array{S,1}:
 S(1, "one")
 S(2, "twoa")
 S(2, "twob")
 S(2, "twoc")
 S(3, "three")

julia> unique( v->v.x, svec )
3-element Array{S,1}:
 S(1, "one")
 S(2, "twoa")
 S(3, "three")
  • You could require that both v.x and v.s are unique, e.g., by creating a concat code from the two.

Keeping The N "Lowest" Structs in Vector of Structs

snippet.juliarepl
julia> struct S; x::Int; s::String; end#struct


julia> svec= [ S(10, "A"), S(2, "B"), S(3, "C"), S(2, "D"), S(3, "E") ]
5-element Array{S,1}:
 S(10, "A")
 S(2, "B")
 S(3, "C")
 S(2, "D")
 S(3, "E")

julia> xonly = map( i -> svec[i].x, 1:length(svec) )	## x is our "Low" Criterion
5-element Array{Int64,1}:
 102
  3
  2
  3

julia> newvec= svec[ sortperm( xonly ) ][1:2]		## 2 is our number that we want to retain
2-element Array{S,1}:
 S(2, "B")
 S(2, "D")
  • you could also define operators for S structs, and use this directly.

Interrogating Mixed Data Struct

The dump function works not only on struct types, but also on struct objects with values:

snippet.juliarepl
julia> struct SI; si::Int8; end;##struct        ## the inner type

julia> struct SE; v1::Int32; v2::Float64; v3::SI; end;#struct##	## the outer type

julia> se= SE(12, 12.2, SI(2));			## the (nested) object

julia> dump(SE)					## the type
SE <: Any
  v1::Int32
  v2::Float64
  v3::SI

julia> dump(se)					## the object
SE
  v1: Int32 12
  v2: Float64 12.2
  v3: SI
    si: Int8 2

Important Reminder: Copies and Deepcopies

See also Arrays and Dictionaries.

Only Assignment (Alias) Can Alter Referenced Structs

snippet.juliarepl
julia> mutable struct Inner; i::Vector{Int}; end#struct Inner

julia> mutable struct Outer; o::Vector{Int}; ri::Inner; end#struct Outer	## nested

julia> original= Outer( [1,2], Inner( [3,4] ) )		## create an Outer object
Outer([1, 2], Inner([3, 4]))

julia> asgn= original					## both asgn and original are aliases to the same struct
Outer([1, 2], Inner([3, 4]))

julia> asgn.o= [5,6]; asgn.ri.i= [7,8];			## tinker with asgn...

julia> original						## ...and it changes the origina, too.
Outer([5, 6], Inner([7, 8]))

Copy and Deepcopy

snippet.juliarepl
julia> mutable struct Inner; i::Vector{Int}; end#struct Inner

julia> mutable struct Outer; o::Vector{Int}; ri::Inner; end#struct Outer

julia> original= Outer( [1,2], Inner( [3,4] ) )		## all as in the previous example
Outer([1, 2], Inner([3, 4]))

julia> cp= copy(original)				## impossible without custom copy method because of nesting
ERROR: MethodError: no method matching copy(::Outer)
Closest candidates are:
  copy(!Matched::Expr) at expr.jl:36
  copy(!Matched::BitSet) at bitset.jl:46
  copy(!Matched::Markdown.MD) at /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v1.0/Markdown/src/parse/parse.jl:30
  ...
Stacktrace:

julia> dpcp= deepcopy(original)				## but deepcopy still works
Outer([1, 2], Inner([3, 4]))

julia> dpcp.o= [5,6]; dpcp.ri.i= [7,8] ;		## try this

julia> dpcp.o[2]=(7); dpcp.ri.i[2]=(9);		## or this,

julia> original						## but the original remains unaffected
Outer([1, 2], Inner([3, 4]))

Defining Broadcast (Facilitating Dot Assignments for Structures)

Ordinary assignment means that a and b point to the same object. Assigning to one changes the other:

snippet.juliarepl
julia> mutable struct Point; x::Float64; y::Float64; end#struct##

julia> a= Point( 1.0, 2.0 ); b= Point( 3.0, 4.0 );

julia> a= b;   b.x= 5.0; a	## only one point is left, other to be garbage collected
Point(5.0, 4.0)

(Note that the julia Pair{Float64,Float64} type would probably be more suitable.)

It is possible to define dot operators for structs to allow element-wise and/or copy operations, like .= (which should presumably copy the contents of one struct to another):

snippet.juliarepl
julia> mutable struct Point; x::Float64; y::Float64; end#struct

julia> function Base.broadcast!(::typeof(identity), dest::Point, src::Point)
	    dest.x= src.x
	    dest.y= src.y
	    dest
	end;#function##

julia> a= Point( 1.0, 2.0 ); b= Point( 3.0, 4.0 );

julia> a.= b;

julia> b.x= 5.0; a		## both points remain.  contents of b were copied into a
Point(1.0, 2.0)
  • Be careful about how deep the broadcast copies

Comparing Structs

WARNING Easy to get wrong

snippet.juliarepl
julia> struct I; x::Int; end;

julia> I(1) == I(1)
true

julia> mutable struct M; x::Int; end;

julia> M(1) == M(1)
false

Parametric Composite Types (Abstract Types, Ala C++ STL)

Structures can be defined to hold all sorts of types, just like Array or Dictionaries can be containers for different content types. The ideas are similar to the functionality in the C++ STL.

snippet.juliarepl
julia> struct Point{T};  x::T; y::T; end#struct		## Point Contents could take any type, truly generic

julia> PointFloat= Point{Float32};			## Pointfloat is a type 

julia> p= PointFloat(1.0,2.0)				## Point{Float32} is a type, p is a variable
Point{Float32}(1.0f0, 2.0f0)

julia> p= Point{Int8}(1.0,2.0)				## this Point type is for Int8's.  p is an object
Point{Int8}(1, 2)

julia> p= Point{Int8}(1.5,2.0)				## you cannot assign Float contents to an Int8's Point.
ERROR: InexactError: Int8(Int8, 1.5)
Stacktrace:

julia> Point{Float16} <: Point				## julia knows that the specific Point type belong to Point
true

julia> Point{Float16} <: Point{Float32}			## but julia does not know about the eltype hierarchy.
false

With the Point type, you could now define methods like

snippet.juliarepl
julia> function normed_distance(p::Pair{Real,Real}); sqrt(first(p)^2+last(p)^2); end#function#
normed_distance (generic function with 1 method)

Sometimes your function needs to use information from the specific type that your generic function caught. In this case,

snippet.julianoeval
[download only julia statements]
julia> function normed_distance(p::Pair{T,T})::T where T<:Real; sqrt(first(p)^2+last(p)^2); end#function
normed_distance (generic function with 1 method)
 
julia> normed_distance( Pair( 2, 3.0 ) )
ERROR: MethodError: no method matching normed_distance(::Pair{Int64,Float64})
Closest candidates are:
  normed_distance(::Pair{T<:Real,T<:Real}) where T<:Real at REPL[2]:1
Stacktrace:

The latter works only if the pair has the same inputs.

Extending Structures (Inheritance)

Full inheritance is not natively supported in Julia. However, it is possible to define abstract types, and to use the macro facility to accomplish similar functionality.

snippet.juliarepl
julia> macro shared_fields_mammals()			## all mammals should have a weight field
	   return esc(:(
	   weight::Int;
	))
	end#macro##
@shared_fields_mammals (macro with 1 method)

julia> abstract type Mammal end

julia> mutable struct Giraffe <: Mammal			## a giraffe is now a struct that belongs to mammals
	    @shared_fields_mammals			## add the weight field
	    spots::Int
	end#struct##

julia> mutable struct Elephant <: Mammal
	    @shared_fields_mammals			## add the weight field
	    fatratio::Float64
	end#struct##

julia> function fatten(animal::T, w::Int) where {T<:Mammal}	## fatten works on any mammal
	    animal.weight += w
            animal
	end#function##
fatten (generic function with 1 method)

julia> a1= Giraffe( 400, 10 )
Giraffe(400, 10)

julia> fatten( a1, 20 )
Giraffe(420, 10)

julia> a2= Elephant( 800, 0.2 )
Elephant(800, 0.2)

julia> fatten( a2, 50 )
Elephant(850, 0.2)

Data Encapsulation

Hiding the internals of structs is not supported by Julia.

Backmatter

Useful Packages on Julia Repository

Notes

  • It is possible but tedious to define two structs, where one extends the other by extra fields. The relationship between the two is then made clear with an abstract and <: declaration.
  • See also show() for custom printing of structs in File IO.
  • There is a bizarre reason for the new() inside inner constructors, which has to do with an ability to replace new().
  • One may need uninitialized structs to construct self-referential objects. See the manual, Chapter 16.3 of julia 0.6.2.
  • promote can often be used in structs for initializers.

References

Appendix

An Inner Initializer

WARNING Unlike other languages, Julia discourages inner initializers. You need them only when you want to override the default constructor. There is an example in the appendix.

snippet.juliarepl
julia> mutable struct SE
           f1::Float64
           i1::Int64

           function SE( in1::Float64, in2::Int64 )        ## an inner constructor
               println("calling SE inner")
               new( Float64(in1+10), Int64(in2+20) )      ## IMPORTANT: Inner must use `new`
           end#function
       end#struct##

julia> function SE( in1::Float64 )
               println("calling SE outer")
               SE( Float64(in1), Int64(1) )         ## now calls the inner constructor
           end#function##
SE

julia> m= SE(2.0, 8); dump(m)
calling SE inner
SE
  f1: Float64 12.0
  i1: Int64 28

julia> m= SE(2.0); dump(m)                            ## good initializations
calling SE outer
calling SE inner
SE
  f1: Float64 12.0
  i1: Int64 19
  • Once you define an inner (non-default) constructor, then Julia no longer defines an inner default constructor. It is then up to you to define it.

"Extending" Built-In Primitive Types with Structs (E.g., Limited Numeric Types)

Though tempting, it is rarely useful for an end-user program to try to define more limited custom numeric types than the standard built-in primitives (e.g., UInt8). There is no speed gain, either. Here is an example of a FiveToTwenty limited numeric type that can, on demand, interoperate with Int64s:

snippet.juliarepl
julia> struct FiveToTwenty <: Unsigned
           val::UInt8
           function FiveToTwenty(val::Number)
		@boundscheck begin @assert val >= 5 && val <= 20 end
		new(UInt8(val))
           end#function
       end#struct##

julia> Base.show(io::IO, v::FiveToTwenty) = print(io, v.val)

julia> FiveToTwenty(4)
ERROR: AssertionError: val >= 5 && val <= 20
Stacktrace:

julia> FiveToTwenty(14)
14

julia> Base.promote_rule(::Type{Int}, ::Type{FiveToTwenty}) = Int

julia> Base.convert(::Type{Int}, x::FiveToTwenty) = Int(x.val)

julia> dump( FiveToTwenty(13) + 10 )                ## note the result type is now Int64
Int64 23

See also [https://discourse.julialang.org/t/floats-or-integers-or-vectors-with-specific-ranges/8682/7]

To Integrate

From https://discourse.julialang.org/t/struggling-with-a-point-type-for-several-dimensions/18066/18

To implement an x,y point type, wrap a tuple, which can be parametrized by the length:

snippet.julianoeval
[download only julia statements]
julia> struct Point{S}
           x::NTuple{S,Float64}
       end
 
julia> p = Point{2}((1.0, 2.0))
Point{2}((1.0, 2.0))
 
julia> p = Point{3}((1.0, 2.0, 3.0))
Point{3}((1.0, 2.0, 3.0))

Tensors.jl 13 for tensor operations like dot, inner/outer products etc.

StaticArrays.jl 4 for general fixed size arrays.

snippet.julianoeval
[download only julia statements]
julia> using StaticArrays
 
julia> struct PointType{S}
           data::MVector{S,Float64}
       end
 
julia> PointType(args...) = PointType(MVector(args...))
PointType
 
julia> tt = PointType(2.,3.,4.)
PointType{3}([2.0, 3.0, 4.0])
 
julia> tt.data[2] = 0.
0.0
 
julia> tt
PointType{3}([2.0, 0.0, 4.0])

or

snippet.julianoeval
[download only julia statements]
julia> using StaticArrays
 
julia> struct Point3D <: FieldVector{3, Float64}
           x::Float64
           y::Float64
           z::Float64
       end
 
julia> p = Point3D(1,2,3)
3-element Point3D:
 1.0
 2.0
 3.0
 
julia> p2 = setindex(p, 5, 1)
3-element Point3D:
 5.0
 2.0
 3.0

StaticArrays are so fast, you can often come out ahead by using StaticArrays even if you are looking for a mutable array.

structs.txt · Last modified: 2018/12/05 20:32 (external edit)